Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
baa8700
fix(search): count a price-basis qualifier as met when the answer is …
jm-rivera Oct 20, 2026
d083a69
feat(search): offer each choice refusal's options as ready requests
jm-rivera Oct 20, 2026
c18924f
fix(search): count words for every member of a role as met when the a…
jm-rivera Oct 20, 2026
b011128
Merge the price-basis and every-member qualifier fixes into the choic…
jm-rivera Oct 20, 2026
89c50f1
fix(search): name every ambiguous currency in its option and judge ev…
jm-rivera Oct 20, 2026
ce3fbc6
fix(search): offer readings when a direction's coverage is unknown an…
jm-rivera Oct 20, 2026
c2f933f
fix(search): depart per part on an every-member phrase a sibling part…
jm-rivera Oct 20, 2026
e2323e2
fix(search): take role-option mentions from the readings and pin the …
jm-rivera Oct 20, 2026
d135153
fix(search): name a role refusal's untold places by the request's own…
jm-rivera Oct 20, 2026
2c39697
fix(search): word a role refusal by its untold places, naming one pla…
jm-rivera Oct 20, 2026
a4c7210
fix(search): name each untold place once in role option labels, and d…
jm-rivera Oct 20, 2026
a7e99ff
docs(search): word the choice-option and qualifier comments as plain …
jm-rivera Oct 20, 2026
4553224
docs(search): list the three cases that offer no choice options
jm-rivera Oct 21, 2026
9b55683
feat(search): state each answer's assumptions and open a mixed answer…
jm-rivera Oct 20, 2026
cb94121
fix(search): match a resubmitted assumption by its values and mark ki…
jm-rivera Oct 20, 2026
d229f5c
fix(search): word the place scope by member type and keep asked roles…
jm-rivera Oct 20, 2026
6123036
test(search): recompute the sentence golden's assumptions from its re…
jm-rivera Oct 20, 2026
55b8095
fix(search): count ranked axes as asked, name defaults by label, and …
jm-rivera Oct 20, 2026
46bd3da
test(search): re-record the sentence golden with its slot filters
jm-rivera Oct 20, 2026
24b0d2d
docs(search): number the answer-statement clauses C193 to C195
jm-rivera Oct 20, 2026
2985ea0
fix(search): keep a merged part's declared default from its own request
jm-rivera Oct 20, 2026
1069a3d
feat(search): list each candidate reading's own assumptions
jm-rivera Oct 20, 2026
54cedec
feat(search): echo each candidate reading's assumptions on a resubmit…
jm-rivera Oct 20, 2026
c0ee284
fix(search): keep a merged part's default on an unchanged repost, and…
jm-rivera Oct 20, 2026
14fcec0
fix(search): keep a folded part's assumptions beside a separate part …
jm-rivera Oct 20, 2026
06635c4
feat(search): send the mixed opener as its own coverage segment
jm-rivera Oct 20, 2026
268e5ce
docs(search): word the assumptions, clause and opener text as plain s…
jm-rivera Oct 21, 2026
89c7992
Merge branch 'dev' into fix/search-choice-options
jm-rivera Oct 21, 2026
b128325
fix(search): name a where-bound place as the destination, and pin the…
jm-rivera Oct 21, 2026
6d1bf9e
Merge branch 'fix/search-choice-options' into feat/search-answer-stat…
jm-rivera Oct 21, 2026
0696bb6
fix(search): keep one resubmitted reading per spec_id, state a place …
jm-rivera Oct 21, 2026
95f9df8
fix(search): word a resubmit that rebuilds to one reading, keep a sco…
jm-rivera Oct 21, 2026
70e41ba
fix(search): count every resolving candidate in a collapsed set's war…
jm-rivera Oct 21, 2026
29c973d
docs(search): name the coverage and assumption segments in the older …
jm-rivera Oct 21, 2026
90777ec
docs(search): say the README opener needs an answered part, and list …
jm-rivera Oct 21, 2026
4ce6adb
Merge branch 'dev' into fix/search-choice-options
jm-rivera Oct 22, 2026
daea6f4
Merge branch 'fix/search-choice-options' into feat/search-answer-stat…
jm-rivera Oct 22, 2026
cf57610
fix(search): omit source choices that empty existing scope
jm-rivera Oct 22, 2026
efcf813
chore(search): integrate validated source choices
jm-rivera Oct 22, 2026
f4dd484
fix(search): check every choice option and refuse contradictory sources
jm-rivera Oct 22, 2026
9529b0e
Merge branch 'fix/search-choice-options' into feat/search-answer-stat…
jm-rivera Oct 22, 2026
06e1059
Merge dev into the choice-option checks
jm-rivera Oct 23, 2026
7d559d8
Merge the choice-option checks into the answer statements
jm-rivera Oct 23, 2026
e2b1d3e
Merge dev into the answer statements
jm-rivera Oct 23, 2026
a9021bf
Merge dev into the answer statements
jm-rivera Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions corpus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ The status keys `definite`, `candidates`, `no_data`, `unsupported` and `mixed` a
| `unit` | shared or status | `{currency, prices, base_year}` as the identity's unit request, or `none`. |
| `operations` | shared or status | `[{verb, over, target_year}]` in query order; `[]` asserts none. |
| `warnings` | shared or status | Warning codes the answer must carry; extra warnings pass. |
| `assumptions` | shared or status | `[{kind, binding_ref, text}]` in clause order; `kind` is required and the others are asserted when set. `[]` asserts none. |
| `skills` | `definite` | `{present, ids}`. |
| `include_families` | `candidates` | Families the offered candidates must include. |
| `reason`, `category`, `gate` | `no_data` | The refusal; `reason` may be a list of accepted reasons. |
Expand Down
13 changes: 8 additions & 5 deletions docs/conformance-search-api-v1.md

Large diffs are not rendered by default.

71 changes: 57 additions & 14 deletions scripts/sentence_golden.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@

- `id`, `query`: the panel query.
- `rendered_sentence`, `sentence_plan`: the envelope sentence and plan.
- `parts`: each part with its status, `variable_text`, warnings, departures and own
sentence; a definite part's Spec, a candidates part's shown Specs, a no-data part's reason.
- `parts`: each part with its status, `variable_text`, warnings, departures, assumptions,
operations and own sentence; a definite part's Spec, a candidates part's shown Specs, a
no-data part's reason.

A Spec keeps its definition and the resolved fields `render_sentence_plan` reads (shape,
slots, StatVar counts and served dates, entities and groups, coverage, scope, facets, slot
options, open-axis and role-axis facts, measure label, the trace's sources) with its recorded
sentence. Beside `resolved` each Spec stores `sentence_facts`, what the renderer reads that
the wire does not carry: the facts of its served members (`search.models.SentenceFacts`),
options, open-axis and role-axis facts, measure label, the trace's sources and slot filters)
with its recorded sentence. The slot filters carry each slot's key, binding kind, values and
provenance, so a replay reads every declared default the answer stated. Beside `resolved`
each Spec stores `sentence_facts`, what the renderer reads that the wire does not carry: the
facts of its served members (`search.models.SentenceFacts`),
which the in-process panel run records from each answer, and, for a Spec whose measure label
names code pairs, those pairs (`measure_label_named_values`), which the script derives from
the Spec's `measure_axis_values` and the measure index of the release `GRAPH_RELEASE_URI` names,
as `spec_build.measure_label_for_partition` does. A replay states both on the Spec it rebuilds
(`golden_parts`). Observations, timing, diagnostics, skills, entity and facet options and the
rest of the trace go; the model fields that are required but unread (the trace's entity and
filter lists) are written empty, and a role-axis fact's `member_count`, which the wire omits
when it is null, is restored as null. A no-data part keeps only its reason, since the envelope
rest of the trace go; the trace's entity list, required but unread, is written empty, and a
role-axis fact's `member_count`, which the wire omits when it is null, is restored as null.
A no-data part keeps only its reason, since the envelope
composer reads no other `no_data` field.

The files are gzipped: the 182-query golden is about 370 kB compressed against 4.8 MB
Expand All @@ -42,6 +45,16 @@
part's candidate plans and its own plan, and the envelope plan, with the sentence of each.
A no-data part's own plan needs inputs the wire does not carry, so it stays as recorded.

A recording keeps each part's assumptions as the answer gave them. A re-render recomputes
them from what the golden holds: a definite part's from its Spec and operations
(`assumptions_for`) and a candidates part's as the items every shown Spec's list so read
holds alike (`common_assumptions`), as `engine.assemble` does. The golden records no
every-member axes ("all donors") and no resubmit echo, so a re-render states an open role on
an axis the request's words asked every member of. The golden also lacks the Spec each part
of a same-shape merge or a fold resolved to before the merge, from which that part reads its
declared defaults. A definite part whose Spec another part of the line shares therefore
keeps the assumptions it recorded, as does a part of another status.

Usage:
just sentence-panel --in-process
just sentence-golden runs/sentence-panel
Expand All @@ -57,6 +70,7 @@
import json
import sys
import tempfile
from collections import Counter
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
Expand All @@ -68,13 +82,15 @@
from code_index.types import label_for_pairs
from graph_release.context import bound_served_release
from search.models import (
Assumption,
CandidatesPart,
DefinitePart,
SentenceFacts,
SentenceToken,
Spec,
)
from search.models.response import Part
from search.render.assumptions import assumptions_for, common_assumptions
from search.render.candidates import candidate_differences, candidates_sentence_plan
from search.render.envelope import compose_envelope_sentence
from search.render.heading import declared_axis_names, sibling_how_properties
Expand All @@ -88,6 +104,8 @@
"variable_text",
"warnings",
"departures",
"assumptions",
"operations",
"rendered_sentence",
"sentence_plan",
)
Expand All @@ -109,6 +127,10 @@
"sentence_plan",
)

# The fields of a `trace.slot_filters` entry that the assumptions read: a declared default's
# slot, values and provenance.
_SLOT_FILTER_KEYS = ("key", "binding_kind", "refs", "provenance")

_PARTS = TypeAdapter(list[Part])
_PLAN = TypeAdapter(list[SentenceToken])

Expand Down Expand Up @@ -186,7 +208,10 @@ def trim_spec(spec: dict[str, Any], facts: dict[str, Any]) -> dict[str, Any]:
trimmed["trace"] = {
"resolved_sources": resolved["trace"]["resolved_sources"],
"resolved_entities": [],
"slot_filters": [],
"slot_filters": [
_pick(slot_filter, _SLOT_FILTER_KEYS)
for slot_filter in resolved["trace"]["slot_filters"]
],
}
if role_facts := resolved.get("role_axis_facts"):
# The wire omits null fields and `RoleAxisFact.member_count` is required but
Expand All @@ -203,8 +228,8 @@ def trim_spec(spec: dict[str, Any], facts: dict[str, Any]) -> dict[str, Any]:


def trim_part(part: dict[str, Any], facts: dict[str, dict[str, Any]]) -> dict[str, Any]:
"""`part` with its own sentence, departures and warnings, and the inputs the renderer
reads for its status, each Spec with its entry in `facts`, keyed by spec id."""
"""`part` with its own sentence, departures, assumptions and warnings, and the inputs
the renderer reads for its status, each Spec with its entry in `facts`, keyed by spec id."""
trimmed = _pick(part, _PART_KEYS)
match part["status"]:
case "definite":
Expand Down Expand Up @@ -317,24 +342,42 @@ def _set_plan(target: dict[str, Any], plan: list[SentenceToken]) -> None:
target["rendered_sentence"] = sentence_from_plan(plan)


def _set_assumptions(raw: dict[str, Any], assumptions: list[Assumption]) -> None:
"""Store `assumptions` on the part `raw` as the wire writes them."""
raw["assumptions"] = [item.model_dump(mode="json", exclude_none=True) for item in assumptions]


def rerender_line(line: dict[str, Any]) -> dict[str, Any]:
"""`line` with every plan the golden test compares re-rendered by the current renderer
and every input unchanged."""
"""`line` with every plan the golden test compares re-rendered by the current renderer,
each assumptions list recomputed from the Spec but those of definite parts that share
one, and every other input unchanged."""
parts = golden_parts(line["parts"])
declarations = declared_axis_names()
spec_ids = Counter(part.spec.spec_id for part in parts if isinstance(part, DefinitePart))
for part, raw in zip(parts, line["parts"], strict=True):
if isinstance(part, DefinitePart):
plan = render_sentence_plan(part.spec, axis_declarations=declarations)
_set_plan(raw["spec"]["resolved"], plan)
# A Spec two parts share served a merge or a fold, whose parts read their
# declared defaults from their own pre-merge Specs, which the golden lacks.
if spec_ids[part.spec.spec_id] == 1:
_set_assumptions(raw, assumptions_for(part.spec, operations=part.operations))
elif isinstance(part, CandidatesPart):
specs = part.candidates.specs
raw_specs = raw["candidates"]["specs"]
for raw_spec, plan in zip(raw_specs, rerendered_candidates(specs), strict=True):
_set_plan(raw_spec["resolved"], plan)
assumptions = common_assumptions(
[assumptions_for(spec, operations=part.operations) for spec in specs]
)
_set_assumptions(raw, assumptions)
_set_plan(
raw,
candidates_sentence_plan(
specs, truncated=part.candidates.truncated, departures=part.departures
specs,
truncated=part.candidates.truncated,
departures=part.departures,
assumptions=assumptions,
),
)
composed = compose_envelope_sentence(golden_parts(line["parts"]))
Expand Down
Loading
Loading