Repository navigation
check-cross-package-test-inputs' literal collector cannot see split-segment or ascent-relative paths, so four declared radii are held by prose alone #9763
Description
Activity
- added a commit that references this issue
on Aug 18, 2026 os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage: grading = de-label —
findingoff; confirmedpm:queue·domain:devx· Bug (the file's own prose promises radius coverage it does not deliver — declared ≠ enforced inside the gate's guarantee). The widening is one-directional as the card prices (over-collection can only force a wider declaration), with a--self-testcase per added spelling (#8995).
Generated by Claude Code
- added a commit that references this issue
on Aug 19, 2026 Claiming this issue.
- Session ID:
session_01XqDQYVU5smx29ts9pAErja - Branch:
claude/issue-9763-literal-collector-spellings - Container & model: M, mode:subagent, model: opus
Worktree branched from current
main(06f9848f9, which containsde7799edb/ PR #9765). Starting with H1's three-step counterfactual reproduction against that baseline before writing any fix.
Generated by Claude Code
- Session ID:
- added a commit that references this issue
on Aug 19, 2026 { "issue": 9763, "status": "done", "branch": "claude/issue-9763-literal-collector-spellings", "pr": "https://github.com/objectstack-ai/objectstack/pull/9826", "premise_still_valid": true, "summary": "Taught the radius roster to RECONSTRUCT paths instead of only reading quoted whole-path literals. No AST-ification was needed and H3's price does not apply: 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. Split-segment join/resolve and ascent-relative literals are one mechanism; skills/ was correctly diagnosed in the card as a one-line data fix to the flat alternation. 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 -- packages/lint/src/** covers that listing while not matching the bare string. The pass also found two radii that were NEVER declared: spec reads content/docs/api/error-catalog.mdx and docs/audits/2026-07-unknown-key-strictness-ledger.md ascent-relative; their globs plus matching turbo.json inputs are added. Per ruling 1 no declaration was narrowed or reshaped to fit the collector.", "tests": "All on merged head c74b2b9aa (main merged, not rebased, at 9ff11921a; union re-run AFTER the merge). H1 LADDER, ablating create-objectstack's glob and unquoting the two header-comment mentions -- BEFORE (06f9848f9): step1 RED naming template-consistency.test.ts (a mention); step2 RED naming template-version-stamps.test.ts (the other mention); step3 'OK: 12 package(s) read outside themselves, all declared' exit 0. AFTER: step3 RED 'scripts/sync-template-versions.mjs (named in packages/create-objectstack/src/template-version-stamps.test.ts)' -- the file that actually READS -- exit 1; and with the glob KEPT + both mentions unquoted the gate is GREEN exit 0, so the glob is held by the read. Reverse-verified from the committed state (git restore --source=origin/main, ladder re-run, false green returns; git checkout HEAD restores, tree byte-clean). NO-FALSE-LEADS census: 46 -> 54 (package,path) pairs, +8, 0 lost, 6 packages gaining (vs #9639's +139,084 and #9700's +4,751/0/1); all 8 hand-checked as real reads, 6 already inside declared globs. Measured and NOT shipped: adding 'docs' beside 'skills' in the alternation changed the count by exactly 0 (54 both ways). GATES: check:cross-package-test-inputs PASS ('OK: 12 package(s) ... all declared'), --self-test 33 -> 52 cases all pass, check:examples-live-imports PASS (0 invisible), check:ratchet-remedy-authority PASS (97 scripts swept), check:nul-bytes PASS (6271 files), dispatch-gates --self-test PASS (285 cases), eslint PASS, --union-into smoke test correctly pulls spec and formula in on the newly declared paths. dispatch-gates derived only 2 families both pre- and post-merge; the other four were added by hand per H4. Prettier is NOT a gate here -- origin/main's copies of both changed files fail prettier --check too and lint is eslint-only -- so no reformat was taken.", "open_questions": [], "out_of_scope_findings": [] }H2 — the full sweep, all 12 entries (53 declared globs)
The card's "four of twelve" was found while investigating one entry. Splitting the roster by which half produced each match:
roster half before after held by a reconstructed read 0 (mechanism did not exist) 19 held by a flat quoted literal only 35 23 held by nothing machine-visible 16 11 Seven globs moved into read-held — all four the card named, plus three more:
package glob before after create-objectstackscripts/sync-template-versions.mjsprose mention READ @objectstack/specpackages/lint/src/**prose mention (in an unrelated test) READ @objectstack/metadata-protocolscripts/check-durability-degradation-log-level.mjsnothing READ @objectstack/formulaskills/objectstack-formula/**nothing READ @objectstack/clipackages/connectors/.../connector-mcp-plugin.tsnothing READ @objectstack/plugin-securitypackages/spec/src/security/**nothing READ @objectstack/downstream-contractpackages/spec/package.jsonnothing READ A correction to the card's framing, and it matters for sizing. "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 iterate a literal table offile: 'packages/client/src/realtime-api.ts'entries and read each one, which IS the designed spelling. 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 with an unmeasured prose/code split, not "23 at risk" — and the answer to "is it 4 of 12 or 9 of 12" is that the unit was wrong: 7 of 53 globs were unheld-or-prose-held and are now read-held, and 11 remain held by nothing machine-visible (mostly live ESM imports thatcheck-examples-live-importsholds instead, plusgit ls-fileswalks and module resolution — reads nofs-call scan can see).H3 — verdict: neither regex-only nor AST
The checker was already not a flat regex — it carried a recursive expression resolver folding constants across
join/resolvearguments to compute depth, and discarded the names. Reconstruction is that same walk in a second coordinate, so the change is modest and the gate stays dependency-free (the property that keeps it un-mutable in CI). What it still misses, stated rather than left to be discovered: template-literal paths, segments from unfoldable variables, and loop-variable directory reads. Each yields no name, never a wrong one — an unreadable argument costs the name and keeps the depth, so the escape verdict is untouched and the roster never gains an entry pointing at a file nobody reads. Reads that reach another package through Node's resolver rather thanfsare outside this gate entirely, which is whydownstream-contract'spackages/spec/src/**stays unheld.
Generated by Claude Code
Generated by Claude Code
{ "issue": 9763, "status": "done", "branch": "claude/issue-9763-literal-collector-spellings", "pr": "https://github.com/objectstack-ai/objectstack/pull/9826", "premise_still_valid": true, "summary": "Conflict resolution only, no re-scoping. Merged origin/main 56656aa1aa into the accepted branch and pushed 1e60ca74ec to the existing branch; mergeable_state went dirty -> blocked. git reported ONE textual conflict but auto-merged TWO more regions with no marker (main's serve.ts glob + 11-line rationale in the @objectstack/spec entry, and all of turbo.json, which never conflicted so has no stages) - both verified rather than trusted. Both sides survive: the branch's rewritten 'what forces the glob' rationale plus all six of main's globs and both of its new paragraphs; turbo.json lost 0 inputs and 0 task keys against either parent. I edited four words INSIDE the conflict block: main's new prose cited sync-template-versions.mjs as an example of the 'named rather than read' shape, which this PR makes false and which the paragraph directly above now contradicts, so I dropped that half of the cross-reference and kept check-nul-bytes.mjs (still true). The identical stale clause in the cleanly auto-merged region I did NOT touch - filed as #9977 per ruling 4, and it is the second occurrence of a clause #9651 already retired once. Auto-merge left disabled and draft state untouched as instructed.", "tests": "All on pushed head 1e60ca74ec (union re-run AFTER the final amend moved the head from c0eee64340). Exit codes captured before any pipe. VERDICT LINES: 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 '97 scripts swept ... Control corpus: 18 hand-classified scripts, set-equality audited both ways.' | nul-bytes exit=0 'scanned 6322 text file(s) ... no raw ASCII control bytes' | dispatch-gates-selftest exit=0 '310 cases pass'. dispatch-gates re-derived on the new head still names 2 families, both the same gate. node --check PARSE_EXIT=0; zero duplicate top-level bindings and zero duplicate table keys (the #9712 failure mode). H1 ABLATION A (branch's guard): dropped the sync-template-versions.mjs glob + unquoted both flat-visible mentions; landing confirmed independently of the editor - asserted match counts x1/x1/x1, then git diff --stat '3 files changed, 2 insertions(+), 3 deletions(-)', removed text grepped to 0/0/0 and injected unquoted text grepped present at :79 and :3. Merged tree ABLATION_A_EXIT=1 'create-objectstack names path(s) no declared glob covers ... scripts/sync-template-versions.mjs (named in packages/create-objectstack/src/template-version-stamps.test.ts)' - the file that really READS. Control at merge-base 9ff11921a under the SAME ablation: PREFIX_EXIT=0 'OK: 12 package(s) ...' - the exit-0 false green, so the ablation discriminates. H1 ABLATION B (main's guard): removed all five globs main's two commits added; landing confirmed by asserted counts x2/x1/x1/x1, then '1 file changed, 5 deletions(-)', then each glob grepped to 0. ABLATION_B_EXIT=1 naming all five across both entries (@objectstack/spec -> serve.ts via publish-smoke-port-collision.test.ts; create-objectstack -> gen-sdui-manifest.sh, publish-smoke.sh, serve.ts, .github/workflows/scaffold-e2e.yml via scaffold-e2e-boot-probe.test.ts). Both ablations restored byte-clean from the committed merge (git diff HEAD -> 0 lines, gate green again). No ablation involved a build - this gate is dependency-free and runs from source, so there is no dist staleness to rebuild. NARROWING DECLARED: no eslint locally, this worktree has no node_modules and none exists in the container; the change is comment text plus a string array in a file whose parse and execution I verified directly.", "h2_assertion_counts": "By the harness's own cases.length, not grep (fixture strings contain the call text): merge-base 9ff11921a = 33, origin/main 56656aa1aa = 33, branch head c74b2b9aa = 52, merged = 52. 33 + 0 + 19 = 52 exactly; main's two commits added no self-test cases, so no case was dropped.", "h3_population": "packages 12 at every rev. Pairs: merge-base 46, origin/main 51 (+5), branch head 54 (+8), merged 60. ZERO pairs lost against either parent. NON-ADDITIVE by one: 46+5+8=59 but merged=60. The extra pair is 'create-objectstack | .github/workflows/scaffold-e2e.yml', rostered by the merge and by NEITHER parent - main's scaffold-e2e-boot-probe.test.ts 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) and which does not exist on the branch. Ablated to prove it: dropping that glob on origin/main ALONE is MAIN_ALONE_EXIT=0, a silent exit 0 - main's own newest declaration is held by nothing machine-visible there, and this merge is what makes it load-bearing.", "h4_merge_tree": "Clean. Re-checked after pushing; origin/main DID move while I worked (56656aa1aa -> 8fa4e2a76d, 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.", "open_questions": [ { "question": "Should the four-word cross-reference edit inside the conflict block stand, or should the merge be literal?", "options": [ "A - keep it: main's paragraph cited sync-template-versions.mjs as 'NAMED rather than read', which the paragraph directly above it now flatly contradicts; dropping that half leaves check-nul-bytes.mjs, still true per the file's own @objectstack/cli rationale", "B - revert to main's literal text and let #9977 cover both occurrences together" ], "recommendation": "A, because the conflict block is text I had to author, and authoring a comment that contradicts itself two paragraphs apart is a defect I would be introducing rather than inheriting. The gate is green either way, and B is a one-line revert if you disagree." } ], "out_of_scope_findings": [ "filed as #9977: the @objectstack/spec entry's serve.ts rationale - the region git auto-merged with NO conflict marker - still cites sync-template-versions.mjs as 'named in a comment rather than read', the second occurrence of a clause #9651 already retired eight days ago; left untouched per ruling 4, one-line patch included in the card" ] }
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 21, 2026 - added 3 commits that reference this issue
on Aug 23, 2026 - added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Sep 28, 2026
Found while implementing #9651 (a prose fix to the
create-objectstackrationale inscripts/check-cross-package-test-inputs.mjs). Not fixed there — out of scope, recording only.Instance of the #9747 meta-shape, in the fails toward FALSE GREEN half: the recognizer is
narrower than the path spellings in the repo, and the shortfall reads as "the radius is
covered" rather than as "unrecognised".
The guarantee that does not hold
check-cross-package-test-inputslets a declaration be NARROW rather than repo-wide, and saysin its own prose why that is safe:
repoRelativeLiterals(): "a glob is only allowed to be narrow while it still coversevery path the tests actually name, and the moment someone adds a probe outside the declared
radius the gate fails naming the file."
verify(): "This is what keeps narrowing honest: extending a probe roster past thedeclared radius fails here, by file name, instead of silently going ungated again."
The recognizer behind both claims is one regex:
It sees a path only when the WHOLE repo-relative path sits inside ONE quoted string and starts
at a top-level directory. Three spellings in the repo today do not, so the paths those tests
read are absent from the coverage check — silently, with exit 0.
The three spellings, each live in the tree
1. Split-segment
join/resolve— the path is real but arrives as separate arguments:packages/create-objectstack/src/template-version-stamps.test.ts:47path.join(repoRoot, 'scripts', 'sync-template-versions.mjs')packages/spec/src/identity/position-delegatable-enforcer.pin.test.ts:57join(REPO_ROOT, 'packages', 'lint', 'src')packages/qa/downstream-contract/test/source-resolution.pin.test.ts:87resolve(PACKAGE_DIR, '..', '..', 'spec', 'src')2. Ascent-relative literal — one string, but it starts at
..:packages/metadata-protocol/src/sys-metadata-repository.draft-drain.test.ts:451reads
new URL('../../../scripts/check-durability-degradation-log-level.mjs', import.meta.url).This one is already KNOWN — the
@objectstack/clientry documents it forconnector-mcp-plugin.ts— but it is documented as a fact about one entry, not as a generalhole in the guarantee above.
3. A top-level directory missing from the alternation —
skills/is not inpackages|apps|examples|content|scripts, sopackages/formula/src/skill-catalog-sync.test.ts:19reading
resolve(here, '../../../skills/objectstack-formula/SKILL.md')is invisible twice over.Measured, not inferred
For
create-objectstackthe declared globscripts/sync-template-versions.mjsis held byNOTHING mechanical. The two tests that genuinely read that script both spell it split-segment;
what the collector actually picks up is the quoted mention in each test's header comment.
Run against
6f40ed736:create-objectstack names path(s) no declared glob covers ... scripts/sync-template-versions.mjs (named in packages/create-objectstack/src/template-consistency.test.ts)OK: 12 package(s) read outside themselves, all declared, exit 0So an innocent prose edit to either test header can unforce a live declaration, after which a
narrowing passes in silence. A probe script over the whole table says four declared globs are
held by prose alone for this reason:
create-objectstack'sscripts/sync-template-versions.mjs,@objectstack/spec'spackages/lint/src/**,@objectstack/metadata-protocol'sscripts/check-durability-degradation-log-level.mjs, and@objectstack/formula'sskills/objectstack-formula/**.Severity: bounded, and stated
The ESCAPE detector is unaffected — it walks bindings, not literals, so every one of these
packages is still correctly flagged as escaping and still carries a declaration. What is
unheld is the radius, so the exposure is a narrowing that goes ungated, not an undeclared
package. That is smaller than #7802 but it is the same direction of failure, and it is the
half the file's own prose promises is covered.
Shape of a fix, and its price
Teach the collector the three spellings — join/resolve argument lists whose segments compose to
a repo-relative path, ascent-relative literals resolved against the file, and
skills/in thealternation. The design already absorbs the risk: the file states over-collection "can only
force a WIDER declaration, never a narrower one", so a false positive costs cache
invalidation, not correctness. Per #8995 each added spelling needs a
--self-testcase in thesame edit.
Worth weighing against #9747's finding that "widen each matcher" has been separately declined
elsewhere — the difference here is that the widening is cheap and one-directional, which is not
true of the durability-log matchers.
Generated by Claude Code
Generated by Claude Code