test(pm): scope each window's summary assertions to its own clause - #13654
Merged
Conversation
`summaryLine` returns one sentence carrying every pass's disclosure, so a `summaryLine(...).includes(phrase)` case asked "did ANY window say this" while its name promised "did H8 say this". Exact while H8 was the only speaker (#13499); a check that cannot fail once #13606 put four more clauses beside it. Adds `summaryClause(summary, key)` + `SUMMARY_CLAUSE_ANCHORS`, and routes all 94 summary-disclosure cases through a `saidBy(key, line)` helper so a case can only be answered by the window it names. Two whole-line cases are kept deliberately (the sentence-terminal `endsWith` contract, and one `undefined` sweep) and marked where they sit. No change to the rendered summary line — test scaffolding only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
zhuangjianguo
marked this pull request as ready for review
August 31, 2026 05:15
zhuangjianguo
enabled auto-merge
August 31, 2026 05:15
zhuangjianguo
pushed a commit
that referenced
this pull request
Aug 31, 2026
`summaryLine`'s H19 clause carried its terminating `. ` inside the cross-repo branch alone, so the only rendering that ended the sentence was the one where an unrelated optional probe had fired. On an ordinary healthy pass -- no shortfall and no probe -- the clause stopped at `card(s)` and a lone space joined it straight onto H20's lead-in, which the live 2026-08-31 patrol rendered as `...card(s) Dispatch liveness (H20 + H27): ...`. Terminate the clause unconditionally and let the two optional branches contribute only their own text. The cross-repo branch already ended in a period of its own, so its leading space and the join's trailing space move into the terminator rather than adding a second one: across every rendered shape the whole line differs from base by exactly one inserted `.`, inside H19's terminator region and nowhere else. Pin it with the #13654 scoped-assertion scaffolding: `saidBy('h19Blockers', ...)` cases assert the clause ends `. ` on the healthy, shortfall, probe and both-branches shapes, plus that the probe shape does not render `resolves them.. `. The neighbouring whole-line `endsWith('not a gate verdict.')` cases could not see this -- they pin where the SENTENCE ends, not where this clause does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 1, 2026
…3677) `summaryLine`'s H19 clause carried its terminating `. ` inside the cross-repo branch alone, so the only rendering that ended the sentence was the one where an unrelated optional probe had fired. On an ordinary healthy pass -- no shortfall and no probe -- the clause stopped at `card(s)` and a lone space joined it straight onto H20's lead-in, which the live 2026-08-31 patrol rendered as `...card(s) Dispatch liveness (H20 + H27): ...`. Terminate the clause unconditionally and let the two optional branches contribute only their own text. The cross-repo branch already ended in a period of its own, so its leading space and the join's trailing space move into the terminator rather than adding a second one: across every rendered shape the whole line differs from base by exactly one inserted `.`, inside H19's terminator region and nowhere else. Pin it with the objectstack-ai#13654 scoped-assertion scaffolding: `saidBy('h19Blockers', ...)` cases assert the clause ends `. ` on the healthy, shortfall, probe and both-branches shapes, plus that the probe shape does not render `resolves them.. `. The neighbouring whole-line `endsWith('not a gate verdict.')` cases could not see this -- they pin where the SENTENCE ends, not where this clause does. Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13629
summaryLinereturns ONE sentence carrying every pass's disclosure, so asummaryLine(...).includes(phrase)case asks "did ANY window say this" while its name promises "did H8 say this". That was exact while H8 was the only speaker, and became a check that cannot fail once four more clauses joined the line.This is test scaffolding only. The rendered summary line does not change — proof below.
What landed
SUMMARY_CLAUSE_ANCHORS— the 17 windows that speak on the line, in render order, each with the lead-in phrase that opens its clause.summaryClause(summary, key)— cuts one window's clause out of a rendered line. A clause runs from its own anchor to whichever other anchor comes next, so an absent neighbour widens rather than truncates.saidBy(key, line)inselfTest— every summary-disclosure case now asserts against the extraction.Two failure modes, deliberately asymmetric and documented at the function: an unknown key throws (author error, and the loud abort names the typo), while a missing anchor returns
''(the ablation path, which must never throw — a throw inside at()argument aborts the whole suite, and the per-window ablation this card asks for would be unreadable).Every summary case, adjudicated
No silent skips. 96 cases call
summaryLineinselfTest; 94 were scoped, 2 were kept whole-line on purpose.已改 (scoped) — 94 cases
h19Blockersh22Readh20Dispatchh14Fallbackh35GatePatrolh9Restartheadh23Windowh8Windowh23Readh22Windowh17HoldsopenListingsh36SharedFilesh37Foldsh32Seats本就 scoped — 13 cases. The
timeBound(...)/exBound(...)cases calldescribeWindowBounddirectly. Their subject IS a single clause, with no summary line involved, so there is no neighbouring text that could answer them. Left as they are.明确不在范围 — 2 cases.
summary: the report-only contract still ends the sentenceand…after H20assert.endsWith('not a gate verdict.')on the whole line. That is a claim about the assembled sentence, not about a window; scoping it to thereportOnlyclause would only weaken it. Both are marked where they sit.One case's whole-line reach was replaced rather than dropped: the per-window
…never to undefinedcases now speak only for their own clause, so a newsummaryLine: no clause ANYWHERE prints the string undefinedcase keeps watch over the gaps between them.Acceptance: one ablation per window, 17 of 17
Predictions were fixed in the harness at
2026-08-31T04:55:09Zon commit8813cf1c7, before any leg ran (PREDICTIONS.json). Each leg deletes that window's clause from the summary construction (its source chunks are replaced with''), then:summaryLine's return body —1 -> 0on every leg — plus an independent render-level check that the window's anchor is gone from the output;git checkout HEAD -- <abs path>(never a bare checkout, which restores from the index and would leave the mutation in place), thengit diff HEADempty andgit hash-objectequal to the HEAD blob. Final blob24e366cd5c506cb5e2439125179e21bc9089d84con every leg.reportOnlyis the one window with no scoped cases of its own — by design, since its two cases are the whole-lineendsWithpair above. Ablating it still reddens its presence control and bothendsWithcases.A window's negative cases stay green under its own ablation: an empty clause satisfies "does NOT say X" vacuously. That is inherent to a negative assertion, not a gap in the scoping, and it is exactly why each window also carries a presence control that goes red when it stops speaking.
What this actually fixed — measured against the base
The same 17-leg ablation was run against the base revision (
7483afdd9, unmutated:✓ check-half-states self-test: 1749 cases pass.). Four pre-existing cases had their own window's clause deleted outright and stayed GREEN, satisfied by a neighbour:summaryLine: absent H16 counts degrade to 0, never to undefinedread on 0 of 0head,h17Holds,h14Fallback,h9Restart,h20Dispatch,h32Seats,h36SharedFiles,h37FoldsH22 summary: a completed window says the boundary was reachedboundary reachedh22Window,h23Windowsummary: …and says the unit is DISTINCT targets, not per-card edgesdistincth19Blockers,h20DispatchH37 summary: …stating the miss-only directionnever invent oneh36SharedFiles,h37FoldsAll four are red under their own ablation now.
The card's headline example (H8's
horizon reached) is not in that list, and the honest reading is that the naming convention #13628 introduced is currently holding: no neighbour says "horizon reached" today, so deleting H8's clause already reddened its cases at base. The convention works — it just is not what keeps working when the next author reaches for the obvious word. Both directions of that are now pinned structurally, against a line deliberately doctored to make a neighbour speak H8's words:#13629 loud direction: a neighbour saying "horizon reached" DOES reach the whole line→ true, and…but H8's truncated clause still does not claim it→ false.#13629 silent direction: the whole line still says it, from the neighbour→ true (H8's own clause deleted), and…and the scoped case refuses to be satisfied by that→ false.The rendered line does not change
summaryLinewas dumped at7483afdd9and at this branch's HEAD over 7 count shapes × 3 finding counts:The extractor was also validated against a real production rendering, not only synthetic shapes — the live read-only patrol line below partitions exactly, 3293 chars of clauses over a 3293-char line, all 17 anchors unique and clause-opening.
Verification
--self-test: 1749 → 1783 cases pass. Scoping replaced; the extractor's own controls (partition on two shapes, anchor uniqueness, declared-vs-rendered order, 17 presence controls, the ablation contract, the doctored-neighbour pins) added.Gate families derived from the ACTUAL diff via
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— 17 by path + 2 by change KIND (this edits a gate script). All 19 run; each gate's own verdict line:check:pm-half-states✓ check-half-states self-test: 1783 cases pass.check:pm-dispatch-gates✓ dispatch-gates self-test: 1017 cases pass.bare-root-worklist --self-testOK self-test: 51 live row(s), 43 unreachable as spelled, 43 recorded verdict(s) — none stalecheck:entry-guard✓ 187 scripts/ file(s) — every entry guard goes through invoked-as.mjscheck:partof-closing-keyword✓ check-partof-closing-keyword self-test: 28 cases pass.check:watch-hint-literal✓ 33 declaration(s) across 4 rostered name(s)check:pnpm-filter-targets✓ 140/179 --filter occurrence(s) across 32 file(s) resolvecheck:cli-command-ids✓ 304 command-id literal(s) across 109 file(s) … all resolvecheck-ci-filter-parityOK: all 127 declared cross-package glob(s) (92 unique) are coveredcheck-closing-keyword-parityOK (3 parsers agree on all 9 keywords and both measured separators)check-shard-attestation✓ 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check-cross-package-test-inputsOK: 24 package(s) read outside themselves, all declaredcheck:agent-test-spelling,check:bash32-floor,check:parse-guardcheck-nul-bytesOK (scanned 7549 text file(s) … no raw ASCII control bytes)node scripts/check-test-completeness.mjsis NOT MEASURED locally, by the gate's own account: it grades a savedturbo run testlog, no local log exists to hand it, and it prints that this branch is unreachable in CI. Not read as a pass or a fail.ESLint, repo-wide rather than narrowed —
pnpm exec eslint . --no-inline-config --format json: 5555 files selected by eslint's own config, 0 with problems, exit 0, and the changed file is in that selected population.Live read-only fire at this commit (
node scripts/pm/check-half-states.mjs, exit 0, run withenv -u GITHUB_TOKEN -u GH_TOKENbecause the container's value is the literal placeholderproxy-injected):Out of scope, filed
#13650 —
summaryLine's H19 clause has no sentence-terminating period unless a cross-repo probe fired, so on an ordinary healthy pass it runs straight into H20's clause. Visible in the live line above. Filed unassigned, for triage; not touched here, since this card's default is that the rendered line does not change.Notes for review
scripts/pm/**only, publishes nothing from any package.skip-changesetapplied.Generated by Claude Code
Generated by Claude Code