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:
| file | spelling |
|---|
packages/create-objectstack/src/template-version-stamps.test.ts:47 | path.join(repoRoot, 'scripts', 'sync-template-versions.mjs') |
packages/spec/src/identity/position-delegatable-enforcer.pin.test.ts:57 | join(REPO_ROOT, 'packages', 'lint', 'src') |
packages/qa/downstream-contract/test/source-resolution.pin.test.ts:87 | resolve(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 alternation — skills/ 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
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