Skip to content

feat(pm): give dispatch-gates a third verdict — unreachable - #9939

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-9883-dispatch-gates-unreachable
Aug 19, 2026
Merged

feat(pm): give dispatch-gates a third verdict — unreachable#9939
os-steve merged 2 commits into
mainfrom
claude/issue-9883-dispatch-gates-unreachable

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9883

One hintCovers sweep over the tracked files gives scripts/pm/dispatch-gates.mjs a
third verdict — unreachable: a gate family whose whole declared population matches
nothing in the tree. Such a family reports exactly what a family that scanned a real
population and found no problems reports — green, silently — which is #4690 one level up.

Visibility only — and the proof that nothing else moved

The verdict is cross-cutting, not a fourth bucket. matched / undetermined /
silent all answer a question about the card ("is this family relevant to these
paths?"); unreachable answers a question about the tree ("does this family's declared
population exist at all?"). It therefore sits outside the residue accounting throw for
the same double-counting reason the unfiltered-workflow count already does, and
classifyEntry is untouched.

Measured against origin/main from a second worktree, same commit, same corpus:

evidenceresult
--tier over 134 sampled paths + 3 multi-path invocations, 591 linesbyte-identical (md5 22abac30… both trees)
full derivation, 7 path sets × (plain, --residue)0 lines removed or changed, 70 added
this tool's own watch-hint setunchanged (6 hints, byte-identical) — so no card's matched column moves because this file grew

No merge-blocking context is added and no accept/reject behaviour changes anywhere.

H1 — the re-derived count: 6 of 114 (the card's census said 5 of 110)

main has moved since PR #9799's census; the recognizer agrees with it, including the
'application/json' specimen the card names. Every one of the five scripts below
declares exactly one path literal, and it is dead:

familydead populationwhy it could not reach
check:driver-memory-census@objectstack/driver-memorynever was a repo path — a package specifier; the gate's real population is a git ls-files census it computes
check:examples-live-importsexamplesthe tree HAS it — a single-segment literal carries no separator, so hintCovers refuses it as too generic
check:objectui-pin-freshobjectstack-ai/objectuinever was a repo path — a GitHub repo slug; its real population is the top-level file .objectui-sha
node scripts/check-objectui-pin-fresh.mjsobjectstack-ai/objectuisame script, discovered a second time through its direct invocation
check:release-bodyapplication/jsonnever was a repo path — a MIME type; its real population is the packages' CHANGELOGs, named only inside its self-test (masked)
node scripts/check-skill-frame-freshness.mjsrefs/remotes/origin/mainnever was a repo path — a git ref

Per ruling 2 none of these is fixed here, and no issue was filed for them either —
the report is on the card so the triage stays in one place.

Why family-level and not per-hint, measured on this tree: of 510 unique hints under
scripts/, 263 are dead, and 38 of the 86 declaring scripts carry at least one.
A per-hint verdict is a noise generator; a family whose entire population is dead is
the reportable fact.

H2 — dormant vs unreachable: not expressible from the tree, so it is not claimed

A population that is empty by design is indistinguishable, from the tree alone, from a
hint spelled for a layout that moved: intent is not in the tree and no signal in this repo
carries it. Rather than conflate them, the verdict names why it could not reach, which
is mechanical — three causes, each printed in words:

  • never was a repo path — no tracked path under its first segment (5 of the 6);
  • the tree stops at X; the layout moved under it — the usual real miss (no live
    instance today; the ablation below manufactures one);
  • the tree HAS it; the covering rule refuses the literal as too generic — the
    examples case. Without this the reader goes hunting a directory sitting in front of
    them.

If dormancy ever needs declaring rather than inferring, the shape is the
# dispatch-gates: marker convention this file already reads — built when a real instance
asks for it, not before.

H3 — the sweep survives its own emptiness by refusing, not by reporting zero

Demonstrated end to end, not only in fixtures:

  • corpus empty (a real git init repo, zero tracked files) → refuses: "the
    tracked-file listing came back EMPTY … zero is a broken scan, not a clean repo (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690)"
    ;
  • not a git work tree → refuses, naming --tier as the half that needs no tree;
  • recognizer broken (ablation: hintCovers forced to false) → the tool exits 2 with
    "every one of the 79 famil(ies) that declare a population reached nothing … suspect the
    recognizer … before reading it as a defect count"
    — a threshold-free guard that fires
    only on the degenerate all-or-nothing shape, never on a count someone finds surprising;
  • residueLines refuses an omitted count and an omitted/zero corpus size, so a zero
    can never print without the number of files it swept beside it.

The sweep runs before a single line is printed, so a refusal is a failed derivation rather
than a footnote under an answer that already looks complete.

H4 — the two ablations

1. A hint pointed at a path that cannot exist is reported. In
scripts/check-service-providers.mjs (one live hint) the literal was rewritten to a file
that does not exist. Landing confirmed independently of the editor's exit code: removed
text grep -c → 0, injected text → 2, git diff --stat → 2 insertions / 2 deletions.
The sweep went 6 → 7 and named it with the layout moved reason. Restore leg:
git checkout HEAD -- the file, removed text back to 2, injected text 0,
git status --porcelain empty, sweep back to 6.

2. Nothing else moved — the table at the top of this PR.

Gates

Derived with the tool itself (no paths, off the merge base) at the final commit
7525a9250a, then run:

pnpm check:pm-dispatch-gates -> ✓ dispatch-gates self-test: 310 cases pass. (289 before)
pnpm check:nul-bytes -> check-nul-bytes: OK (scanned 6309 text file(s) … no raw ASCII control bytes).
pnpm check:cross-package-test-inputs -> OK: 12 package(s) read outside themselves, all declared …
pnpm exec eslint scripts/pm/dispatch-gates.mjs --no-inline-config -> clean

No changeset: this is a scripts/pm/ tool and publishes nothing, so the PR carries
skip-changeset (ruling 5).


Generated by Claude Code

A gate family whose declared population matches nothing in the tree reports
the same thing as one that scanned a real population and found no problems:
green, silently. One hintCovers sweep over the tracked files names those
families, with the reason each could not reach.
Visibility only: the sweep is cross-cutting, like the unfiltered-workflow
count beside it, so no existing verdict moves and the derived gate set is
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
The header lists what each verdict means and what the exit code promises;
leaving the sweep out of it would be the same drift this file polices.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@os-steveos-steve added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m labels Aug 19, 2026 — with Claude
@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PM review — ACCEPT. Ruling 1 held, and H2 was settled by measurement rather than taste. Arming.

Verified at 7525a9250a: 1 file, +410/-9, GOVERNED_HITS=NONE, no non-green gates. Self-test 289 → 310 (+21).

⭐ H4 ablation 2 — the proof I actually needed on this file

This is the tool every seat in the fleet runs to derive its gate family and its tier. A regression here is silent and fleet-wide, so "ruling 1 held" had to be shown, not stated:

measured against a second worktree detached at origin/main, same corpus: --tier over 134 sampled paths + 3 multi-path invocations (591 lines) is BYTE-IDENTICAL, md5 22abac3008b88721972305483c9a16c2 in both trees; the full derivation over 7 path sets × (plain, --residue) diffs to 0 lines removed or changed and 70 added, all of them the new section.

A second worktree at origin/main is the right instrument — comparing against a remembered baseline would have proved nothing. And the extra check nobody would have asked for: this tool's own watch-hint set is unchanged, so no other card's matched column moves merely because this file grew. That is the second-order effect of editing the deriver, and you closed it.

classifyEntry untouched, the verdict sits outside the residue accounting throw as a cross-cutting count rather than a fourth bucket. Clause-②: no stays honest.

⭐ H2 — you decided granularity by measuring both, and the numbers decide it

per-HINT would report 263 dead of 510 unique hints and 38 of the 86 declaring scripts — the noise generator H2 predicts. per-FAMILY (every hint dead) reports 6.

That is the whole question answered in one line. I asked whether dormant-by-design was expressible; your answer is the honest one — intent is not in the tree and no signal in this repo carries it, so the verdict does not pretend. What is mechanical is why it could not reach, and each dead hint prints one of three causes:

  • "never was a repo path" — 5 of 6 (a package specifier, a GitHub repo slug, a MIME type, a git ref);
  • "the tree stops at X; the layout moved under it" — the usual real miss, no live instance today, which is why ablation 1 had to manufacture one;
  • "the tree HAS it; the covering rule refuses the literal as too generic"check:examples-live-imports.

That third cause is the one worth having. Without it a reader goes hunting for a directory sitting in front of them — and it is the only one of the six where the derivation, not the gate, is what cannot express the population. Naming that distinction is what stops the verdict from reading as an accusation against six gates.

Declining to build a dormancy declaration — noting the # dispatch-gates: marker convention as its shape but not building it with zero instances asking for it — is the right restraint.

⭐ H3 — demonstrated on real repos, not fixtures, and it caught my own warning

Three legs, all end-to-end:

That third leg is my H1 caveat compiled into the tool. I wrote it as advice to you; you made it something the next reader cannot miss.

And residueLines refusing an omitted unreachable count, an omitted corpus size, and a zero-file corpus means "0 unreachable" can never print without the number of files swept beside it — the bare zero is the exact shape this whole card exists to kill.

H1 — 6 of 114, and ruling 2 respected

Card said 5 of 110. Re-derived: 6 of 114, all named with causes, none fixed and none carded — so my triage stays in one place, which is what ruling 2 asked. check:examples-live-imports flagged as the one with a known one-line remedy on the gate side (spell the hint as a subtree, per #9626).

The negative result, measured and correctly not filed

the same defect class one level over — a CI paths: trigger glob matching no tracked file — has 0 of 26 positive patterns dead today

Checking the adjacent surface and reporting clean is worth as much as finding a defect, and filing nothing was right. It also bounds #9829's neighbourhood: the paths-filter family has a scheduling gap, not a dead-pattern gap.

Ablation-1 hygiene

Landing proven independently of the editor's exit code — removed-text grep -c 0, injected-text grep -c 2 of 2, git diff --stat 2/2 — then the restore leg verified back to 2/0 with porcelain empty. #9914's lesson, applied without being told twice. And the rebuild statement is stated rather than omitted: no dist/ is interposed, so the grep-count pair is the equivalent proof.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 19, 2026 09:08
@os-steve
os-steve enabled auto-merge August 19, 2026 09:08
@os-steve
os-steve added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit ba5709cAug 19, 2026
27 checks passed
@os-steve
os-steve deleted the claude/issue-9883-dispatch-gates-unreachable branch August 19, 2026 09:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dispatch-gates needs a third verdict unreachable — 5 of 110 gate families declare a population that matches nothing

2 participants

@os-steve@claude