Skip to content

Commit 0f10c65

Browse files
os-justinclaude
andauthored
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

File tree

‎scripts/pm/check-expected-skips.mjs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@
122122
* is this file's own).
123123
*/
124124

125+
// The structural self-test case below reads the enqueue bar and asserts it still
126+
// names this file, so a change to that bar is a change this gate judges — and
127+
// `dispatch-gates.mjs` masks self-test bodies before it scans for a population,
128+
// which dropped the claim with the fixtures. This declaration is what puts it
129+
// back; the derivation grades it against the read this file really performs, so
130+
// it cannot be satisfied by deleting either half.
131+
// dispatch-gates: self-test-reads .claude/skills/pm-dispatch/SKILL.md -- the structural case asserts the SKILL.md enqueue bar still names this file, so a change to that bar is judged here
132+
125133
import process from 'node:process';
126134
import { spawnSync } from 'node:child_process';
127135
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';

0 commit comments

Comments
 (0)