Skip to content

Commit 41015bf

Browse files
Merge pull request #33 from Simon-McIntosh/main
A domain reduction leads its name and wraps the projection
2 parents a2e4ae8 + fc56e34 commit 41015bf

11 files changed

Lines changed: 469 additions & 47 deletions

File tree

docs/plans/grammar-release-and-downstream-pin.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<link rel="stylesheet" href="/_shared/foundation.css">
2424
<link rel="stylesheet" href="/_shared/dashboard.css">
2525
<meta name="plan-modified" content="2026-09-06">
26-
<meta name="plan-version" content="1">
26+
<meta name="plan-version" content="2">
2727
</head>
2828
<body>
2929
<main class="plan-doc">
@@ -157,6 +157,9 @@ <h2><span class="sec">§</span> Decisions</h2>
157157
<div class="r-comment" data-section="s2" data-id="c-run-r-20260906T102805767185-n-grdp-the-kind-pin-tolerates-new-bases" data-who="cx-kindpin" data-when="2026-09-06T10:46:05Z">
158158
<div class="r-comment-body"><p>THE CONTRACT NOW MEASURES WHAT ITS NAME CLAIMS. A frozen SHA-256 digest over the entire physical-base vocabulary is replaced by an explicit 177-entry baseline mapping measured from the loader, projected onto only those named keys - so an existing base changing kind fails while an added base passes. 1 failed at base against 3 passed after, and the whole grammar suite is green at 2110 passed, 34 skipped, 82 xfailed, exit 0. THE WORKER DISPUTED MY SPEC AND WAS RIGHT, which is the most valuable thing here. I told it to also assert that no base outside the three vector corrections carries kind vector, reading the test name as only these are vector. The name says only these CHANGE TO vector, and the loader holds 16 established vector bases outside the corrections, so my assertion was false of this vocabulary and it stopped rather than writing something that could not pass. It also confirmed my reading of the third assertion with a proper argument rather than by agreement: the old baseline copied every current value and overrode only the corrections to scalar, so once the first assertion held, exactly those three necessarily differed and no other key could - no independent failure case existed. TWO-DIRECTION PROOF, both concrete: flipping loaded acceleration from vector to scalar must raise, and inserting a synthetic base must still pass. The census is the part a reader gains most from - 177 bases, 19 vectors, 16 of them established outside the corrections, zero missing current keys and zero stale baseline keys - because the frozen digest had hidden every one of those facts behind one opaque hash. RECEIPT, and the first codex run today to take two turns: 15m50s of a 40m fence, 65 tool calls, 2 turns, zero provider refusals, three self-corrected local retries (an unsupported patch operation, a malformed poll, an over-escaped f-string). Its named discovery was measuring and transcribing the complete base map, which is exactly the work a digest exists to avoid and exactly why the digest was the wrong instrument</p></div>
159159
</div>
160+
<div class="r-comment" data-section="s1" data-id="c-run-r-20260906T114004346214-n-grdp-the-reduction-operator-leads-the-name" data-who="cx-kindpin" data-when="2026-09-06T13:31:28.078Z">
161+
<div class="r-comment-body"><p>A DOMAIN REDUCTION NOW LEADS ITS NAME AND WRAPS THE PROJECTION, and I verified the deliverable rather than the gate because the second attempt passed its whole suite while being wrong. The measure holds on every axis: IR of the lead form equals IR of the authored form, compose of the authored form returns flux_surface_averaged_toroidal_lithium_velocity_at_plasma_boundary which is the specified spelling exactly, and the reduction appears in operators rather than qualifiers. Both negative controls stay byte-identical including the recursive one. All 21 live migrations are the leading form, 2326 of 2347 byte-identical, full suite 2132 passed and 0 failed against a 2110 baseline. THE SECOND ATTEMPT WAS THE INSTRUCTIVE ONE: it made the correct spelling legal and round-tripping while leaving the authored names classifying the reduction as a qualifier, so the grammar admitted TWO canonical spellings and treated them as different meanings - worse than the single wrong answer it started from, and entirely invisible to a green suite. Only comparing representations caught it. IT CORRECTED ME THREE TIMES ACROSS THREE ATTEMPTS and every correction stood. My either-parser-or-renderer framing was a false dichotomy: a base-qualifier-projection expression outranks the operator peel, then the renderer faithfully trails what the parser mis-classified. My instruction to revert the parser edit and fix only the flat model was WRONG because _strict_validate holds two independent canonicality checks and only one passes through _model_to_ir - my six-call-site measurement found every caller of that function but never asked whether canonicality had a path around it. And my placement rule needed two conditions, joiner-free AND precedence 30, where I gave one; the single-condition version breaks seven change_in and normalized contracts. The final shape encodes both as _REDUCTION_PRECEDENCE with a bare_prefix test, keeps the parser edit that could not be reverted, reverts render.py entirely, and records the rule beside the vocabulary so it is not read as an accident. Eight contract parameters were repointed under my authorisation and the ir.py comment documenting the old form as intent was updated with them. gyroaveraged stays unary_postfix at precedence 10 against bare-prefix-30 siblings, reported and unfixed as instructed</p></div>
162+
</div>
160163
</section>
161164
</main>
162165
</body>

docs/state/imas-standard-names/crew.json

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"updated": "2026-09-06T12:48:18",
2+
"updated": "2026-09-06T15:41:19",
33
"project": "imas-standard-names",
44
"doc": "crew",
55
"data": {
@@ -11,7 +11,8 @@
1111
"session_id": "01a07642-fbe2-7d02-97de-ed56606becbb",
1212
"session_model": "gpt-5.6-sol",
1313
"sessions": {
14-
"{\"alias\":\"sol5.6\",\"backend\":\"codex\",\"effort\":\"high\",\"launch\":\"cli\",\"model\":\"gpt-5.6-sol\",\"sandbox\":\"worktree-full\"}": "01a07642-fbe2-7d02-97de-ed56606becbb"
14+
"{\"alias\":\"sol5.6\",\"backend\":\"codex\",\"effort\":\"high\",\"launch\":\"cli\",\"model\":\"gpt-5.6-sol\",\"sandbox\":\"worktree-full\"}": "01a07642-fbe2-7d02-97de-ed56606becbb",
15+
"{\"alias\":\"opus 5\",\"backend\":\"claude-opus\",\"effort\":\"high\",\"launch\":\"cli\",\"model\":\"claude-opus-5\",\"sandbox\":\"worktree-full\"}": "451f0139-ecb1-41fc-bfc4-ff33f8bff921"
1516
},
1617
"created": "2026-09-06"
1718
}
@@ -154,9 +155,164 @@
154155
"role": "implement",
155156
"status": "compatible"
156157
}
158+
},
159+
{
160+
"run_id": "r-20260906T114004346214-n-grdp-the-reduction-operator-leads-the-name",
161+
"plan": "grammar-release-and-downstream-pin",
162+
"section": "\u00a71",
163+
"node": "n-grdp-the-reduction-operator-leads-the-name",
164+
"node_definition": {
165+
"coordinator": {
166+
"authoring_turn": {
167+
"source": "claude-code-session-transcript",
168+
"status": "measured",
169+
"tokens": {
170+
"cache_creation_input_tokens": 895,
171+
"cached_input_tokens": 672457,
172+
"input_tokens": 673354,
173+
"output_tokens": 4220,
174+
"reasoning_output_tokens": 1664,
175+
"total_tokens": 677574,
176+
"uncached_input_tokens": 2
177+
}
178+
},
179+
"harness": "claude-code",
180+
"runtime_session_id": "11b50f59-c77b-4356-8bb3-6dafc6d0a4c0",
181+
"session_id": "ship-s10-20260905"
182+
},
183+
"done_when": "A CLASSIFICATION DEFECT IN THE PARSER, NOT A PLACEMENT RULE. The lead has specified the correct behaviour and the evidence localises the cause. Do not change the tail-relocation rule; it is not implicated.\n\nTHE EVIDENCE, measured against the live grammar. These three spellings all parse to an IDENTICAL intermediate representation:\n toroidal_flux_surface_averaged_lithium_velocity_at_plasma_boundary\n flux_surface_averaged_toroidal_lithium_velocity_at_plasma_boundary\n toroidal_lithium_velocity_flux_surface_averaged_at_plasma_boundary\nand that representation is:\n operators: []\n projection: {axis: toroidal, shape: component}\n qualifiers: [{token: flux_surface_averaged}, {token: lithium}]\n base: {token: velocity, kind: quantity}\n locus: {relation: at, token: plasma_boundary, type: position}\n\nSo `flux_surface_averaged` is landing in QUALIFIERS with an empty operators list. Contrast the untailed, unprojected control `flux_surface_averaged_magnetic_field_magnitude`, which parses correctly with operators carrying an OperatorApplication for flux_surface_averaged with bare_prefix true, alongside the magnitude postfix. Same token, two classifications, and the difference is the presence of a projection or a locus tail.\n\nTHE CONSEQUENCE, and it is why this blocks a release rather than being cosmetic. A trailing qualifier renders after the projection and base and before the tail, which is why the composer emits toroidal_lithium_velocity_flux_surface_averaged_at_plasma_boundary. And because the token sits in qualifiers rather than operators, the representation cannot distinguish a flux-surface-averaged quantity from an unaveraged one carrying a stray qualifier - representation equality is this project's test for meaning equality, and a downstream consumer uses exactly that test to decide whether a rename preserves meaning.\n\nTHE REQUIRED BEHAVIOUR, from the lead verbatim: the flux-surface-average operator is ALWAYS FIRST - flux surface average OF the quantity. It wraps everything including the projection. So the canonical spelling for that representation is:\n flux_surface_averaged_toroidal_lithium_velocity_at_plasma_boundary\nThe authored spelling was also wrong, for the opposite reason: it placed the projection OUTSIDE the operator. Neither the authored form nor the current canonical form is correct, so the target is a third spelling.\n\nWHERE TO LOOK FIRST, before changing anything. The operator is registered as kind unary_prefix, precedence 30, with flux_surface_reduction true, and it renders joiner-free through a bare_prefix path at grammar/render.py:196. A helper at grammar/render.py:302 named _leading_bare_operators exists to split leading bare operators from ordinary base qualifiers and is called at :341 - which is machinery built for exactly this, and it is not producing the leading form here. Establish whether the defect is that the PARSER never classifies the token as an operator when a projection or locus is present, or that it does classify it and the renderer's split fails to lead with it. Report which, with the file and line that decides it. The fix belongs wherever that answer points, and I do not want a compensating change at the other end.\n\nPRESERVE THE REDUCTION CLASS CONSISTENTLY. volume_averaged, line_integrated and root_mean_square share kind unary_prefix at precedence 30; flux_surface_averaged additionally carries flux_surface_reduction. Say whether your fix generalises to all operators at that precedence or is specific to the flux-surface reduction, and prefer the general form if the evidence supports it. Note also an existing inconsistency worth reporting but NOT fixing here: gyroaveraged is registered unary_postfix at precedence 10 while its semantic siblings are prefix at 30.\n\nWHAT MUST NOT MOVE. A downstream consumer measured 2,339 of 2,347 accepted names rendering byte-identical under the current grammar. Your change must not disturb that. The untailed control flux_surface_averaged_magnetic_field_magnitude must stay byte-identical, and so must the recursive control flux_surface_averaged_ratio_of_square_of_toroidal_flux_coordinate_gradient_magnitude_to_square_of_major_radius.\n\nEVIDENCE REQUIRED. Add tests pinning: the representation for the tailed projected name now carrying flux_surface_averaged in operators rather than qualifiers; the canonical spelling being flux_surface_averaged_toroidal_lithium_velocity_at_plasma_boundary; a strict parse of that spelling succeeding and round-tripping; a strict parse of both wrong spellings being rejected as non-canonical while still parsing to the same representation; and both controls byte-identical. Run once before your change and once after:\n uv run pytest tests/ -p no:cacheprovider\nReport both full-suite counts. The suite is the blast radius and it must not lose a passing test; if it does, name each loss and stop rather than adjusting expectations.\n\nDo not touch the vocabulary's precedence numbers unless your diagnosis shows precedence is the cause, and say so explicitly if it is. This repository's environment is its own, so plain uv run is correct.\n\nOPERATIONAL RECEIPT IN YOUR MANIFEST: wall clock from first tool call to manifest write, tool calls, turns used, any request retried or refused and why, and one sentence naming what you had to discover versus what this brief handed you.",
184+
"estimated_hours": null,
185+
"goal": "A flux-surface reduction operator leads its name and wraps the projection rather than trailing as a qualifier",
186+
"id": "n-grdp-the-reduction-operator-leads-the-name",
187+
"manifest_path": "/home/ITER/mcintos/.config/reckon/crew/runs/r-20260906T114004346214-n-grdp-the-reduction-operator-leads-the-name/manifest.md",
188+
"plan": "grammar-release-and-downstream-pin",
189+
"requires_decisions": [],
190+
"role": "implement",
191+
"section": "\u00a71",
192+
"spec_level": "guided",
193+
"time_budget": "55m",
194+
"write_paths": [
195+
"imas_standard_names/grammar/parser.py",
196+
"imas_standard_names/grammar/render.py",
197+
"tests/grammar/test_reduction_operator_scope.py"
198+
]
199+
},
200+
"role": "implement",
201+
"spec_level": "guided",
202+
"member": "cx-kindpin",
203+
"backend": "claude-opus",
204+
"local": false,
205+
"agent": {
206+
"alias": "opus 5",
207+
"backend": "claude-opus",
208+
"effort": "high",
209+
"launch": "cli",
210+
"model": "claude-opus-5",
211+
"sandbox": "worktree-full"
212+
},
213+
"dispatched_at": "2026-09-06T11:40:13Z",
214+
"completed_at": "2026-09-06T13:31:28.078Z",
215+
"completed_at_source": "terminal_event",
216+
"worker_seconds": 6667,
217+
"worker_seconds_source": "stream_events",
218+
"wall_seconds": 6675,
219+
"stalled": true,
220+
"time_budget": "55m",
221+
"base_sha": "a2e4ae80605dfec68d37bfc2f52abe199978cb26",
222+
"commits": [
223+
"0357c9321ae4c975ca65efd69b88258f1ee1d992"
224+
],
225+
"changed_lines": {
226+
"added": 60,
227+
"removed": 50,
228+
"files": 3
229+
},
230+
"tests_added": 22,
231+
"gate": "passed",
232+
"gate_check": {
233+
"command": "uv run pytest tests/ -p no:cacheprovider",
234+
"exit_status": 0,
235+
"log_path": "/tmp/isn-grdp/after.log",
236+
"log_digest": null
237+
},
238+
"suite_delta": null,
239+
"failure_classification": null,
240+
"outcome": "",
241+
"manifest_path": "/home/ITER/mcintos/.config/reckon/crew/runs/r-20260906T114004346214-n-grdp-the-reduction-operator-leads-the-name/manifest.md",
242+
"scope_changed": true,
243+
"session_id": "451f0139-ecb1-41fc-bfc4-ff33f8bff921",
244+
"budget": {
245+
"headroom": "known",
246+
"utilisation_pct": 67.0,
247+
"rate_limit_type": "five_hour",
248+
"rate_limit_period_minutes": null,
249+
"resets_at": "2026-09-06T16:00:00Z",
250+
"threshold_status": "allowed_warning",
251+
"surpassed_threshold": 1,
252+
"tokens": {
253+
"cache_creation_input_tokens": 32959,
254+
"cache_creation_input_tokens_cumulative": 32959,
255+
"cache_read_input_tokens": 5975113,
256+
"cache_read_input_tokens_cumulative": 5975113,
257+
"inference_geo": "not_available",
258+
"input_tokens": 54,
259+
"input_tokens_cumulative": 54,
260+
"output_tokens": 22456,
261+
"output_tokens_cumulative": 22456,
262+
"service_tier": "standard",
263+
"speed": "standard"
264+
},
265+
"cost_usd": 3.8788165,
266+
"refusal": false,
267+
"detail": "backend reports utilisation and reset time",
268+
"cost_usd_cumulative": 3.8788165
269+
},
270+
"lineage": null,
271+
"shadow_controlled": null,
272+
"shadow_patch": "",
273+
"unreconciled_override": {
274+
"grace": "5m",
275+
"requested": true,
276+
"waived_runs": []
277+
},
278+
"throughput": {
279+
"detail": "generation and wall clock reported separately by the backend",
280+
"elapsed_seconds": 728.43,
281+
"generated_tokens": 22456,
282+
"generation_seconds": 304.396,
283+
"input_budget_tokens": 1000000,
284+
"input_utilisation_pct": 23.6,
285+
"machine_seconds": 424.034,
286+
"peak_input_tokens": 235916,
287+
"tokens_per_second": 73.77,
288+
"wall_tokens_per_second": 30.83
289+
},
290+
"attempt": 3,
291+
"attempt_kind": "resume",
292+
"scope_acceptances": [
293+
{
294+
"path": "tests/grammar/test_tail_gated_operator_order.py",
295+
"reason": "contract pinning the trailing spelling the lead reversed; repointing authorised by ruling"
296+
}
297+
],
298+
"worktree_retention": {
299+
"classification": "retained-for-resume",
300+
"worktree": "/home/ITER/mcintos/Code/.reckon-worktrees/imas-standard-names-5be88ee0fdc3/ship-s10-20260905/n-grdp-the-reduction-operator-leads-the-name",
301+
"session_id": "451f0139-ecb1-41fc-bfc4-ff33f8bff921",
302+
"session_source": "pointer",
303+
"retained_at": "2026-09-06T13:31:28.078Z"
304+
},
305+
"execution_fit": {
306+
"allowed": true,
307+
"execution_capable": true,
308+
"matched_measure": "uv run pytest tests/",
309+
"override": false,
310+
"role": "implement",
311+
"status": "compatible"
312+
}
157313
}
158314
],
159315
"holds": [],
160-
"_version": 3
316+
"_version": 5
161317
}
162318
}

imas_standard_names/grammar/ir.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,15 @@ class LocusType(StrEnum):
146146

147147
# Unary-prefix operators that spell WITHOUT the ``_of_`` joiner
148148
# (``flux_surface_averaged_electron_density``, not
149-
# ``flux_surface_averaged_of_electron_density``). These normally reach the IR as
150-
# qualifiers on the base, because a bare prefix is indistinguishable from a
151-
# qualifier when the operand is an ordinary base. They reach the operator stack
152-
# only when the operand has no base to hang off — an operator application, e.g.
153-
# a binary form — and there they must carry ``bare_prefix`` so the renderer
154-
# reproduces the joiner-free spelling.
149+
# ``flux_surface_averaged_of_electron_density``). The domain reductions among
150+
# them — the precedence-30 members, see the placement policy in
151+
# ``vocabularies/operators.yml`` — lead their name and their operand runs to
152+
# the end of the string, projection and trailing locus included, so they reach
153+
# the IR on the operator stack carrying ``bare_prefix``. Never as a qualifier:
154+
# a qualifier renders after the base and inside the tail, which states a
155+
# narrower scope than the reduction has, and one token with two readings makes
156+
# two names for one quantity. The lower-precedence members modify the base
157+
# rather than reducing it, so they do reach the IR as qualifiers on the base.
155158
BARE_PREFIX_OPERATORS: frozenset[str] = frozenset(
156159
{
157160
"accumulated",

0 commit comments

Comments
 (0)