Skip to content

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

@claude

Found while implementing #9651 (a prose fix to the create-objectstack rationale in
scripts/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-inputs lets a declaration be NARROW rather than repo-wide, and says
in its own prose why that is safe:

  • on repoRelativeLiterals(): "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."
  • in verify(): "This is what keeps narrowing honest: extending a probe roster past the
    declared radius fails here, by file name, instead of silently going ungated again."

The recognizer behind both claims is one regex:

/(['"`])((?:packages|apps|examples|content|scripts)\/[A-Za-z0-9._/-]+)\1/g

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:

filespelling
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:451
reads new URL('../../../scripts/check-durability-degradation-log-level.mjs', import.meta.url).
This one is already KNOWN — the @objectstack/cli entry documents it for
connector-mcp-plugin.ts — but it is documented as a fact about one entry, not as a general
hole in the guarantee above.

3. A top-level directory missing from the alternationskills/ is not in
packages|apps|examples|content|scripts, so packages/formula/src/skill-catalog-sync.test.ts:19
reading resolve(here, '../../../skills/objectstack-formula/SKILL.md') is invisible twice over.

Measured, not inferred

For create-objectstack the declared glob scripts/sync-template-versions.mjs is held by
NOTHING 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:

  • drop the glob only ⇒ red: create-objectstack names path(s) no declared glob covers ... scripts/sync-template-versions.mjs (named in packages/create-objectstack/src/template-consistency.test.ts)
  • drop the glob AND unquote both header-comment mentions ⇒ OK: 12 package(s) read outside themselves, all declared , exit 0

So 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's scripts/sync-template-versions.mjs,
@objectstack/spec's packages/lint/src/**, @objectstack/metadata-protocol's
scripts/check-durability-degradation-log-level.mjs, and @objectstack/formula's
skills/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 the
alternation. 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-test case in the
same 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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions