Skip to content

fix(devx): assert the seed escape depth a tree-scoped walk is rooted at - #18340

Merged
os-try-charles merged 4 commits into
mainfrom
claude/issue-15565-seed-escape-depth-assertion
Sep 16, 2026
Merged

os-try-charles merged 4 commits into
mainfrom
claude/issue-15565-seed-escape-depth-assertion

Conversation

@os-try-charles

@os-try-charles os-try-charles commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #15565

Clause-②: no
Reason: no spec surface is narrowed or widened here. The diff is one gate script under scripts/; packages/spec is read by the gate, never edited.

What this adds

A sixth limb to check:cross-package-test-inputs, judged on the SEED a directory descent is rooted at rather than on the names the descent produces.

The five existing limbs are package-scoped or name-scoped, and a readdirSync descending on a loop variable is neither: it names no literal (limb 3 blind), adds no glob (limbs 4 and 5 blind), and inside a package that already carries a table entry the two package-scoped limbs are already satisfied. So the tree-scoped absence pin the ADR-0049 retirement playbook mandates could land in packages/spec with its whole radius unhashed while this gate exited 0 naming nothing.

The input was already there: the escape verdict is computed from the seed's DEPTH, and an argument this scan cannot read costs the NAME and keeps the DEPTH.

The narrowed mandate, in the source

Triage ruled 「先收窄 mandate,再断言收窄后的半径」 and the maintainer's batch ruling returned the card to the queue on the grounds that it restores an invariant with no product-semantics fork. The narrowed mandate is written into the gate's own header docblock (not only here), and it is what the limb asserts:

A tree-scoped pin descends from NAMED ROOTS, and every root it descends from is a directory some declared glob REACHES — as is every directory below that root. It does not descend from the repo root itself.

Stated at the grain the limb judges at: declaring one FILE under docs/audits/ reaches INTO docs/ and no further, so a walk rooted at docs/ is judged by its children. The header records both bounds it accepts (directory REACH rather than file coverage; dot-directories skipped as the pins skip them).

Measurements — every one paired, none recalled

Baseline re-taken on objectstack-ai/objectstack at a09725dde (today's main), because the card's readings were 8e18931dd. Each limb was fired by one anchored mutation, the gate's exit code captured to disk before anything read it, and each file restored by blob equality against HEAD plus an empty git diff HEAD.

limb firing mutation exit
1 escaping package, no entry delete the @objectstack/metadata-protocol entry 1, names the package
2 stale entry add an entry for a non-escaping package 1, names it stale
3 named path no glob covers add a quoted scripts/js-comment-mask.mjs to a test 1, names path and test
4 glob nothing holds add a glob to the entry AND to turbo.json 1, names the glob
5 turbo.json missing input delete one $TURBO_ROOT$ input 1, names the input
6 the blind spot add one undeclared root to the live pin's walk 0, names nothing

That last row is the card's claim, re-taken rather than recalled. The same mutation on this branch:

Ablation discipline: no sed -i / perl -i. Every mutation went through an anchored replacement that exits non-zero when the anchor is absent or not unique, printing the occurrence-count delta and the byte-length delta; every restore is proved by git hash-object equality with the HEAD blob and an empty git diff HEAD, never by an exit code; every driver carries trap … EXIT INT TERM.

What the limb accepts today, and why it is not the allowance the ruling refused

ACCEPTED_WALK_RADII records THREE pre-existing census and corpus sweeps in other packages — none of them a retirement pin, each already carrying its own written bound in its test's header. The MANDATED shape is enforced from the first run: the live pin's five roots are all reached by @objectstack/spec's globs, and a new pin whose root they do not reach reds. Option C — a dated allowance for the mandated shape — is what triage refused, and is not what this is. Buying the three radius widenings instead would be three cost decisions nobody has ruled on, in three packages outside this card.

A defect this branch found in its own first draft

The ledger originally spelled three test paths and one nested root whole, and named the bare recursive packages glob in backticks inside a reason string. scripts/pm/dispatch-gates.mjs reads every quoted span in a gate's MODULE BODY as a path that gate watches — a backticked span INSIDE a quoted sentence included — so those four literals put five watch hints on this gate that it does not read, and two of them turned pins red in check:pm-dispatch-gates. Rows are now segments joined at runtime and reasons name globs in words. Measured both sides: this gate's hint population is 140 with 0 declared here, on main and on this branch alike.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 33 runnable commands for this path. All 33 run green. The --ran reconciliation and the NOT-MEASURED families CI owns are listed in the dispatch report.

No changeset: nothing published moves. Measured rather than assumed — 70 manifests carry a files[], none names anything under the repo-root scripts/ directory, and the positive control is that all 70 name dist.

Acceptance notes

  • repoRelativeLiterals()'s top-level alternation is packages|apps|examples|content|scripts|skills — it has no docs, while docs/... IS a declarable glob (@objectstack/spec declares one audit file there). So a test that quotes a whole docs/... path is invisible to the flat collector, the same shape as the missing skills/ the gate's own header records. Measured with a lit control: the docs/ literal collects nothing, the scripts/ literal collects. Filed separately rather than widened here — it is a different limb's data set.
  • symlinkSync is absent from NON_DESCENT_CALLEES while its siblings cpSync / renameSync / rmSync are present, so a symlinked directory reads as a walk root. On this tree that is scripts for @objectstack/spec, already declared and arguably a real input (the test runs a build through that symlink). Noted, not filed: no PR and no author is heading for this file.

Authored by the domain:devx executor seat in session session_017ef78bLdybu3AffehKkhfk (https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk) — recorded in prose because this body was edited after creation, and the edit channel appends its own footer block.


Generated by Claude Code

`check:cross-package-test-inputs` computed a seed's escape depth for the escape
verdict and never asserted it, so a directory walk descending on a loop variable
was invisible to all five limbs at once: it names no literal (limb 3 blind), adds
no glob (limbs 4 and 5 blind), and inside a package that already carries an entry
the two package-scoped limbs are already satisfied. A tree-scoped absence pin --
the shape the ADR-0049 retirement playbook mandates -- could land with its whole
radius unhashed and the gate exited 0 naming nothing.

Adds the sixth limb, judged on the SEED a descent is rooted at rather than on the
names the descent produces, which is what limb 3 has already proved it cannot
enumerate. The mandate is narrowed first (the triage ruling) and the narrowed
radius is what is asserted: a pin descends from named roots, every root and every
directory below it is reached by a declared glob, and it does not descend from the
repo root -- the radius the table's positive-glob grammar cannot spell.

Three pre-existing census and corpus sweeps in other packages walk wider than
their entries declare. They are recorded in the shrink-only ACCEPTED_WALK_RADII
with what closes each, and the same pass reds a ledger entry nothing owes any
more, so it cannot grow quietly.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
… judged at

The mandate docblock said no declaration reaches `docs/`, and the limb itself
disagrees: `@objectstack/spec` declares one FILE under `docs/audits/`, which
reaches into `docs/` and no further. Re-stated at the grain the limb judges --
a root's CHILDREN -- with the reading that shows it, and the three paired
red/green/reverse readings that landed the limb recorded beside it, each named
with the tree they were taken against.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
…ch hints

The ledger spelled three test paths and one nested root whole, and named the
bare recursive packages glob in backticks inside a reason string. Every quoted
span in a gate's module body is read by the dispatch derivation as a path that
gate watches -- a backticked span INSIDE a quoted sentence included -- so those
four literals put five watch hints on this gate that it does not read.

Two of them turned pins red in `check:pm-dispatch-gates`: the one holding that
no hint of this gate reaches a TSX test file (the bare packages glob reaches
every file under it), and the one holding that its covering hint is inherited
from the declaration module rather than declared here.

Rows are segments joined at runtime and reasons name globs in words. Measured:
this gate's hint population is 140 with 0 declared here, on `main` and on this
branch alike.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 15, 2026 — with Claude
@os-try-charles
os-try-charles marked this pull request as ready for review September 16, 2026 00:16
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 500c1b5 Sep 16, 2026
51 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-15565-seed-escape-depth-assertion branch September 16, 2026 00:42
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… the whitelist against turbo.json (objectstack-ai#18869)

Fixes objectstack-ai#18342

Clause-②: no (narrowing)

`repoRelativeLiterals()` recognised a repo-relative path literal only
when it started at
`packages|apps|examples|content|scripts|skills`, while its own header
note promised "every
top-level directory a declared glob can name". The note was false, so a
test naming a whole
`docs/...` path inside one quoted string was invisible to the flat
collector and this gate
stayed green over an undeclared cross-package read.

## What landed

- The alternation is hoisted into `RECOGNISED_TOP_LEVEL_DIRECTORIES` and
the regex is built
from it, so the list is the single place a directory is added. `docs`
and `.claude` join it.
- `turboRootTopLevelNames()` reads back what `turbo.json` actually
declares and splits it
  against the **git index** into directories and repo-root files.
- A new self-test battery reconciles the two, so the header note is now
checked rather than
promised: the next top-level directory to gain a `$TURBO_ROOT$` input
reds this gate instead
of arriving unnoticed. Registered in `SELF_TEST_BATTERIES`; floor raised
12 to 13.
- `turbo.json` was read as the criterion and **not modified**.

## Acceptance 2 — the census, taken on this branch's base

Every `$TURBO_ROOT$/...` entry's first segment, against the old
whitelist. Counts are mine,
re-taken at `f9a9e76fee`; they match the dispatching seat's independent
reading.

| first segment | entries | old whitelist | disposition |
|---|---:|---|---|
| `packages` / `scripts` / `examples` / `content` / `skills` | 86 / 55 /
18 / 16 / 4 | recognised | unchanged |
| `docs` | 3 | **not recognised** | **added** |
| `.claude` | 1 | **not recognised** | **added** |
| `.github` | 1 | **not recognised** | **deferred, declared — see
below** |
| `sdui.manifest.json` | 1 | not recognised | a repo-root **file**, not
a directory — see below |

`apps` is declared 0 times and stays in the list: the note says a glob
*can* name it, and
`apps/` is a real top-level directory. Not a defect, untouched.

## Why `.github` is deferred rather than added

Recognising `.github` flat turns this gate **red on two literals that
are over-collection
rather than reads**:

| file | line | literal | what it actually is |
|---|---|---|---|
| `packages/cli/test/scaffold-ci-script-parity.test.ts` | 17 |
`.github/workflows/ci.yml` | inside a **block comment**. The file's real
read is line 74, a reconstructed path under `packages/`, already
declared |
| `packages/create-objectstack/src/template-consistency.test.ts` | 376 |
`.github/workflows/ci.yml` | `expect(scaffolded).toContain(...)` — an
assertion about the **scaffolded project's** contents |

Neither test opens this repo's own workflow. Both literals collide with
it because a
scaffolded project's CI file has the same repo-relative spelling, and
`statSync` then confirms
the collision as a real file. The declaration this gate would demand
folds every edit of our CI
workflow into two packages' test hashes, to hash a file neither one
reads.

Per the dispatch's stop-line I have **not** fixed those two and have
**not** shrunk the
whitelist to chase green. `.github` sits in
`TOP_LEVEL_DIRECTORIES_PENDING` carrying that
reason, and the battery pins the map's exact keys, so the deferral is
declared and loud rather
than silent. **The disposition is the dispatching seat's call.**

Worth recording: `.github` is **not invisible** meanwhile. The
reconstruction half already
rosters `.github/workflows/scaffold-e2e.yml` for `create-objectstack`,
declared on
`create-objectstack#test`. What is deferred is the **flat** spelling
alone.

## The fourth shape — `sdui.manifest.json`, a repo-root file

The note promises "every top-level **directory**". `sdui.manifest.json`
is a repo-root file, so
it is a different shape, and my judgement is to **scope the note to
directories and make the
omission loud** rather than fold it into the same regex.

The cost of the alternative is concrete. A directory anchors the match
on its own name followed
by `/`, which is what separates `docs/x.md` from prose. A repo-root
file's whole repo-relative
path is a **bare filename**, indistinguishable from any other quoted
string; the cheapest rule
that would reach `sdui.manifest.json` — collect a quoted dotted word —
also collects
`package.json`, which `statSync` then confirms as a real repo-root file.
Every test that merely
names that string would roster it, and the over-collection this
collector calls harmless stops
being harmless: it would force a declaration on a file nothing reads.

So `TURBO_ROOT_FILE_INPUTS` pins the repo-root files `turbo.json`
declares today and the battery
asserts the live reading equals it — a **second** one reds here instead
of arriving unnoticed.

## Readings

All taken at base `f9a9e76fee`. Gate exit codes captured before any
pipe.

**Collector, fed directly (printed, not counted).** Before to after:

```
'docs/audits/2026-07-unknown-key-strictness-ledger.md'   []  ->  ['docs/audits/...']
'docs/qa/platform-checklist/areas/grid.json'             []  ->  ['docs/qa/...']
'.claude/skills/spec-property-retirement/SKILL.md'       []  ->  ['.claude/skills/...']
'.github/workflows/scaffold-e2e.yml'                     []  ->  []          (pending, declared)
'sdui.manifest.json'                                     []  ->  []          (repo-root file)
'scripts/js-comment-mask.mjs'      ['scripts/js-comment-mask.mjs']  (unchanged, lit)
'content/docs/deployment/cli.mdx'  ['content/docs/deployment/cli.mdx']  (unchanged, lit)
'zzqq/nope.md'                                           []  ->  []          (dark)
'.zzqq/nope.md'                                          []  ->  []          (dark)
```

**DARK, whole tree, at literal granularity** — every (package, literal)
the collector rosters,
dumped from `findEscapingPackages()` before and after:

- before **219** rows, after **220** rows
- **removed: 0** — every already-visible literal keeps its verdict,
verbatim
- added: exactly one, the bright control proving the new list sees
something real —
`@objectstack/plugin-auth
docs/qa/platform-checklist/areas/identity-auth.json`
(named by `checklist-refusal-envelope-consistency.test.ts`, already
inside that package's
  declared `docs/qa/platform-checklist/areas/*.json`)
- whole-tree gate output is byte-identical before and after: `OK: 29
package(s) read outside
themselves ... 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 2023
test file(s) ...`

**LIT, both directions, one per directory.** Each injects a flat literal
into a test that
already escapes, in a package whose declared globs are known, so no
`turbo.json` edit is needed.
Mutations restored after every leg and the restore proven by blob hash,
not by exit code:

| leg | host | literal | expected | got |
|---|---|---|---|---|
| `docs` undeclared | plugin-auth | `docs/protocol-upgrade-guide.md` |
red | **exit 1**, names the file |
| `docs` declared | plugin-auth |
`docs/qa/platform-checklist/areas/automation.json` | green | **exit 0**
|
| `.claude` undeclared | spec | `.claude/launch.json` | red | **exit
1**, names the file |
| `.claude` declared | spec |
`.claude/skills/spec-property-retirement/SKILL.md` | green | **exit 0**
|
| `.github` pending | plugin-auth | `.github/workflows/ci.yml` | green |
**exit 0** |
| `.github` promoted | plugin-auth | same, `.github` temporarily in the
list | red | **exit 1**, names all three |

The last pair is the honest reading for the deferred case: the flat
collector stays green on
`.github` **because the entry is pending**, not because it cannot see —
promoting the entry reds
it immediately.

**Self-test:** 229 cases before, **255** after — exactly the +26 the new
battery pins. All 14
batteries registered, the 12 pre-existing ones intact (13 were declared
before; floor 12 to 13).

## Changeset

`skip-changeset`, measured rather than assumed: no package's `files[]`
ships anything under
`scripts/` (checked across every `packages/**`, `apps/*` and
`examples/*` manifest, with a
positive control reading `packages/spec`'s real `files[]` back). Nothing
published moves.

## Acceptance notes — noted, not filed

-
`packages/drivers/driver-sql/src/live-dialect-matrix.budget.test.ts:107`
genuinely reads
`join(repoRoot(), '.github/workflows/ci.yml')`, and this gate cannot see
that file **at all**:
its `repoRoot()` (line 38) is a hand-rolled `for(;;)` walk rather than
one of the recognised
`findUp` seed spellings, so the file yields no escape and contributes
nothing. That read is
undeclared today. It is a **different mechanism** from this card (seed
recognition, not the
top-level whitelist) and is reported to the dispatching seat rather than
fixed here.

Refs: objectstack-ai#18340, objectstack-ai#15565, and the `skills/` precedent recorded in this
gate's own header note.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3

---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants