You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
dispatch-gates never names check:entry-guard for a NEW scripts/ file - its declared population is an enumeration of current members #10784
Found while working #10703, which adds scripts/pr-labels.mjs — a new file directly under scripts/. node scripts/pm/dispatch-gates.mjs did not name check:entry-guard, the gate whose population is exactly that directory. The gate then failed in CI on Lint & Repo Gates, after the dev had reported.
This is not a one-off miss. Re-derived after the file was committed and tracked, at e0accbffc7:
It is absent from the matched list, absent from the Unreachable list, and absent from the model-tier lines. --residue places it in Silent — "source names paths, none of which cover yours":
Those names are KNOWN_IMPORT_UNSAFE in scripts/check-entry-guard.mjs:437-448 — the allowlist of the ten files that already violate the import-safety half. The derivation reads that literal list as the gate's declared population.
So the population is an enumeration of the files that already exist. A newly added scripts/*.mjs can never appear in it, by construction — which inverts the verdict exactly where it matters most. At runtime the gate scans all 127 scripts/ files and judges new ones; the derivation says "silent" precisely for the case most likely to fire. A gate that is silent for edits to existing scripts and silent for new ones is silent always.
dispatch-gates already warns that silent is its weakest claim and names two ways to earn it that have nothing to do with the caller's paths. This is a third: a gate whose declared population is a roster of current members rather than the surface it scans.
The sibling, failing the other way
check:parse-guard covers the same directory and is reported Unreachable — dead: 'scripts' — the tree HAS it; the covering rule refuses the literal as too generic (no path separator).
So both scripts/** gates are invisible to the derivation, by two different routes: one declares a population too generic to match, the other declares one too specific. Anyone adding a script gets neither named.
Directions, not a prescription
Let a gate declare a scan surface distinct from its baseline/allowlist artifact, so check-entry-guard can say scripts/** while still carrying KNOWN_IMPORT_UNSAFE as data. This is the same distinction the docblock already draws for gates that "compute their own population and name only their baseline artifact".
Failing that, treat an allowlist-shaped literal as a hint that the real population is its common prefix (scripts/), and report it as matched rather than silent.
At minimum, make silent distinguishable from silent because the gate enumerates its current members in the residue output, so a reader can tell the weak verdict from the inverted one.
Not filed against check-entry-guard itself — that gate worked correctly and caught the defect. This is about the derivation that failed to name it.
Triage (finding round): promoted → pm:queue, type Bug — the derivation answers "silent" for exactly the case most likely to fire (a new scripts/ file), inverting the verdict where it matters; measured cost already paid on #10703's CI round-trip. Direction: the card's option 1 (let a gate declare a scan surface distinct from its baseline artifact, so check-entry-guard says scripts/** while keeping KNOWN_IMPORT_UNSAFE as data) is the principled fix and also repairs the sibling check:parse-guard route; option 3's residue-output distinction is a cheap addition worth taking in the same PR. scripts/pm/dispatch-gates.mjs is dev-loop tooling → devx, not a governed skills face.
⭐ The interlock only this seat can see — read before designing
PR #10839 merged ~14:12Z and added ESCAPABLE_LITERAL_LEDGER to scripts/pm/dispatch-gates.mjs — a shrink-only, bidirectionally-enforced ledger of gates whose declared population is a bare top-level word. It is seeded with 2 rows, and one of them is check:parse-guard / scripts — a gate on your file surface.
That PR's own leg-C ablation already ran your fix and recorded the outcome:
giving check-parse-guard.mjsROOT_DIR_WATCH_HINTS=['scripts/**'] ⇒ RED as stale, and the derivation then names the gate, with unreachable-by-construction dropping 3→2 — the ledger row and the blind spot close in the same step.
⇒ Your fix will make a ledger row stale, and that red is the ledger working, not your change breaking it. The ledger is shrink-only: deleting a discharged row is the sanctioned path and the failure text says so in as many words (⛔ The ledger is SHRINK-ONLY: a new line in it is not the remedy). ⛔ Do not add a row, and ⛔ do not weaken the ledger to make your change green.
Direction, from triage
Option 1: let a gate declare a scan surface distinct from its baseline artifact — check-entry-guard says scripts/** while keeping KNOWN_IMPORT_UNSAFE as data. This is the principled fix and it also repairs the sibling check:parse-guard route. Option 3's residue-output distinction is a cheap addition worth taking in the same PR.
The defect, restated
KNOWN_IMPORT_UNSAFE (scripts/check-entry-guard.mjs:437-448) is the allowlist of the ten files that already violate the import-safety half. The derivation reads that literal list as the gate's declared population — so the population is an enumeration of the files that already exist, and a newly added scripts/*.mjs can never appear in it by construction. At runtime the gate scans all ~129 scripts/ files and judges new ones; the derivation says "silent" for exactly the case most likely to fire. Measured cost already paid on #10703's CI round-trip.
Serial
⚠️#10840 (the ~33 gates whose population literal has no separator at all) is open, unassigned, and lands in the same file. ⛔ Do not widen into it — that card's remedy would be a third idiom, which #10705 explicitly forbids. If your work makes #10840 cheaper or harder, say so in your report.
⚠️ Re-derive every line number: dispatch-gates.mjs gained +244 lines at 14:12Z, and check-entry-guard.mjs reports 129 files now, not the card's 127.
Standing constraints
⛔ Governed, human-merge-only: docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md — propose wording in the report.
⛔ Never edit content/docs/releases/**. ⛔ Never git stash — refs/stash is shared by every worktree.
⚠️A pipe swallows the exit code. Capture as cmd > log 2>&1; ec=$?.
⚠️The derivation is blind to both scripts/** gates — which is this card's subject. Run check:entry-guard and check:parse-guardby hand, before and after, and show the before/after of whether the derivation names them. That before/after is your proof.
⚠️ The shared checkout at /home/user/objectstack is stale. Measure against origin/main or your own worktree, never the shared tree — this bit me twice today.
⚠️ Apply labels additively, never a whole-set PUT, and read back after the size labeler runs.
Report back to me; I review against GitHub and land it.
{
"issue": 10784,
"status": "done",
"branch": "claude/issue-10784-gate-scan-surface",
"pr": "https://github.com/objectstack-ai/objectstack/pull/10873",
"premise_still_valid": true,
"summary": "Verified the premise against origin/main (c5d0c2fd2e) before writing code: check:entry-guard's whole extracted hint set was the ten KNOWN_IMPORT_UNSAFE paths and nothing else, and check:parse-guard's was the single bare word 'scripts' — both re-derived at current HEAD, not taken from the card. Took triage option 1 with the existing ROOT_DIR_WATCH_HINTS idiom: check-entry-guard.mjs and check-parse-guard.mjs each declare 'scripts/**' as a SCAN SURFACE while KNOWN_IMPORT_UNSAFE stays data, with self-tests deriving both directions from the walked root (relative(REPO_ROOT, SCRIPTS)) so a renamed root cannot leave a stale declaration. parse-guard's walkOutside(REPO_ROOT) census side is deliberately left undeclared and that refusal is pinned. No extractor widened — hintCovers' refusal of a bare root word is untouched. The interlock landed exactly as PR #10839's leg-C predicted: declaring the subtree reddened 'check:parse-guard scripts' as STALE, I deleted the row (shrink-only, no row added, ledger not weakened), and unreachable-by-construction dropped 3→2. Option 3 shipped too, but narrower than first written: my first draft printed 'the gate very likely reads your file', which is a FABRICATED lead for check:where-matcher (one baseline JSON under scripts/, walks packages/** test files) — I retightened it to the claim that is exactly true of both live shapes and pinned that restraint as a self-test case. Two leads for PM, neither filed: (1) 'check:published-files scripts' is now the ledger's only row and its own comment says the decision belongs on its own card — one semantic search returned zero hits but the second was rate-limited, so I did not file blind; (2) the new output names 21 artifact-roster families on this tree, which is a derived list the repo's doctrine says not to copy into a card.",
"tests": "All at HEAD 2f61b2e878, exit codes captured before any pipe, each row quoting the gate's OWN verdict line. HAND-RUN before/after (the derivation is blind to these two, so nothing here relies on it): check:entry-guard before and after '✓ check:entry-guard: 129 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 87 export bindings, 77 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).' exit 0; check:parse-guard before and after '✓ check:parse-guard: 128 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.' exit 0. DERIVATION before/after for a new scripts/ file: before, entry-guard grep count 0 in the default output and filed under Silent by --residue ('names: scripts/check-changeset-no-major.mjs, …'), parse-guard Silent AND 'Unreachable — dead: scripts — the tree HAS it; the covering rule refuses the literal as too generic'; after, both 'matched via scripts/check-new-thing.mjs ⇢ gate source scripts/**', matched 3→5, unreachable 3→2, silent 86→84. SELF-TESTS: check-entry-guard 47→52 cases, check:parse-guard 41→46, dispatch-gates 474→501, all pass. ABLATION — no build or dist is involved: these are plain .mjs read from source by the derivation, not resolved through any package exports, so there is nothing to rebuild and the ablation-dist-preflight does not apply. Run from the COMMITTED state so restore was exact. Mutation confirmed ON DISK by anchored marker counts, never by an editor exit code: a python replace asserted exactly 1 anchor hit per file, then grep -cF of the removed literal read 0 and of the injected ABLATION marker read 2. Direction observed = RED, as predicted: '✗ check:entry-guard is MATCHED for a brand-new scripts/ file', '✗ check:parse-guard is MATCHED…', '✗ …strip it and check:entry-guard goes back to silent' (x2), and '✗ no gate has NEWLY joined the escapable-literal species — FRESH: check:parse-guard scripts' — the ledger's other direction still armed after my deletion. '✗ dispatch-gates self-test: 5 of 501 case(s) failed.' Derivation under ablation reproduced the base exactly (3 matched, entry-guard grep 0). Restored with git checkout <branch> -- <paths>; restore proven byte-clean by decl-restored=1 per file, ABLATION-marker=0, and an EMPTY git status; self-test back to 501 pass. ⚠️ The first ablation exposed a bug in my own test: one case passed VACUOUSLY (with no subtree hint to strip, silent === silent reads as a pass) — 3 red not 5. I added a hints-length check and re-ablated to confirm it now reds. GATE UNION derived by `node scripts/pm/dispatch-gates.mjs` with NO paths (it reads its own merge base; I did not hand it a git diff), run under scripts/pm/os-verify-lock.sh — 'VERDICT command-exit 0 · held the lock 20s · waited 76s (1m16s)': check:entry-guard EXIT=0, check:parse-guard EXIT=0, check:pm-dispatch-gates EXIT=0 ('✓ dispatch-gates self-test: 501 cases pass.'), check:cross-package-test-inputs EXIT=0 ('OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'), check-ci-filter-parity EXIT=0 ('OK: all 82 declared cross-package glob(s) (71 unique) are covered by core or crosspkg…'), check-cross-package-test-inputs EXIT=0, check:nul-bytes EXIT=0 ('check-nul-bytes: OK (scanned 6284 text file(s) … no raw ASCII control bytes).'). Also self-scanned the three changed files with grep -naP for control bytes: no hits. Verified dispatch-gates' OWN extracted hint set is byte-identical before and after my edits (8 entries), so none of the new strings polluted its declared population.",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
huangyiirene commentedon Aug 21, 2026
Triage (finding round): promoted →
pm:queue, type Bug — the derivation answers "silent" for exactly the case most likely to fire (a newscripts/file), inverting the verdict where it matters; measured cost already paid on #10703's CI round-trip. Direction: the card's option 1 (let a gate declare a scan surface distinct from its baseline artifact, socheck-entry-guardsaysscripts/**while keepingKNOWN_IMPORT_UNSAFEas data) is the principled fix and also repairs the siblingcheck:parse-guardroute; option 3's residue-output distinction is a cheap addition worth taking in the same PR.scripts/pm/dispatch-gates.mjsis dev-loop tooling → devx, not a governed skills face.Generated by Claude Code
claude commentedon Aug 21, 2026
Claim:
/pm-dispatchdomain:devx execution seat (bug-first round).Session:
session_01DdCnBGcHeufjrq7drTD3wt· Branch:claude/issue-10784-gate-scan-surface· Worktree:objectstack-issue-10784Container & model:
M,mode:subagent,model: opusFile surface:
scripts/pm/dispatch-gates.mjs,scripts/check-entry-guard.mjs,scripts/check-parse-guard.mjs.⭐ The interlock only this seat can see — read before designing
PR #10839 merged ~14:12Z and added
ESCAPABLE_LITERAL_LEDGERtoscripts/pm/dispatch-gates.mjs— a shrink-only, bidirectionally-enforced ledger of gates whose declared population is a bare top-level word. It is seeded with 2 rows, and one of them ischeck:parse-guard/scripts— a gate on your file surface.That PR's own leg-C ablation already ran your fix and recorded the outcome:
⇒ Your fix will make a ledger row stale, and that red is the ledger working, not your change breaking it. The ledger is shrink-only: deleting a discharged row is the sanctioned path and the failure text says so in as many words (
⛔ The ledger is SHRINK-ONLY: a new line in it is not the remedy). ⛔ Do not add a row, and ⛔ do not weaken the ledger to make your change green.Direction, from triage
Option 1: let a gate declare a scan surface distinct from its baseline artifact —
check-entry-guardsaysscripts/**while keepingKNOWN_IMPORT_UNSAFEas data. This is the principled fix and it also repairs the siblingcheck:parse-guardroute. Option 3's residue-output distinction is a cheap addition worth taking in the same PR.The defect, restated
KNOWN_IMPORT_UNSAFE(scripts/check-entry-guard.mjs:437-448) is the allowlist of the ten files that already violate the import-safety half. The derivation reads that literal list as the gate's declared population — so the population is an enumeration of the files that already exist, and a newly addedscripts/*.mjscan never appear in it by construction. At runtime the gate scans all ~129scripts/files and judges new ones; the derivation says "silent" for exactly the case most likely to fire. Measured cost already paid on #10703's CI round-trip.Serial
dispatch-gates.mjsgained +244 lines at 14:12Z, andcheck-entry-guard.mjsreports 129 files now, not the card's 127.Standing constraints
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md— propose wording in the report.content/docs/releases/**. ⛔ Nevergit stash—refs/stashis shared by every worktree.cmd > log 2>&1; ec=$?.pnpm --filter <typo>exits 0 with "No projects matched the filters" — a whole verification round can be fictitious and read as green #10853, measured today:pnpm --filter <name>with a name that does not exist printsNo projects matched the filtersand EXITS 0. Verify your filter matched.scripts/**gates — which is this card's subject. Runcheck:entry-guardandcheck:parse-guardby hand, before and after, and show the before/after of whether the derivation names them. That before/after is your proof./home/user/objectstackis stale. Measure againstorigin/mainor your own worktree, never the shared tree — this bit me twice today.Report back to me; I review against GitHub and land it.
Generated by Claude Code
os-zhuang commentedon Aug 21, 2026
os-dev-report
{ "issue": 10784, "status": "done", "branch": "claude/issue-10784-gate-scan-surface", "pr": "https://github.com/objectstack-ai/objectstack/pull/10873", "premise_still_valid": true, "summary": "Verified the premise against origin/main (c5d0c2fd2e) before writing code: check:entry-guard's whole extracted hint set was the ten KNOWN_IMPORT_UNSAFE paths and nothing else, and check:parse-guard's was the single bare word 'scripts' — both re-derived at current HEAD, not taken from the card. Took triage option 1 with the existing ROOT_DIR_WATCH_HINTS idiom: check-entry-guard.mjs and check-parse-guard.mjs each declare 'scripts/**' as a SCAN SURFACE while KNOWN_IMPORT_UNSAFE stays data, with self-tests deriving both directions from the walked root (relative(REPO_ROOT, SCRIPTS)) so a renamed root cannot leave a stale declaration. parse-guard's walkOutside(REPO_ROOT) census side is deliberately left undeclared and that refusal is pinned. No extractor widened — hintCovers' refusal of a bare root word is untouched. The interlock landed exactly as PR #10839's leg-C predicted: declaring the subtree reddened 'check:parse-guard scripts' as STALE, I deleted the row (shrink-only, no row added, ledger not weakened), and unreachable-by-construction dropped 3→2. Option 3 shipped too, but narrower than first written: my first draft printed 'the gate very likely reads your file', which is a FABRICATED lead for check:where-matcher (one baseline JSON under scripts/, walks packages/** test files) — I retightened it to the claim that is exactly true of both live shapes and pinned that restraint as a self-test case. Two leads for PM, neither filed: (1) 'check:published-files scripts' is now the ledger's only row and its own comment says the decision belongs on its own card — one semantic search returned zero hits but the second was rate-limited, so I did not file blind; (2) the new output names 21 artifact-roster families on this tree, which is a derived list the repo's doctrine says not to copy into a card.", "tests": "All at HEAD 2f61b2e878, exit codes captured before any pipe, each row quoting the gate's OWN verdict line. HAND-RUN before/after (the derivation is blind to these two, so nothing here relies on it): check:entry-guard before and after '✓ check:entry-guard: 129 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 87 export bindings, 77 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).' exit 0; check:parse-guard before and after '✓ check:parse-guard: 128 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.' exit 0. DERIVATION before/after for a new scripts/ file: before, entry-guard grep count 0 in the default output and filed under Silent by --residue ('names: scripts/check-changeset-no-major.mjs, …'), parse-guard Silent AND 'Unreachable — dead: scripts — the tree HAS it; the covering rule refuses the literal as too generic'; after, both 'matched via scripts/check-new-thing.mjs ⇢ gate source scripts/**', matched 3→5, unreachable 3→2, silent 86→84. SELF-TESTS: check-entry-guard 47→52 cases, check:parse-guard 41→46, dispatch-gates 474→501, all pass. ABLATION — no build or dist is involved: these are plain .mjs read from source by the derivation, not resolved through any package exports, so there is nothing to rebuild and the ablation-dist-preflight does not apply. Run from the COMMITTED state so restore was exact. Mutation confirmed ON DISK by anchored marker counts, never by an editor exit code: a python replace asserted exactly 1 anchor hit per file, then grep -cF of the removed literal read 0 and of the injected ABLATION marker read 2. Direction observed = RED, as predicted: '✗ check:entry-guard is MATCHED for a brand-new scripts/ file', '✗ check:parse-guard is MATCHED…', '✗ …strip it and check:entry-guard goes back to silent' (x2), and '✗ no gate has NEWLY joined the escapable-literal species — FRESH: check:parse-guard scripts' — the ledger's other direction still armed after my deletion. '✗ dispatch-gates self-test: 5 of 501 case(s) failed.' Derivation under ablation reproduced the base exactly (3 matched, entry-guard grep 0). Restored with git checkout <branch> -- <paths>; restore proven byte-clean by decl-restored=1 per file, ABLATION-marker=0, and an EMPTY git status; self-test back to 501 pass. ⚠️ The first ablation exposed a bug in my own test: one case passed VACUOUSLY (with no subtree hint to strip, silent === silent reads as a pass) — 3 red not 5. I added a hints-length check and re-ablated to confirm it now reds. GATE UNION derived by `node scripts/pm/dispatch-gates.mjs` with NO paths (it reads its own merge base; I did not hand it a git diff), run under scripts/pm/os-verify-lock.sh — 'VERDICT command-exit 0 · held the lock 20s · waited 76s (1m16s)': check:entry-guard EXIT=0, check:parse-guard EXIT=0, check:pm-dispatch-gates EXIT=0 ('✓ dispatch-gates self-test: 501 cases pass.'), check:cross-package-test-inputs EXIT=0 ('OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'), check-ci-filter-parity EXIT=0 ('OK: all 82 declared cross-package glob(s) (71 unique) are covered by core or crosspkg…'), check-cross-package-test-inputs EXIT=0, check:nul-bytes EXIT=0 ('check-nul-bytes: OK (scanned 6284 text file(s) … no raw ASCII control bytes).'). Also self-scanned the three changed files with grep -naP for control bytes: no hits. Verified dispatch-gates' OWN extracted hint set is byte-identical before and after my edits (8 entries), so none of the new strings polluted its declared population.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
fix(pm): the #10784 ablation pins NOT MATCHED, not the spelling 'silent'