Skip to content

Declare the populations four under-matching gates really read, and refuse a dead declaration - #14188

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-13519-gate-population-underdeclare
Sep 1, 2026
Merged

Declare the populations four under-matching gates really read, and refuse a dead declaration#14188
baozhoutao merged 3 commits into
mainfrom
claude/issue-13519-gate-population-underdeclare

Conversation

@claude

@claude claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #13519

Four of the seven gates in the census declared a population that under-matched
what they actually read. Each is repaired where the read lives — a literal
population declaration in the gate's own module body, held against the very
constant that gate reads from — and the class is closed by giving a verdict to
a fact the derivation already computes and prints as prose.

scripts/pm/dispatch-gates.mjs is not touched. No gate name was added to
any derivation, table or roster.

⭐ The lead gate: matched population 0 → 29, with the command

check:stall-guard-headroom resolves to scripts/measure-stall-guard-headroom.mjs
(not check-stall-guard-*.mjs — re-derived from package.json, as the card
warns). Its only path-shaped literal was DEFAULT_REPO — the repo slug
objectstack-ai/objectstack — which names no tracked file.

node --input-type=module -e "
import * as G from './scripts/pm/dispatch-gates.mjs';
const files = G.trackedFiles(); const tree = G.watchHintTree(files);
const e = G.discoverFamilies({ tree }).byCheck.get('check:stall-guard-headroom');
const own = (e.hints||[]).filter(h => !e.hintOrigin?.has(h));
console.log('declared:', JSON.stringify(own));
console.log('literals reaching the tree:', own.filter(h => files.some(f => G.hintCovers(h,f))).length);
console.log('matched population:', files.filter(f => G.classifyEntry(e,[f]).verdict==='matched').length);
"
before (45b9051248) after
declared literals ["objectstack-ai/objectstack"] ["objectstack-ai/objectstack", ".github/workflows/**"]
literals reaching the tree 0 1 (covering 29 workflow files)
matched population over 7757 tracked files 1 (its own file, by identity) 30

Why it declared nothing is the interesting half, and it is not a forgotten
string. The population is declared — in check-stall-guard-budget.mjs, which
this tool imports WORKFLOW_DIR and scan from. The import follow deliberately
refuses a target that is itself a discovered gate file, so nothing was
inherited. That refusal is right; what was missing is this end of it. A gate that
reads a population through a sibling gate has to declare that population
itself. The declaration is held against the imported WORKFLOW_DIR in the gate's
own --self-test, so a moved read reds there rather than going quiet in a brief.

Per-gate verdict — 4 fit the pattern, 3 need another mechanism

gate script verdict matched population
check:stall-guard-headroom scripts/measure-stall-guard-headroom.mjs fits.github/workflows/** 1 → 30
check:skill-docs packages/spec/scripts/build-skill-docs.ts fitsskills/*/SKILL.md + skills/README.md 2 → 14
check:service-providers scripts/check-service-providers.mjs fits — three PARENT/*/package.json patterns 2 → 55
check:turbo-task-graph scripts/check-turbo-task-graph.mjs fits — the member manifests 3 → 82
check:nul-bytes scripts/check-nul-bytes.mjs needs something else 2 → 2
check-comment-mask-corpus scripts/check-comment-mask-corpus.mjs needs something else 2 → 2
check:refd-timer-probe scripts/check-refd-timer-probe.mjs needs something else 2 → 2

Each was re-confirmed by reading the gate's own header on this tree, independent
of the mechanical classifier. None was already fixed.

Why the last three are refused rather than forced

All three read a repo-wide population and their headers say so:
check-nul-bytes scans git ls-files plus untracked-not-ignored;
check-refd-timer-probe's readTree does the same; check-comment-mask-corpus
walks REPO_ROOT for every authored JS/TS file. A truthful declaration for any
of them is "every file", and the derivation's own header prices that direction:
"22 leads is the same as none." A gate on every card is a gate on no card.

They do not need a declaration; they need a repo-wide / always-runs channel
in the derivation, which is the derivation side and out of this card's fence.
Filed unassigned as #14189, with the per-gate evidence.

What check:skill-docs cost to decide — the tree had already refused the easy answer

The first draft declared skills/**, which is what the idiom's usual escape
looks like. scripts/pm/bare-root-worklist.mjs reddened on it, by name, and it
was right to: it carries the measured triage for this exact root and records this
gate's population as 12 of the 50 files tracked under it (24%), so a subtree
hint names the gate for 38 files it never opens — the REFUSE-WIDE trade this
repo prices as the costlier error.

What that recorded refusal turned on is worth reading closely, because it is a
claim about one spelling: "no single spelling of this idiom reaches both"
the 11 skill entrypoints, and the root README the generator WRITES, which sits
outside every skill directory. Two literals reach both and reach nothing else:
12 of 12, precise and complete. So the declaration stands and the row's
verdict is re-pointed SPELLABLE-UNDECLAREDDECLARED-NARROWER, which is the
value defined for a gate that took the escape at a strictly narrower population
and whose bare root stays uncovered. That is one of the two resolutions the
worklist's own self-test names for this contradiction.

⛔ Its neighbour row, check-skills-token-ratchet on the same root, is not
re-pointed: that gate walks the root recursively at 49 of 50, where the precise
spelling buys one file of discrimination, and its deferral is untouched.

What check:turbo-task-graph cost to decide

Its declaration block used to say the member manifests "stay undeclared on
purpose (the enumerator owns them and declares none)". workspace-enumerator.mjs
does declare none — but its header states why, and the reason points the other
way: "each gate keeps declaring its OWN population in its OWN module body […]
What is consolidated here is the PARSE, never the DECLARATION."
The +41725
(gate, file) pairs it measured and refused are the price of the subtree
claim, ~5400 files. The claim made here is the manifests only — 79 files, every
one of which readWorkspaceScripts really opens — and --self-test holds it
against the enumerator's live answer in both directions, so a twelfth glob in
pnpm-workspace.yaml reds in this gate.

Why this class cannot recur under another gate name

Adding four declarations protects four gates. It does not stop the next gate
shipping with a declaration that reaches nothing — and that is the shape the lead
had: the declaration and its absence print identically. A family whose
declared literals all reach zero tracked files scores an ordinary silence, byte
for byte the output of a gate that declared nothing, and no one reading the brief
can tell them apart.

The derivation already computes this fact. It prints it under --residue:

0 of the 199 declare a population that reaches NOTHING in the tree, swept over 7757 tracked file(s) […] A standing repo fact, not a verdict about your paths

scripts/check-declared-population-live.mjs gives that standing fact a verdict.
No new instrument, no allowlist, no ratchet, and no gate name in it.

The rule is per-family, not per-literal: a gate may legitimately spell a slug,
a sentinel path or an example inside a message beside a live declaration, and
only a whole declaration that reaches nothing is refused.

Proven able to red, by ablation (mutation and restore both confirmed on
disk — git hash-object against the HEAD blob, before and after):

$ git checkout 45b9051248 -- scripts/measure-stall-guard-headroom.mjs
$ grep -c ROOT_DIR_WATCH_HINTS scripts/measure-stall-guard-headroom.mjs   # 0
$ git hash-object scripts/measure-stall-guard-headroom.mjs                # 73d8e93d… ≠ HEAD 18e5cd8f…
$ node scripts/check-declared-population-live.mjs                         # exit 1
✗ check:declared-population-live: 1 of 155 declaring famil(ies) declare a population that reaches NOTHING in this tree.
  check:stall-guard-headroom
    declares: objectstack-ai/objectstack
$ git checkout HEAD -- scripts/measure-stall-guard-headroom.mjs
$ git diff HEAD                                                          # empty
$ git hash-object scripts/measure-stall-guard-headroom.mjs               # 18e5cd8f… = HEAD blob
$ node scripts/check-declared-population-live.mjs                         # exit 0

The stronger rule was implemented and refused

"A gate that enumerates a directory must carry a watch-hint declaration" is the
rule that suggests itself first. Measured at 45b9051248: of 193 discovered gate
files, 114 enumerate a directory and 86 of those carry no declaration under any
spelling of the idiom
. A gate shipping 86 findings is an allowlist with a
verdict attached — and an allowlist of gate names is the repair this lane has
already ruled against three times. Most of the 86 are not defects either: they
declare their population as ordinary path literals, invisible to a scan for the
idiom. Refused, and recorded here rather than in a follow-up, because the refusal
is part of the argument for the rule that shipped.

The second dead declaration the new gate found

The sweep found exactly two families in the fleet. The other is
check:pm-clause2-carriers, whose only path-shaped literal was the
`owner/name` placeholder inside a refusal message. That gate reads no
file in the tree at all
— its whole input is the GitHub API — so it takes the
gate's own remedy (b): the example is respelled `owner`/`name` so it
is no longer read as a declaration, and the gate now carries a
dispatch-gates: no-path-population marker with its reason. The seven-member
repair set is untouched; this is the guard's own remedy applied to the one other
carrier the guard names, and without it the guard could not ship green.

Changeset

skip-changeset: nothing here is published from any package. The diff is gate
scripts, one workflow step, one root package.json script entry, and
packages/spec/scripts/build-skill-docs.tspackages/spec's files field
publishes dist, json-schema, liveness, prompts, llms.txt, README.md,
src/**/*.zod.ts, api-surface and spec-changes.json, and not scripts/.

Gates run — at 04b7f769, the final commit

The family was derived from the actual diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands
(59 commands, re-derived after the last commit and unchanged), plus
node scripts/check-ratchet-remedy-authority.mjs. All 60 pass, except four
that report PREREQUISITE NOT MET because this container's workspace is not built
check-dev-prereqs (exit 1, "the workspace is not built"),
check-test-completeness (exit 3, no saved turbo log — its own text says to
record this as NOT MEASURED), check:dual-build-cjs-loads (exit 3, no dist/)
and check:type-check-debt (exit 3, closure not built). Each exit code was
captured before any pipe. None is a finding.

bare-root-worklist --self-test was a genuine red on the first draft and is
green here — see the check:skill-docs section above for what it caught.

Also run, beyond the derived family: pnpm lint over the whole repo (exit 0, no
narrowing needed), pnpm --filter @objectstack/spec exec tsc --noEmit (exit 0)
and — because --listFiles shows the edited .ts is not in that program —
tsc -p tsconfig.scripts.json --noEmit --listFiles, which does include it
(1 match) and is clean.

Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV


Generated by Claude Code

@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actions github-actions Bot added the size/l label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json da839ba11697703eb9391e36525fd5dd23242a63packageMentionDocs.

@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 11:08
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit 987fe37 Sep 1, 2026
42 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13519-gate-population-underdeclare branch September 1, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

2 participants