Repository navigation
Commit 0f10c65
pm(dispatch-gates): derive a gate family for the governed file its self-test reads (#18735)
Fixes #18673
Clause-②: no
`maskSelfTests` blanks every self-test body before `extractWatchHints`
runs, and that mask is right for the reason its own docblock measures —
a self-test is made of fixture paths, and admitting them printed a gate
in the MATCHED column for most of the tree. But a self-test case can
also assert a STRUCTURAL fact about a real tracked file:
`scripts/pm/check-expected-skips.mjs` asserts that the enqueue bar in
`.claude/skills/pm-dispatch/SKILL.md` still names it. That read is not a
fixture, it is a population claim on a file a card edits, and the mask
dropped it with the fixtures. What it cost is on the record: a PR
re-keying that bar derived its gate list with this tool, ran every
derived family to exit 0, reconciled with `--ran` at 0 NOT-MEASURED, and
`Lint & Repo Gates` went red on `check:pm-expected-skips` — the family
the list did not contain. A reconciliation's zero is a statement about
the DERIVED set, so a family outside that set is invisible to it by
construction: a list that confirms itself is more dangerous than no
list.
## The derived list, before and after
Change set: `.claude/skills/pm-dispatch/SKILL.md` alone. Measured twice
each way — once with the path fed on argv and once from a throwaway
commit in a scratch worktree, `--changed` semantics; both derive the
same set, because both reach `derive(paths, …)` by the same call.
| | derived commands | `pnpm check:pm-expected-skips` | `--ran` over the
18-line run record |
|---|---|---|---|
| before (`f8eaf6704`) | 18 | absent | `18 derived, 18 run, 0
NOT-MEASURED, 0 UNRUN` — the false green |
| after (`26dc4f5369`) | 19 | present | `1 of 19 derived famil(ies)
UNRUN — pnpm check:pm-expected-skips [absent from the run record]` |
The row now reads:
```
- pnpm check:pm-expected-skips [lint.yml] matched via .claude/skills/pm-dispatch/SKILL.md
⇢ declared self-test read by scripts/pm/check-expected-skips.mjs '.claude/skills/pm-dispatch/SKILL.md'
```
With all 19 run, the reconciliation closes again: `19 derived, 19 run, 0
NOT-MEASURED, 0 UNRUN`. `--ran` is unchanged in meaning; it now
reconciles a set that contains the family.
## The mechanism — one declaration, one spelling
```
// dispatch-gates: self-test-reads PATH [PATH ...] -- REASON
# dispatch-gates: self-test-reads PATH [PATH ...] -- REASON (shell gates)
```
- **Why a declaration rather than a widening.** `anchoredReadTargets`
already resolves this read — it scans unmasked source, so the
self-test's `readFileSync(join(ROOT, '.claude/…/SKILL.md'))` comes back
from it today. What drops it is `readProgramTargetsInSource`'s boundary:
a followed read must be PROGRAM TEXT, because a gate that parses a data
file it found by walking a tree would otherwise contribute every fixture
it ever touched. Dropping that boundary re-admits the class the mask
exists to refuse. A declaration is decidable where a widening is a
guess, and it puts the fact on the file it describes.
- **ADMITTING only, never INVENTING.** Every declared path is checked
against `anchoredReadTargets` of the same source text and REFUSES
(throws) on a path that is not there. The declaration can only re-admit
a read the scan already sees and the boundary dropped. A declaration
whose read is DELETED stops parsing and reddens every run of the tool —
which is what makes the pin below unsatisfiable by deleting the read.
- **One spelling, deliberately.** The two path-list markers
(`inherited-population` and this one) are now built from one
`MARKER_LINE_HEAD`, so the indent, comment form and key are spelled ONCE
for both; a self-test case pins that the two patterns differ only in
their key. Written as a `//` whole line with the reason on that same
line, so it parses under today's grammar and survives both queued marker
cards behind this one (#18661, comment forms; #18662, the reason-cut
reading) whichever way they land.
- **The read edge carries a label.** Discovery records
`declared-self-test` in a new `entry.readEdge` map beside
`entry.readOrigin` (the counterpart of `hintEdge`), and `coveringKey`
prints `declared self-test read by FILE` rather than `program text read`
— a dev reading the row has to know which claim to go check.
## The class, not the instance — the census pin
`governedReadCensus()` scans every tracked `scripts/**` JS/TS source for
an anchored read of a GOVERNED surface (`AGENTS.md`, `CLAUDE.md`,
`.claude/**`, `skills/**`) and reports each `(script, file)` pair with
whether the script declares it. Its eyes are deliberately WIDER than the
derivation's follow: it scans with `GOVERNED_READ_CALL`, a strict
superset adding the asynchronous and `promises` spellings, so a governed
read the follow cannot carry is REPORTED rather than silently absent.
`GOVERNED_READ_FLOOR` pins the population; four self-test cases hold the
tree to it, and the fourth is the deliverable stated over the whole
class: **every governed file a script structurally reads is DERIVED for
the family that runs that script**, however it is spelled.
The measured census on this tree is three rows:
| script | governed file | declared | derived today, via |
|---|---|---|---|
| `scripts/check-commit-card-trailers.mjs` | `.claude/agents/os-dev.md`
| no | `gate source` |
| `scripts/pm/check-expected-skips.mjs` |
`.claude/skills/pm-dispatch/SKILL.md` | **yes** | `declared self-test
read` |
| `scripts/pm/check-settings-deny-roster.mjs` | `.claude/settings.json`
| no | `gate source` |
The two undeclared rows are the named list this PR does NOT declare:
both already spell their governed path in their MODULE BODY as well as
in their self-test, so `extractWatchHints` sees it and the derivation
already names the family for a card touching that file — measured, both
ways, at this landing. They cost nothing today, and declaring them would
widen this PR onto two gates it was not dispatched to. They are a floor
the next card lowers, and the roster reds in every direction that
matters: a read that disappears, a new governed read, and a `declared`
flag that moves either way.
## Ablation, from the committed fix
Blob-verified at `HEAD:scripts/pm/check-expected-skips.mjs` = on-disk
`775cc9f4`, the declaration line deleted on disk (marker grep 1 to 0,
blob `ed526a71` — the mutation is proven landed, not inferred from an
editor exit code), then:
- derivation for the SKILL.md change set: back to **18** commands,
`check:pm-expected-skips` absent;
- `node scripts/pm/dispatch-gates.mjs --self-test`: **5 of 1788 cases
fail**, and only these five —
- `check:pm-expected-skips is derived for the SKILL.md its self-test
reads (no key)`
- `every governed read in the census is DERIVED for the file it reads` —
reading: `check:pm-expected-skips`,
`.claude/skills/pm-dispatch/SKILL.md`, verdict `silent`
- `each pinned row's DECLARED flag still reads off the file (pinned
declared=true, live declared=false)`
- `the census is not vacuous — it finds the declared row AND undeclared
ones`
- `…and the 0 DECLARED data read(s) are the only hole in it, each one
really carried (none)`
Restored with `git checkout HEAD -- FILE`: on-disk blob back to
`775cc9f4`, `git diff HEAD` empty, `git status --porcelain` empty. The
restore is proven by blob identity and an empty diff, never by an exit
code.
## Self-test readings, at `26dc4f5369`
```
pnpm check:pm-dispatch-gates exit 0 ✓ dispatch-gates self-test: 1788 cases pass.
✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.
pnpm check:pm-expected-skips exit 0 ✓ check-expected-skips self-test: 99 cases pass
```
## Derived gate list, with exit codes
Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` at the committed head
`26dc4f5369` — that is, by the version of the tool this PR changes;
re-derived on the base `f8eaf6704` as a control and the list is the same
30 families, since the change set is the same two files and neither
family membership nor any other family's population moves. All 30 run,
exit code captured with redirect-then-`$?`:
```
node scripts/check-ci-filter-parity.mjs exit 0
node scripts/check-closing-keyword-parity.mjs exit 0
node scripts/check-closing-keyword-parity.mjs --self-test exit 0
node scripts/check-comment-mask-corpus.mjs exit 0
node scripts/check-declaration-mirrors.mjs exit 0
node scripts/check-declaration-mirrors.mjs --self-test exit 0
node scripts/check-scripts-symbol-anchors.mjs exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test exit 0
node scripts/check-self-test-wired.mjs exit 0
node scripts/check-self-test-wired.mjs --self-test exit 0
node scripts/check-self-test-workflow-commands.mjs exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test exit 0
node scripts/check-whole-set-label-write.mjs exit 0
node scripts/check-whole-set-label-write.mjs --self-test exit 0
node scripts/pm/bare-root-worklist.mjs --self-test exit 0
pnpm check:agent-test-spelling exit 0
pnpm check:bash32-floor exit 0
pnpm check:cli-command-ids exit 0
pnpm check:cross-package-test-inputs exit 0
pnpm check:declared-population-live exit 0
pnpm check:driver-memory-census exit 0
pnpm check:entry-guard exit 0
pnpm check:nul-bytes exit 0
pnpm check:parse-guard exit 0
pnpm check:pm-dispatch-gates exit 0
pnpm check:pm-expected-skips exit 0
pnpm check:pnpm-filter-targets exit 0
pnpm check:ratchet-remedy-authority exit 0
pnpm check:refd-timer-probe exit 0
pnpm check:watch-hint-literal exit 0
```
Reconciled: `dispatch-gates --ran: 30 derived famil(ies) accounted for —
30 run, 0 NOT-MEASURED, 0 UNRUN`.
Repo-wide `pnpm lint` (`eslint . --no-inline-config`) at `26dc4f5369`:
exit 0, no output. Not a narrowed run — the whole repository, so no
narrowing has to be argued.
`skip-changeset`: the diff is two files under `scripts/pm/`, which no
package's `files[]` publishes; nothing released moves.
## Acceptance notes
- Out of scope and noted, not filed: #18661 and #18662 are queued on
this same file behind this card. This declaration's spelling was chosen
to survive both — a `//` whole line with a one-line reason — and the
shared `MARKER_LINE_HEAD` means #18661's comment-form widening lands in
one place for all five markers rather than two.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e77a23f commit 0f10c65
2 files changed
Lines changed: 563 additions & 24 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
125 | 133 | | |
126 | 134 | | |
127 | 135 | | |
| |||
0 commit comments