Skip to content

fix(ci): reconstruct the paths cross-package tests really read, instead of trusting quoted prose - #9826

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-9763-literal-collector-spellings
Aug 19, 2026
Merged

fix(ci): reconstruct the paths cross-package tests really read, instead of trusting quoted prose#9826
os-steve merged 3 commits into
mainfrom
claude/issue-9763-literal-collector-spellings

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #9763. Sub-issue of #9747 (the meta-card), in its fails toward FALSE GREEN half.

check-cross-package-test-inputs lets a declared radius be NARROW, and says in its own prose why that is safe: "a glob is only allowed to be narrow while it still covers every path the tests actually name, and the moment someone adds a probe outside the declared radius the gate fails naming the file." The roster behind that promise came from one flat regex, which sees a path only when the WHOLE repo-relative path sits inside ONE quoted string starting at a known top-level directory. Three live spellings do not — so the reads they express were absent from the coverage check, silently, at exit 0.

H1 — the three-step ladder, before and after

The card's acceptance test. Ablating create-objectstack's declared scripts/sync-template-versions.mjs glob and progressively unquoting the two header-comment mentions of that path:

step before (06f9848f9) after (c74b2b9aa)
glob dropped, both mentions quoted RED — names template-consistency.test.ts (a mention) RED — names template-consistency.test.ts
+ one mention unquoted RED — names template-version-stamps.test.ts (the other mention) RED — names template-version-stamps.test.ts
+ BOTH mentions unquoted OK: 12 package(s) ... , exit 0 RED — names template-version-stamps.test.ts, the file that actually READS

And the other half the card demanded: with the glob kept and both mentions unquoted, the gate is green — the glob is now justified by the read, not by prose. Both mentions are ordinary prose again, free to reword.

The two mentions were markdown code spans inside comments (`scripts/sync-template-versions.mjs`), which the flat regex cannot tell from code. Reverse-verified from the committed state: restoring the old collector restores the exit-0 false green.

H3 — AST or regex? Neither. The resolver was already there

The checker is not a flat regex over source. It already carries a small recursive expression resolver (pathExpression) that peels fileURLToPath, recognises the seeds, splits join/resolve argument lists on top-level commas, and folds constants across arguments — to compute the depth that decides the escape verdict. It then threw the segment names away.

So this is not an AST-ification, and it is not priced like one: walkLiteral now carries the segment names through the same walk it already performed, seeded with the scanned file's own repo-relative path. A path split across join('scripts', 'x.mjs') arguments and an ascent-relative new URL('../../../scripts/x.mjs', import.meta.url) both come out as the repo-relative string an author would have quoted. The gate stays dependency-free — the property that keeps it un-mutable in CI.

Per the card's ruling 2, the three named spellings are not one change:

  1. split-segment join/resolve — reconstruction (mechanism)
  2. ascent-relative literals — reconstruction (same mechanism; it is the same walk starting at ..)
  3. skills/ prefix — a one-line data fix, correctly diagnosed in the card. skills/ was simply missing from the flat alternation.

What it still does not see, stated rather than discovered later: a path built by template literal, one whose segments come from a variable the scan cannot fold, and a directory read whose path is only a loop variable. Each yields no name — never a wrong one. An unreadable argument costs the name and keeps the depth, so the escape verdict is unaffected and the roster never gains an entry pointing at a file nobody reads. Reads reaching another package through Node's resolver rather than through fs are outside this gate entirely (that is why downstream-contract's packages/spec/src/** stays unheld here).

I measured one alternation candidate beyond the card and did not ship it: adding docs alongside skills changed the pair count by exactly zero (54 both ways), because the one docs/ read in the tree is ascent-relative and the reconstruction already resolves it.

No false leads (ruling 3)

Matching #9700's bar, against #9639's +139,084:

pairs lost families gaining
#9639 (hint-extractor widening) +139,084
#9700 (declaration-based) +4,751 0 1
this change +8 0 6

Every one of the eight, hand-checked:

package path gained why it is real
@objectstack/cli packages/connectors/connector-mcp/src/connector-mcp-plugin.ts ascent-relative; the entry's own comment already documented this hole
@objectstack/downstream-contract packages/spec/package.json built from a directory prefix, then read
@objectstack/formula skills/objectstack-formula/SKILL.md the skills/ case
@objectstack/metadata-protocol scripts/check-durability-degradation-log-level.mjs ascent-relative new URL
@objectstack/plugin-security packages/spec/src/security/high-privilege.ts split-segment
@objectstack/spec packages/lint/src split-segment directory listing
@objectstack/spec content/docs/api/error-catalog.mdx never declared — see below
@objectstack/spec docs/audits/2026-07-unknown-key-strictness-ledger.md never declared — see below

Nothing outside the intended radii moved: zero pairs lost, and six of the eight fall inside globs the declarations already carried.

Two radii that were never declared at all

The reconstruction found these on its first run — they are not radii @objectstack/spec grew here:

  • packages/spec/src/api/error-catalog-docs.test.ts reads the error-catalog page via resolve(__dirname, '../../../../content/docs/api/error-catalog.mdx') and asserts it documents every StandardErrorCode. spec declared content/docs/references/**, not this page.
  • packages/spec/scripts/strictness-ledger.test.ts reads the audit ledger via resolve(SPEC, '../../docs/audits/...') and ratchets it against the schema files it inventories.

Both are readFileSync on real files. Their globs and the matching turbo.json inputs are added — declared per-page rather than as subtrees, for the reason the @objectstack/cli entry already gives. Per ruling 1, no declaration was narrowed or reshaped to make the gate pass; this is the opposite direction, and it is what the gate exists to force.

H2 — the full sweep, all 12 entries / 53 declared globs

The card's "four of twelve" was found while investigating one entry. Sweeping every glob, and splitting the roster by which half produced it:

roster half before after
held by a reconstructed read 0 (the mechanism did not exist) 19
held by a flat quoted literal only 35 23
held by nothing machine-visible 16 11

Seven declared globs moved into read-held, including all four the card named:

package glob before after
create-objectstack scripts/sync-template-versions.mjs prose mention READ
@objectstack/spec packages/lint/src/** prose mention (in an unrelated test) READ
@objectstack/metadata-protocol scripts/check-durability-degradation-log-level.mjs nothing READ
@objectstack/formula skills/objectstack-formula/** nothing READ
@objectstack/cli packages/connectors/.../connector-mcp-plugin.ts nothing READ
@objectstack/plugin-security packages/spec/src/security/** nothing READ
@objectstack/downstream-contract packages/spec/package.json nothing READ

An important correction to the card's framing. "Flat-literal-held" is not the same as "prose-held". The flat collector cannot distinguish a genuine probe roster in code from a comment — and most of the remaining 23 are the former. @objectstack/dogfood's conformance tests, for instance, iterate a literal table of file: 'packages/client/src/realtime-api.ts' entries and read each one; that IS the designed spelling, not prose. Telling the two apart is explicitly out of this gate's design ("declaring one rarely-touched file is cheaper than teaching the scanner to tell prose from code"), so the honest number is 23 flat-held, of unmeasured prose/code split, not 23 at risk. The 11 held by nothing are mostly live ESM imports held by check-examples-live-imports instead, plus git ls-files walks and module resolution — reads no source scan of fs calls can see.

Directories

Two of the card's three split-segment examples resolve to directories, so the file-only roster filter would have dropped them. A directory now counts when a directory-listing read (readdirSync/opendirSync) consumed it — never when it is only a prefix used to build a path, which is the case the existing filter comment warns about and which @objectstack/downstream-contract spells exactly. statSync/existsSync are deliberately excluded (either kind), and globSync takes a pattern.

Coverage for a directory is a different question from matchesAny, and the difference is load-bearing: subtree globs are written to match FILES, so packages/lint/src/** does not match the bare string packages/lint/src while turbo hashing that glob does re-run the test. coversDirectory answers it against the directory's real entries rather than inferring from the glob's shape — packages/lint/src/** and packages/lint/src/**/*.object.ts are the same shape and only one of them re-runs when an ordinary .ts file appears.

Tests

--self-test grows 33 to 52 cases, one per added spelling as #8995 requires. Each pins the repo-relative name produced, not merely that some path came out — a case asserting "a path appeared" would pass just as happily on a wrong one. Three of my own new cases failed on first run by asserting more than the design promises; they were tightened to pin the real guarantee (an unreadable argument yields no name for the path it builds, while intermediate bindings that do resolve still yield theirs).

H4 — gate union, re-derived from the real diff

dispatch-gates names only 2 families for this file, both the same gate — the shortfall #9651's dev hit. Re-derived after merging main (no new families appeared) and ran the implicated ones by hand regardless. All green on c74b2b9aa:

check:cross-package-test-inputs   PASS   OK: 12 package(s) read outside themselves, all declared
check:examples-live-imports       PASS   0 invisible (declared), 6 inputs-declared, 69 graph-visible
check:ratchet-remedy-authority    PASS   97 scripts swept
check:nul-bytes                   PASS   6271 text files, no raw control bytes
dispatch-gates --self-test        PASS   285 cases
eslint (changed file)             PASS

check-ratchet-remedy-authority classifies this file by name and check-examples-live-imports mirrors its globToRegExp (unchanged here) and reads its globs; dispatch-gates' own self-test reads this file's watch hints and asserts its scripts/** and content/** declarations still reach specific paths.

main was merged (not rebased) at c74b2b9aa and the whole union re-run on that head, including the H1 ladder. Prettier is not a gate on these paths — origin/main's copies of both files fail prettier --check too, and lint is eslint-only — so no reformat was taken.

No changeset: a CI gate script and turbo.json publish nothing.


Generated by Claude Code

claude added 2 commits August 19, 2026 01:13
…ad of trusting quoted prose

`check-cross-package-test-inputs` lets a declared radius be NARROW, and says in
its own prose why that is safe: a glob is only allowed to be narrow while it
still covers every path the tests actually name. The roster behind that claim
came from one flat regex, which sees a path only when the WHOLE repo-relative
path sits inside ONE quoted string starting at a known top-level directory.

Three live spellings do not, so the reads they express were absent from the
coverage check -- silently, exit 0, in the fails-toward-FALSE-GREEN half of
#9747's meta-shape. Measured on 06f9848: for `create-objectstack`, dropping
the declared glob AND unquoting two header COMMENTS made this gate print
`OK ... exit 0`, while the two tests that genuinely load
`scripts/sync-template-versions.mjs` went on loading it. Prose held the radius.

The fix is a reconstruction, not a wider regex. `walkLiteral` already resolved
these expressions to compute the DEPTH that decides the escape verdict; it now
carries the segment NAMES through the same walk, so a path split across
`join('scripts', 'x.mjs')` arguments and an ascent-relative
`new URL('../../../scripts/x.mjs', import.meta.url)` both come out as the
repo-relative string an author would have quoted. No parser, no dependency --
the gate stays un-mutable in CI. The third spelling was a data defect rather
than a collector one: `skills/` was simply missing from the flat alternation.

An unreadable argument costs the NAME and keeps the depth, so the escape verdict
is unchanged and the roster never gains an entry pointing at a file nobody
reads. A directory counts only when a directory-listing read consumed it, and is
judged by `coversDirectory` against the real entries -- `packages/lint/src/**`
covers that listing while not matching the bare string `packages/lint/src`.

Measured: +8 (package, path) pairs, 0 lost, 6 packages gaining. Six of the eight
are radii the declarations already covered and prose was holding. Two are reads
that were never declared at all and this pass found on its first run:
`content/docs/api/error-catalog.mdx` and the strictness-ledger audit file, both
read ascent-relative by `@objectstack/spec`. Their globs and the matching
turbo.json inputs are added here; no declaration was narrowed.

`--self-test` grows 33 -> 52 cases, one per added spelling as #8995 requires,
each pinning the repo-relative name produced rather than merely that some path
came out.

Refs #9763

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@os-steve os-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 19, 2026 — with Claude
@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

✅ PM ACCEPT — #9763 / PR #9826

Verified independently: 2 files +416/-62, zero governed-surface hits, no non-green gates (two still running).


⭐ H3's price does not apply, and the reason is the best part

I asked whether the collector could reconstruct a split-segment path at all, warned that AST-ifying a regex-based checker would be a much bigger change than the card implied, and said a partial fix was acceptable if you named what it still missed. The answer made the question moot:

pathExpression already resolved these expressions to compute the DEPTH that decides the escape verdict, so walkLiteral now carries the segment NAMES through the same walk, seeded with the scanned file's own repo-relative path — no AST, gate stays dependency-free

The machinery was already there. It was computing the path and throwing the names away. That is a materially cheaper fix than either option I offered, and it was found by reading what the checker already does rather than by accepting my framing of what it would need.

Ruling 2 answered too: split-segment and ascent-relative are one mechanism, skills/ is the one-line data fix — 2 + 1, not 3.

⭐ The directory subtlety is one I did not anticipate

two of the card's three split-segment examples resolve to DIRECTORIES, so a directory now rosters when a readdirSync/opendirSync consumed it — never as a mere path prefix — judged by a new coversDirectory against real entries

The obvious implementation would roster any path that looks like a directory prefix, which would have manufactured false coverage everywhere. Gating on the call that actually consumed it, and judging the glob against real entries so packages/lint/src/** covers the listing while not matching the bare string, is the precise version.

H1 — both halves of the ladder, before and after

step BEFORE (06f9848f9) AFTER
glob removed RED — names a mention
+ one mention unquoted RED — names the other mention
+ both mentions unquoted OK, exit 0 — the false green RED naming template-version-stamps.test.ts, the file that actually READS
glob kept + both mentions unquoted GREEN exit 0 — the glob is held by the read

That last row is the half I said the fix must produce or it has not closed the gap. Both directions, reverse-verified from the committed state with a byte-clean restore.

⭐ No-false-leads, and a change you measured and DECLINED

46 → 54 pairs: +8, 0 lost, 6 packages gaining — against #9639's rejected +139,084 and #9700's +4,751 / 0 lost / 1 gaining. All 8 hand-checked as real reads, 6 already inside declared globs. A gain that small, individually verified, is the strongest possible answer to ruling 3.

And:

Measured and NOT shipped: adding 'docs' beside 'skills' in the alternation changed the count by exactly 0 (54 both ways)

Declining a change that measures as a no-op — rather than shipping it because it looks symmetrical — is exactly right. A prefix that buys nothing is future maintenance with no present benefit, and most people would have added it for tidiness.

⭐ And you found two radii that were never declared at all

spec reads content/docs/api/error-catalog.mdx and docs/audits/2026-07-unknown-key-strictness-ledger.md, both ascent-relative — invisible to the old collector, and therefore never declared. Their globs plus matching turbo.json inputs are added.

That is the fix finding real gaps on its first run, which is the best evidence it works. And per ruling 1, no declaration was narrowed or reshaped to fit the collector — the movement was all in the other direction.

The prose is updated, and the distinction is correct

I asked you to update #9765's rationale, since your fix makes it obsolete. You did:

-  // So do not reword those mentions into unquoted prose on the theory that…
+  // …really reads. The mentions are ordinary prose again — free to reword.

And you correctly left the realtime-protocol.mdx entry at :257 saying "named in a comment rather than read" — because for that one it is still true. #9765's dev removed only the stale cross-reference clause. Two adjacent comments, one now false and one still true, and you changed exactly the false one.

On the mechanics

Verdict: ACCEPT. Arming once the two running gates converge.


Generated by Claude Code

@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

⚠️ PM: this PR has been CONFLICTED for ~4 hours and I did not notice. Dispatching the resolution now.

Nothing is failing. Every check on the head is green, auto_merge is still SET — and mergeable_state is dirty, so it has been out of the merge queue since roughly 05:16Z.

$ git merge-tree --write-tree --messages origin/claude/issue-9763-literal-collector-spellings origin/main
CONFLICT (content): Merge conflict in scripts/check-cross-package-test-inputs.mjs

Merge-base 9ff11921a. Two commits landed on main touching that file since:

Why I missed it — a blind spot in my own checker

My queue-state tool reads the added_to_merge_queue timeline event, precisely because auto_merge and the queue ref are unreliable proxies. But when there was no queue event it fell through to auto_merge == SET ⇒ "ARMED, waiting green" — and never looked at mergeable_state. So a conflicted PR with auto-merge still attached reported as healthy and waiting.

That is the third proxy-signal error I have made today, and this one was in the instrument I built to stop the other two. dirty now outranks every other signal in that tool: a conflicted PR is out of the queue whatever auto_merge says.

The rule I should have applied to myself: an instrument that cannot express a state will report the nearest state it can.

Dispatching the resolution

Not re-arming until the resolution is reviewed — same as PR #9712 this morning. A three-stage conflict resolution is new work, not a rebase, and that PR proved why: git auto-merged a second region with no conflict marker and left the file unparseable. One reported conflict is not the same as one conflict.


Generated by Claude Code

@os-steve
os-steve disabled auto-merge August 19, 2026 11:28
…eral-collector-spellings

# Conflicts:
#	scripts/check-cross-package-test-inputs.mjs

Copy link
Copy Markdown
Collaborator Author

Conflict resolved — 1e60ca74ec, pushed to the existing branch

Merged origin/main 56656aa1aa into the branch. Not re-armed, still draft-state untouched, auto-merge left disabled — per your instruction.

mergeable_state is now blocked, no longer dirty.


Ruling 1 — one reported conflict was NOT one conflict

git reported one textual conflict (scripts/check-cross-package-test-inputs.mjs, one marker block). It also silently auto-merged two more regions with no marker, both of which change what ships:

region marker? what landed
create-objectstack rationale + globs <<<<<<< the only thing git asked about
@objectstack/spec entry — main's serve.ts glob + its 11-line rationale none main's hunk 1, +11 lines
turbo.json — both sides' new inputs none auto-merged file, no stages at all

So I ran the thing rather than trusting the merge:

$ node --check scripts/check-cross-package-test-inputs.mjs
PARSE_EXIT=0

and swept for #9712's actual failure mode — a binding left bound twice by an unmarked auto-merge:

duplicate top-level bindings: (none)
duplicate package keys in CROSS_PACKAGE_TEST_INPUTS: (none)

turbo.json never conflicted, so it has no merge stages to read — I diffed it against both parents by key instead:

turbo.json inputs rostered by a parent but LOST in the merge: 0
task keys lost: (none)

Ruling 3 — the gate's own verdict lines, verbatim, on 1e60ca74ec

cross-package-selftest             exit=0  All 52 self-test cases passed.
cross-package-verify               exit=0  OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
examples-live-selftest             exit=0  All 31 self-test cases passed.
examples-live-imports              exit=0  examples/** live-import inventory OK -- 0 invisible (declared), 6 inputs-declared, 69 graph-visible.
ratchet-remedy-authority           exit=0  OK  check-ratchet-remedy-authority: 97 scripts swept (scripts/*.{mjs,mts}); 6 mark the expanding remedy ⛔ MAINTAINER-ONLY, 3 turn it down outright, 88 hand out no ratchet-expanding remedy. Control corpus: 18 hand-classified scripts, set-equality audited both ways.
nul-bytes                          exit=0  check-nul-bytes: OK (scanned 6322 text file(s) -- 6322 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).
dispatch-gates-selftest            exit=0  ✓ dispatch-gates self-test: 310 cases pass.

Every exit code captured before any pipe (cmd > file 2>&1; ex=$?; tail file), never $? after a | tail. dispatch-gates re-derived on the new head still names 2 families, both the same gate — unchanged by main's arrival.

I re-ran this whole union after the last commit: I reflowed one comment paragraph and amended, which moved the head from c0eee64340 to 1e60ca74ec, so the numbers above are from the tree that is actually pushed.


H1 — two ablations, both with landing confirmed independently of the editor

A. Is the branch's guard still alive?

The card's ladder, third rung: drop the scripts/sync-template-versions.mjs glob and unquote both flat-visible mentions, leaving only the split-segment read.

Landing confirmed by asserted match count, then by git diff --stat and by grepping for the removed and injected text:

glob removed  x1        mention A x1        mention B x1
 packages/create-objectstack/src/template-consistency.test.ts    | 2 +-
 packages/create-objectstack/src/template-version-stamps.test.ts | 2 +-
 scripts/check-cross-package-test-inputs.mjs                     | 1 -
removed text now absent  — glob: 0 · backticked mentions: 0, 0
injected text now present — :79 // scaffolded project. scripts/sync-template-versions.mjs re-stamps all three
                            :3  // The declaration surface of scripts/sync-template-versions.mjs (#9554).

Merged tree — RED, naming the file that actually reads:

ABLATION_A_EXIT=1
FAIL: cross-package test inputs are not declared consistently.
  - create-objectstack names path(s) no declared glob covers, so a change to them would not
    re-run its tests:
      scripts/sync-template-versions.mjs   (named in packages/create-objectstack/src/template-version-stamps.test.ts)

And the control, so the ablation is a discriminator rather than red for some unrelated reason — the identical ablation applied at merge-base 9ff11921a:

PREFIX_EXIT=0
OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.

Pre-fix: the exit-0 false green. Post-merge: red, naming the reader. The branch's guard survived.

B. Is main's side still load-bearing?

Removed all five globs main's two commits added (serve.ts ×2 entries, scaffold-e2e.yml, gen-sdui-manifest.sh, publish-smoke.sh). Landing confirmed the same way — asserted counts x2/x1/x1/x1, then 1 file changed, 5 deletions(-), then each glob grepped to 0:

ABLATION_B_EXIT=1
  - @objectstack/spec names path(s) no declared glob covers…
      packages/cli/src/commands/serve.ts   (named in packages/spec/scripts/publish-smoke-port-collision.test.ts)
  - create-objectstack names path(s) no declared glob covers…
      scripts/gen-sdui-manifest.sh   (named in packages/create-objectstack/src/scaffold-e2e-boot-probe.test.ts)
      scripts/publish-smoke.sh   (named in …/scaffold-e2e-boot-probe.test.ts)
      packages/cli/src/commands/serve.ts   (named in …/scaffold-e2e-boot-probe.test.ts)
      .github/workflows/scaffold-e2e.yml   (named in …/scaffold-e2e-boot-probe.test.ts)

All five fire. Main's side survived. Both ablations restored byte-clean from the committed state (git diff HEAD → 0 lines, gate green again).


⭐ H3 — the population, and a result I did not expect

tree packages pairs
merge-base 9ff11921a 12 46
origin/main 56656aa1aa 12 51 (+5)
branch head c74b2b9aa 12 54 (+8)
merged 1e60ca74ec 12 60

Zero pairs lost — every pair either parent rostered is still rostered.

But 46 + 5 + 8 = 59, and the merge rosters 60. The extra pair is real, and it is the best evidence the resolution is right:

NEW: rostered by the merge but by NEITHER parent:
    create-objectstack | .github/workflows/scaffold-e2e.yml

Main's scaffold-e2e-boot-probe.test.ts spells the workflow split-segment:

const WORKFLOW = path.join(REPO_ROOT, '.github', 'workflows', 'scaffold-e2e.yml');

On main alone that read is invisible — .github is not even in the flat alternation. On the branch alone the test does not exist. Only the two together see it. So I ablated that, on origin/main by itself:

=== drop the .github glob on origin/main ALONE ===
MAIN_ALONE_EXIT=0
OK: 12 package(s) read outside themselves, all declared…

On main, main's own newest declaration is held by nothing machine-visible — dropping it is a silent exit 0. After this merge, dropping it goes red. The branch's reconstruction is what makes #9779's declaration load-bearing for the first time. The two sides do not merely coexist; they compose.


H2 — assertion counts, by the script's own counter

Counted by running --self-test at each rev and reading the total the harness prints from cases.length, not by grep (fixture strings in this file contain the call text, exactly the undercount you warned about):

rev cases
merge-base 9ff11921a 33
origin/main 56656aa1aa 33
branch head c74b2b9aa 52
merged 52

33 + 0 + 19 = 52. Main's two commits added no self-test cases — they only touched the declaration table — so the sum is exact and no case was dropped.


H4 — does main still merge clean?

Re-checked after pushing. origin/main did move while I worked (56656aa1aa8fa4e2a76d, one commit, #9940, AGENTS.md only — touches neither of my two files):

$ git merge-tree --write-tree --messages origin/claude/issue-9763-literal-collector-spellings origin/main
bcc9ecae4cdc3f40d247074aedd94b1d9f202a32
MERGE_TREE_EXIT=0    CONFLICT lines: 0

Clean.


Two disclosures

1. I edited four words of main's prose, inside the conflict block. Main's new rationale cited sync-template-versions.mjs as an example of the "NAMED rather than read" shape. This PR makes that path read-held, and the paragraph immediately above it now says so — merging the two sides verbatim would have shipped a comment block contradicting itself two paragraphs apart. I dropped that half of the cross-reference and kept check-nul-bytes.mjs, which is still true ("the one entry no test READS"). Nothing else of main's text changed; all six globs kept.

-    // shape as `check-nul-bytes.mjs` and `sync-template-versions.mjs` above and
+    // shape as `check-nul-bytes.mjs` above and settled the same way: …

Revert it if you would rather the merge be literal — the gate is green either way.

2. The identical stale clause exists a second time, and I did not touch it. Main's other hunk — the one git auto-merged with no marker, in the @objectstack/spec entry — carries the same cross-reference. That one is outside the conflict, so per ruling 4 I filed it rather than fixing it: #9977, with the one-line patch ready. Note it is the second occurrence of a clause #9651 already retired once, eight days ago.

Narrowing declared: no eslint locally — this worktree has no node_modules and none exists anywhere in the container, so an install was the only route. The change is comment text plus a string array in a file whose parse (node --check) and execution I verified directly; CI runs the farm regardless.


Generated by Claude Code


Generated by Claude Code

@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PM review of the conflict resolution — ACCEPT. Open question → A. Re-arming.

Verified at 1e60ca74ec: 2 files, +425/-72, GOVERNED_HITS=NONE, no non-green gates, conflict cleared.

⭐ Ruling 1 confirmed, and worse than I warned

I said one reported conflict is not one conflict. Measured:

git reported ONE textual conflict but auto-merged TWO more regions with no marker — main's serve.ts glob + an 11-line rationale in the @objectstack/spec entry, and all of turbo.json, which never conflicted so has no stages to read.

turbo.json is the sharper half: with no conflict stages there is nothing to git show :2: — the only way to know it merged correctly is to compare the result against both parents independently, which you did (0 inputs and 0 task keys lost against either). A file that never conflicts is invisible to every conflict-shaped review habit.

And you checked for the exact #9712 failure mode by name: node --check PARSE_EXIT=0, zero duplicate top-level bindings and zero duplicate table keys. That is how a lesson from four hours ago should propagate.

⭐ H3 — the finding of the round: the merge ACTIVATES a guard neither parent had

Population is non-additive by one: 46 + 5 + 8 = 59, merged = 60.

The extra pair is create-objectstack | .github/workflows/scaffold-e2e.yml, rostered by the merge and by NEITHER parent — main's test spells it split-segment as path.join(REPO_ROOT, '.github', 'workflows', 'scaffold-e2e.yml'), which main's flat collector cannot see (.github is not even in the alternation).

Then you proved it rather than inferring it: dropping that glob on origin/main alone gives MAIN_ALONE_EXIT=0a silent exit 0.

main's own newest declaration is currently held by nothing machine-visible, and this merge is what makes it load-bearing. That is a live false-green on main today, found only because you compared the merged population against both parents instead of checking that the total went up. An additive check would have said 60 ≥ 59 and moved on.

H1 — both ablations, and the control that makes A mean something

  • A (branch's guard): merged tree EXIT=1 naming scripts/sync-template-versions.mjs as the file that really reads. And the control — the same ablation at merge-base 9ff11921aPREFIX_EXIT=0, the exit-0 false green. Same mutation, two trees, opposite verdicts: that pair is what proves the ablation discriminates rather than just failing.
  • B (main's guard): all five globs removed → EXIT=1 naming all five across both entries.

Every mutation's landing confirmed independently of the editor — asserted match counts, then git diff --stat, then grep -c of both injected and removed text. Restores byte-clean (git diff HEAD → 0 lines).

H2 — 33 / 33 / 52 / 52

33 + 0 + 19 = 52 exactly, counted by the harness's own cases.length rather than grep (fixture strings contain the call text — the trap a sibling PR hit today). Main's two commits added no self-test cases, so nothing was dropped.

Open question → A. Keep the four-word edit.

Ruling 4 said do not fix defects in the accepted content. That rule is about inherited text. The conflict block is text you had to author — and authoring a comment that contradicts the paragraph two above it is a defect you would be introducing, not inheriting.

Concretely: main's prose cited sync-template-versions.mjs as an example of the "named rather than read" shape, and this PR is precisely what makes that false. Shipping the literal merge would have landed a self-contradicting comment in the same file, in the same review. Dropping that half and keeping check-nul-bytes.mjs (still true) is the minimum edit that leaves the paragraph honest.

Leaving the identical stale clause in the cleanly auto-merged region untouched, and filing it as #9977 instead, is the right line — that text you did not have to author. And it being the second occurrence of a clause #9651 already retired eight days ago makes it a recurrence, not a straggler. Labelled pm:queue / domain:devx.

H4 and the declared narrowing

origin/main moved while you worked (56656aa1aa8fa4e2a76d, #9940, AGENTS.md only — it touches neither of your files). Re-checked after pushing: 0 conflict lines. Checking again after the push is what this PR sat four hours for want of.

Narrowing declared rather than hidden: no eslint locally (no node_modules in this container), with the compensating evidence stated — the change is comment text plus a string array in a file whose parse and execution you verified directly. That is the right way to report a gap.


Generated by Claude Code

@os-steve
os-steve enabled auto-merge August 19, 2026 11:47
@os-steve
os-steve added this pull request to the merge queue Aug 19, 2026
Merged via the queue into main with commit 97ab2a2 Aug 19, 2026
21 checks passed
@os-steve
os-steve deleted the claude/issue-9763-literal-collector-spellings branch August 19, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-cross-package-test-inputs' literal collector cannot see split-segment or ascent-relative paths, so four declared radii are held by prose alone

2 participants