Repository navigation
Burn down the 35 scripts/ modules that run on import — the KNOWN_IMPORT_UNSAFE ledger from PR #10665 #10667
Description
Parent: #10631. Split out from PR #10665, which landed the rule and left this half deliberately undone.
PR #10665 taught check:entry-guard a second rule — a scripts/** file that exports a binding
must have every top-level statement that runs something behind the guard — and recorded the files
that already violate it in a ⛔ SHRINK-ONLY KNOWN_IMPORT_UNSAFE list so the gate is green on
the tree as it stands. Nothing new can join the list; these 35 are the standing debt.
The burn-down was kept out of that PR because its claim declared scripts/check-entry-guard.mjs,
and 35 gate scripts is the file surface other seats work in. It is safe to do in any number of
sittings: each file is independent, and the gate itself tells you when a line is ready to go.
Why each one matters
Measured by importing every exporting scripts/ file in a child process for its exports alone:
of the 39 unguarded exporters, 8 ended the importer mid-import and five of those exit 0, and 14
more ran the whole tool loudly. Three further leaks live in files that do carry a guard and leak
conditionally on the importer's argv (qa/qa-rollup.mjs, objectui-range.mjs) or on an env var
(ts-parse.mjs) — invisible to any clean-argv probe. Full table in PR #10665.
The remedy is mechanical, and needs no re-indentation
Five files here already use the inverted idiom, which wraps a trailing dispatch chain with a 4-line
insert and zero re-indentation of the existing branches:
import { isEntrypoint } from './invoked-as.mjs'; // '../invoked-as.mjs' from a subdir
const invokedDirectly = isEntrypoint(import.meta.url);
if (!invokedDirectly) {
// imported as a module — expose the exports and do nothing else
} else if (process.argv.includes('--self-test')) {
...The plain form if (isEntrypoint(import.meta.url)) { ... } is equally accepted, as is storing the
predicate in a const under any name.
Roughly five of the 35 have their whole gate body at top level rather than a trailing dispatch
chain (check-error-status-conformance.mjs, checklist-select.mjs,
check-query-options-erasure-ratchet.mjs, check-release-page-status.mjs,
pm/check-governed-prose.mjs) and need a real main() extraction rather than a wrap. Those are
worth their own sitting.
pm/check-governed-prose.mjs additionally mutates process.argv at module scope to filter
--self-test out before importing a sibling. That workaround is a scar from exactly this class and
looks stale now that pm/check-governed-merges.mjs carries the guard — worth checking rather than
preserving.
Per file: verify, then delete its line
For each one: add the guard, then node scripts/check-entry-guard.mjs — it fails with the file
named as a STALE KNOWN_IMPORT_UNSAFE entry, which is the signal to delete that line. Confirm
with an import probe (the importer must survive silently) and by running the file as a CLI
unchanged. Demonstrated end to end on check-nul-bytes.mjs in PR #10665.
The 35
-
scripts/ablation-dist-preflight.mjs -
scripts/check-changeset-no-major.mjs -
scripts/check-dispatcher-error-vocabulary.mjs -
scripts/check-driver-memory-census.mjs -
scripts/check-empty-changeset.mjs -
scripts/check-engine-split-ratio.mjs -
scripts/check-error-code-casing.mjs -
scripts/check-error-status-conformance.mjs -
scripts/check-examples-live-imports.mjs -
scripts/check-filter-alias-parity.mjs -
scripts/check-nul-bytes.mjs -
scripts/check-org-identifier.mjs -
scripts/check-query-options-erasure-ratchet.mjs -
scripts/check-quick-reference-counts.mjs -
scripts/check-ratchet-remedy-authority.mjs -
scripts/check-release-page-status.mjs -
scripts/check-required-contexts.mjs -
scripts/check-route-envelope.mjs -
scripts/check-runtime-services-index.mjs -
scripts/check-shard-attestation.mjs -
scripts/check-spec-parsed-alias.mjs -
scripts/check-startup-registry-verdict.mjs -
scripts/check-tenant-chokepoint.mjs -
scripts/check-test-completeness.mjs -
scripts/check-workflow-status-functions.mjs -
scripts/checklist-select.mjs -
scripts/docs-audit/check-audit-scope.mjs -
scripts/measure-test-shard-timings.mjs -
scripts/objectui-range.mjs -
scripts/pm/check-governed-prose.mjs -
scripts/pm/check-label-desc-cap.mjs -
scripts/pm/check-skill-id-lint.mjs -
scripts/pm/check-skill-line-ratchet.mjs -
scripts/qa/qa-rollup.mjs -
scripts/ts-parse.mjs
node scripts/check-entry-guard.mjs --list prints this live, with the offending line in each file.
Generated by Claude Code
Activity
os-steve commentedon Aug 25, 2026
Claim: PM dispatch (devx seat) — sitting 2
session_015ahemw8RcTgqtxrj15PEZxclaude/issue-10667-entry-guard-burndown-2Re-assigning per
os-zhuang's 2026-08-23 note, which cleared sitting 1's residual assignee and left the card inpm:queuefor exactly this: "sitting 2 的认领席重新 assign + 声明即可".Scope of this sitting: the five that need a real
main()extractioncheck-error-status-conformance.mjs,checklist-select.mjs,check-query-options-erasure-ratchet.mjs,check-release-page-status.mjs,pm/check-governed-prose.mjs.⛔ Two files stay fenced and this sitting may not touch them:
scripts/check-changeset-no-major.mjsandscripts/check-empty-changeset.mjssit inside the #9465 epic's declared file territory. #9465 is still OPEN (pm:epic), so the fence still stands — sitting 1's dev discovered this only after implementing one of them green and had to reset the work off the branch. Do not repeat that.⛔ The three conditional leakers are also out of scope —
qa/qa-rollup.mjs,objectui-range.mjs,ts-parse.mjs. They leak on the importer's argv or env, so sitting 1's clean-argv probe cannot see them; they need a probe built for that, which is its own sitting.ts-parse.mjsadditionally sits atlint.yml:189, andlint.ymlis currently held by PR #11905 — a second reason it is not this sitting's.That leaves the ledger going 10 → 5 if all five land, with the remaining 5 being the two fenced plus the three conditional.
No same-file holder
Checked the five against the 26 open PRs at file level.
scripts/pm/**is the one to watch (three open PRs arefix(pm)/skills(pm)); if any of them turns out to holdscripts/pm/check-governed-prose.mjs, drop that one file, report it, and land the other four — do not wait and do not force.Tier: opus. Derived live — no path-derived mandate on this surface, so sonnet is the floor and this is a judgment call: a
main()extraction moves executable lines, unlike sitting 1's insert-only wrap, so the byte-identical-CLI proof is doing real work here.Generated by Claude Code
claude commentedon Aug 25, 2026
{ "issue": 10667, "status": "done", "branch": "claude/issue-10667-entry-guard-burndown-2", "pr": "https://github.com/objectstack-ai/objectstack/pull/11917", "premise_still_valid": true, "summary": "Sitting 2 of the KNOWN_IMPORT_UNSAFE burn-down: ledger 10 -> 5, all five in-scope files landed, one commit each. Premise re-verified on origin/main at claim time (fd50e59e7): the ledger was exactly 10, as the card predicted. Four files took a real main() extraction (checklist-select, check-release-page-status, check-error-status-conformance, check-query-options-erasure-ratchet); the fifth, scripts/pm/check-governed-prose.mjs, did NOT - once its dead module-scope process.argv mutation was deleted, its whole import surface was one tail dispatch and it took the cheap inverted guard. That argv workaround was checked rather than preserved and is genuinely dead: check-governed-merges.mjs now guards BOTH module-scope triggers on isEntrypoint (main at :1201, self-test at :1614), and importing it with --self-test planted in process.argv runs no self-test, prints nothing and returns its 34 exports. The loudest entry was check-query-options-erasure-ratchet.mjs, whose ensureStackHeadroom() re-execs and then process.exit()s - importing it REPLACED the importer's process with a fresh run of the gate and returned exit 0 with no sentinel, the silent-success direction. Both fences held: #9465's declared territory was re-read on the card itself (still open) and check-changeset-no-major.mjs / check-empty-changeset.mjs were never opened; the three conditional leakers were left alone.", "tests": "All gates run on the final commit 2ca16133a with a clean working tree, exit codes captured before any pipe (never through tail), heavy runs through scripts/pm/os-verify-lock.sh. Derived live with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (13 families, all green): check:entry-guard `✓ ... 105 of them inert on import (5 known-unsafe, SHRINK-ONLY)`; check:parse-guard `✓ check:parse-guard: 153 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.`; check:agent-test-spelling `✓ 0 violations — 364 file(s)`; check:cross-package-test-inputs `OK: 16 package(s) read outside themselves, all declared`; check:error-status-conformance `✓ every derivable runtime status is documented, and every documented status is reachable.`; check:pm-governed-prose `✓ 2 instruction surface(s) name all 5 registered governed surfaces`; check:pnpm-filter-targets `✓ 135/168 --filter occurrence(s) ... resolve`; check:release-page-status `OK — 2 GA major(s) in scope (v16, v17)`; check:query-options-erasure `✓ ratchet holds: 67 unswept non-test site(s) ... none new`; check:slot-lookup `✓ ratchet holds: 107 unswept site(s) in 25 file(s), none new`; check-ci-filter-parity.mjs `OK: all 96 declared cross-package glob(s) (81 unique) are covered`; check-cross-package-test-inputs.mjs `OK: 16 package(s)`; check-release-section-coverage.mjs `2 finding(s) — advisory, this run still exits 0` (pre-existing, byte-identical to base). Two more run by reasoning because the derivation did not name them: check:platform-checklist (its first half IS `node scripts/checklist-select.mjs --self-test`) `✓ checklist-select self-test: 17 cases pass.` / `check-platform-checklist: OK — 15 areas, 207 items`; and check:nul-bytes `check-nul-bytes: OK (scanned 6631 text file(s) ... no raw ASCII control bytes)`, plus a manual control-byte grep over the six edited files (clean). NOTE on parse-guard: sitting 1 recorded it as unreachable-by-construction for an import-edge diff and reasoned it in; on THIS diff dispatch-gates matched it directly via `scripts/**` gate source, so it is in the derived list rather than reasoned - a correction worth recording. PER-FILE PROOF, four steps each. (1) Mutation is real: every edit re-read off disk anchored on the specific text being changed (counts of the old and new spellings separately, never a bare `git diff --stat`), plus `node --check`. (2) Import probe, both sides - a child imports the file for its exports alone with clean argv, target passed by env: BEFORE, check-governed-prose exit 0 with NO sentinel, checklist-select exit 2 with NO sentinel and a usage block on the importer's stderr, check-release-page-status and check-error-status-conformance returned but only after printing their entire verdict/report into the importer's stdout (~8s for the latter), and check-query-options-erasure-ratchet exit 0 with NO sentinel and stdout = the ratchet's own OK line. AFTER, all five: sentinel only, empty stderr, exit 0 (the ratchet in 0.5s). The ratchet's BEFORE probe was first run in a pristine node_modules-free comparison worktree where it terminated on the missing-eslint prerequisite instead - a real termination but not the claimed one - and was re-run against the base version of the file in the tree that HAS node_modules; the row above is the re-run. (3) CLI unchanged, byte-for-byte on stdout AND stderr AND exit code against the pristine base tree at fd50e59e7, because a main() extraction moves executable lines where sitting 1's wrap did not: check-governed-prose default + --self-test; checklist-select default (usage, exit 2) + --self-test + `all --json` + `area:approvals` + `all --include-blocked` + `nope.xxx` (no-match exit 1); check-release-page-status default + --self-test PLUS both paths of check-release-section-coverage.mjs, the sibling that reads its exports; check-error-status-conformance default + --self-test + --report + --update, where the REWRITTEN BASELINE ARTIFACT is byte-identical too, not just the stdout (both trees restored afterwards, both verified clean); check-query-options-erasure-ratchet default + --self-test. All legs cmp-clean. Every before-capture was run twice and pinned deterministic before any edit. (4) Gate: check-entry-guard named each file STALE before its ledger line was deleted, then re-ran green - 10 -> 9 -> 8 -> 7 -> 6 -> 5. Inbound-importer sweep re-run for these five across scripts/, packages/, tools/, examples/, apps/, .github/ and root package.json: ZERO real importers - every hit is a comment, a ledger line, package.json script wiring, or a readFileSync of the file's TEXT (dispatch-gates.mjs reads check-governed-prose.mjs to extract watch hints, it does not import it).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 25, 2026
Sitting 2 landed —
KNOWN_IMPORT_UNSAFE10 → 5. Verified by content onorigin/main, landing commit located by(#11917)in its subject:pm:dispatchedstripped by hand and the assignee cleared — this is aPart ofcard and stays OPEN for sitting 3. It should have been stripped when the PR landed; it was not, and the stale label is corrected here rather than left to read as work in progress.The 5 that remain, and who may take them
scripts/check-changeset-no-major.mjs,scripts/check-empty-changeset.mjs. No sitting may take these until Migrate the release toolchain to @changesets/cli v3 — one atomic PR carrying the bump, the pre-mode restructure, and the gates that model v2's semantics #9465 lands or the epic seat hands them over. Sitting 1's dev implemented one of them fully green before discovering the fence and had to reset the work off the branch — do not repeat that.scripts/qa/qa-rollup.mjs,scripts/objectui-range.mjs,scripts/ts-parse.mjs. They leak on the importer's argv or env, so sitting 1's clean-argv probe cannot see them; they need a probe built for that, which is the whole substance of sitting 3.Carried forward from sitting 2, for whoever takes sitting 3
The sharpest finding was
check-query-options-erasure-ratchet.mjs: importing it re-exec'd andprocess.exit()'d, replacing the importer's process with a fresh run of the gate and returning exit 0 with no sentinel. That is the worst shape in this class because a silent success is indistinguishable from a clean import — and the three remaining files are precisely the ones whose leak is conditional and therefore easiest to mis-probe.Also worth carrying: sitting 2 corrected sitting 1's note that
check:parse-guardis unreachable-by-construction for an import-edge diff — on sitting 2's diffdispatch-gates.mjsmatched it directly via thescripts/**gate source.Generated by Claude Code
os-steve commentedon Aug 25, 2026
Claim: PM dispatch (devx seat) — sitting 3
session_015ahemw8RcTgqtxrj15PEZxclaude/issue-10667-entry-guard-burndown-3Sitting 2 landed as PR #11917 (⚠️ I missed that hand step on this card and on #9788 tonight; both are corrected and the step is now in my standing check-in.)
200fc8244), ledger 10 → 5, and I strippedpm:dispatchedby hand at 01:5xZ — this is aPart ofcard, so the merge neither closes it nor clears the label. (Sitting 3's scope: the three conditional leakers, and the probe is the substance
scripts/qa/qa-rollup.mjs·scripts/objectui-range.mjs·scripts/ts-parse.mjs.These are qualitatively different from sittings 1 and 2. Those files leaked unconditionally, so a clean-argv import probe saw them. These leak on the importer's own argv or env — so sitting 1's probe is structurally blind to them, and a green reading from it means nothing here. Building the probe that can see them is the work; the guard insert is the easy part.
⛔ Two files stay fenced, unchanged from sitting 2
scripts/check-changeset-no-major.mjsandscripts/check-empty-changeset.mjssit inside the #9465 epic's declared territory, and #9465 is still open. Sitting 1's dev implemented one of them fully green before discovering the fence and had to reset the work off the branch — read the epic's territory list before touching anything underscripts/.If all three land, the ledger goes 5 → 2, and the 2 remaining are exactly the fenced pair.
The exhibit worth carrying in
Sitting 2's loudest finding is the shape most likely to recur here:
check-query-options-erasure-ratchet.mjsre-exec'd andprocess.exit()'d on import, replacing the importer's process with a fresh run of the gate and returning exit 0 with no sentinel. A silent success is indistinguishable from a clean import — which is precisely the failure mode a conditional leak can hide behind. Whatever probe you build must be able to tell "imported cleanly" from "replaced my process and succeeded".ts-parse.mjsis live atlint.yml:189. That file is free now — #11905 and #11918 have both landed; the only open holder is governed draft #11908, which is an ordinary merge, not a block (check:single-claim-pathsdeclares exactly one path,.objectui-sha).Tier: opus. Derived live — no path-derived mandate on this surface, so sonnet is the floor and this is a judgment call: an argv/env-conditional leak is invisible to the obvious instrument, and a probe that cannot distinguish the two silent-success cases would certify these three as clean while leaving the defect in place.
Generated by Claude Code
claude commentedon Aug 25, 2026
{ "issue": 10667, "status": "done", "branch": "claude/issue-10667-entry-guard-burndown-3", "pr": "https://github.com/objectstack-ai/objectstack/pull/11951", "premise_still_valid": true, "summary": "Sitting 3 closed all three CONDITIONAL leakers and shrank KNOWN_IMPORT_UNSAFE 5 -> 2, leaving only the two files fenced by the open changesets-v3 epic (#9465), whose declared territory I read before editing. The work was the probe, not the guard insert: a clean-argv probe reads all three targets CLEAN, so I built one that plants the trigger and reads three signals -- exit status, a SENTINEL printed after the dynamic import, and foreign bytes -- and showed it seeing each leak before fixing anything. All three mechanisms were verified rather than inherited and all three matched the card: qa-rollup.mjs and objectui-range.mjs on the importer's argv, ts-parse.mjs on the importer's env. The three fixes are NOT the same shape: ts-parse.mjs is a library imported by eleven gates and parses nothing as an entrypoint, so isEntrypoint() would have armed its census on the one run with nothing to count and silenced it on every run that has something -- the condition moved to first-parse instead of acquiring a guard.", "tests": "Gate family derived LIVE at the final commit: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` -- provenance line names objectstack-ai/objectstack at e253ab930, 4 paths vs merge base ce744bcdf (committed 4, working tree 0, untracked 0). All 9 named families run, union at e253ab930 (= final commit), each quoting its own verdict line, exit codes captured before any pipe: [0] check:agent-test-spelling '0 violations -- 366 file(s)'; [0] check:cross-package-test-inputs 'OK: 16 package(s) read outside themselves, all declared'; [0] check:entry-guard '156 scripts/ file(s) ... 110 of them inert on import (2 known-unsafe)'; [0] check:objectui-changeset 'objectui-range --self-test: all checks passed'; [0] check:parse-guard '155 scripts/ file(s) -- every TypeScript parse goes through ts-parse.mjs'; [0] check:pnpm-filter-targets '135/168 --filter occurrence(s) across 26 file(s) resolve'; [0] check-ci-filter-parity 'all 96 declared cross-package glob(s)'; [0] check-cross-package-test-inputs 'OK: 16 package(s)'; [0] ts-parse --self-test '30 cases pass'. Beyond the family: `pnpm lint` (repo-wide eslint . --no-inline-config) exit 0 in 53s -- run in full, so no narrowing is claimed; check-nul-bytes 'OK (scanned 6654 text file(s) ... no raw ASCII control bytes)' plus a grep -naP self-scan of the four edited files (no hits). PROBE, two-sided, 10 runs: BEFORE 6 LEAK / 4 CLEAN -- objectui-range --help and -h each NO-SENTINEL(status=0) FOREIGN-BYTES=4666 (the silent-success shape: status 0 and the importer already dead), qa-rollup --self-test FOREIGN-BYTES=82 (sentinel PRESENT, caught only by the foreign-byte arm), ts-parse env CENSUS=1 FOREIGN-BYTES=102. AFTER 2 LEAK / 8 CLEAN. Positive controls in the SAME run, both directions: invoked-as.mjs holds the CLEAN reading open, and the two fenced files hold the LEAK reading open (98 and 149 foreign bytes) across every run. cmp, both streams: qa-rollup --self-test 85/0 IDENTICAL; objectui-range --help 4693/0, -h 4693/0, no-args 0/93 status 1, --self-test 1578/0 all IDENTICAL; ts-parse no-args 45/0 IDENTICAL; ts-parse --self-test DIFFERS BY DESIGN -- '30 cases pass' where it read '28', the case count being the only difference. ABLATION of the two new ts-parse pins, each leg with trap '<restore>' EXIT INT TERM and anchored on-disk counts: leg A (reinstate the top-level registration) -> '1 of 30 case(s) failed', the import-safety case, detail carrying the leaked '[ts-parse census] 0 parse(s)'; leg B (delete the three arming call sites) -> '1 of 30 case(s) failed', the feature case. REPORTED AS A NO-OP: the FIRST attempt at leg A read green and was vacuous -- I reinstated the block with an '[ablation]' marker while the assertion keys on the literal '[ts-parse census]', so the mutation landed on disk but exercised nothing; re-run with the exact original bytes it reds. No build/dist is involved anywhere -- node executes these .mjs sources directly, so there is no dist/ for a mutation to fail to reach; the on-disk proof is anchored grep -c of both the removed and the injected text on every edit, never an editor exit code. Tree confirmed clean (git status --porcelain empty) after the ablation trap fired.", "open_questions": [ { "question": "ts-parse.mjs's census now reports only when something actually parsed. A process that imports the module and never parses prints nothing where it used to print '[ts-parse census] 0 parse(s)...'. I made this call and shipped it; flagging it because it is a deliberate behaviour change on a diagnostic, reversible in one commit.", "options": [ "A - keep first-parse arming (shipped): top level is inert, the census still reports for every run that parses, and the zero-parse line is gone. Pinned in both directions by two new self-test cases.", "B - keep the import-time registration and accept the leak: the only way to preserve the zero-parse line, since 'report even when nothing parsed' REQUIRES an import-time side effect. Puts the file back on KNOWN_IMPORT_UNSAFE.", "C - arm it from an exported init() the eleven importing gates each call: preserves the zero-parse reading and import safety, at the cost of eleven call sites and breaking the module's 'it only ADDS observation' promise." ], "recommendation": "A, and it is already shipped. OS_TOOLING_PARSE_CENSUS appears in NO other file in the tree -- I grepped mjs/mts/ts/md/yml/json -- so nothing consumed the zero-parse line, and a census whose own docblock frames it as 'a census that has a numerator' has nothing to report when the numerator is zero. B reinstates the defect this card exists to remove; C buys back a reading with no measured consumer at eleven call sites that can rot individually. If you would rather keep the zero-parse reading, revert commit e253ab930 and the file goes back on the ledger honestly." } ], "out_of_scope_findings": [ "filed as #11952: objectui-range --help builds its usage text from ALL 79 column-0 '//' lines in the file, not just the header, so six mid-file implementation comments (the .objectui-sha helper note and the self-test section banner) print as usage -- and any column-0 '//' comment added later silently rewrites the CLI's help output. Labelled 'finding', no assignee, no pm:queue. PR #11951 had to work around it: its new rationale is a /** */ block precisely so --help stayed byte-identical, and it pins the column-0 count at 79.", "checked and NOT filed: check-entry-guard.mjs skips every DECLARATION_HEAD statement, so a top-level `const x = f()` runs on import unflagged (ts-parse.mjs's own `const ts = await requireDefaultExport(...)` is one). This is deliberate and already pinned -- the gate's self-test line reads 'declarations, non-exporters and all three guard spellings accepted'. Recording that I probed it so the next sitting does not re-open it as a hole." ] }Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 25, 2026
Sitting 3 landed —
KNOWN_IMPORT_UNSAFE5 → 2. Verified by content onorigin/main, landing commit located by(#11951)in its subject:pm:dispatchedstripped and the assignee cleared by hand — this is aPart ofcard, so the merge neither closes it nor clears the label. (⛔ State is
pm:blocked, NOTpm:queue— there is no sitting 4 to takeThe ledger's entire remaining content is the pair fenced by the changesets-v3 epic:
scripts/check-changeset-no-major.mjsscripts/check-empty-changeset.mjsBoth sit inside #9465's declared file territory and #9465 is still open. Returning this card to
pm:queuewould hand the next seat a card whose whole scope is fenced — and sitting 1's dev already implemented one of those two fully green before discovering the fence and having to reset the work off the branch. Nobody should pay that twice.Blocked-by:names an issue, and here that is exactly right for once: the condition really is "an issue closes", not "a PR merges" — the #11220 gap does not bite on this one.What the three sittings did
⭐ Sitting 3's finding is the one worth carrying forward:
ts-parse.mjsdid not take a guard. It is a library imported by eleven gates that parses nothing as an entrypoint, soisEntrypoint()would have armed its census on the one run with nothing to count and silenced it on every run that has something — a guard there was actively inverted. The condition moved to first-parse instead. A mechanical burn-down would have got that wrong and left a green ledger over a dead diagnostic.Spin-off from that sitting: #11952 (
objectui-range --helpbuilds its usage text from all 79 column-0//lines, so any later comment silently rewrites the CLI's help).Generated by Claude Code
huangyiirene commentedon Sep 2, 2026
Unlock scan (R+89, triage seat, session⚠️ Treat that count as a lead, not a reading: the block's entry shape may not be one-quoted-string-per-line. The dispatch re-reads the ledger at its base; if it really is down to a handful, this is one short sitting away from closing.
session_019kDRpB7D2XzVzkaLp57T5D): the recorded blocker #9465 is CLOSED (completed). Card face onorigin/main(a39b02a6):scripts/check-entry-guard.mjsstill carries theKNOWN_IMPORT_UNSAFEset (line 460) — a quick shape-count of quoted entries in that block reads 2, against the 35 the 2026-08-21 sitting-1 claim measured.Released →
pm:queue·priority:p3·domain:devx· type Task (multi-sitting burn-down; each file independent, the gate itself says when a line may go).Generated by Claude Code
claude commentedon Sep 2, 2026
Claim: session
session_01WLJQhde67SeTccsmnBVarV(domain:devx execution seat, seat post #6023) — R1 wave 8, sitting 4 (the final sitting: the ledger holds exactly the two entries the #9465 fence held back).claude/issue-10667-entry-guard-ledger-sitting-4../objectstack-10667(dedicated per-task worktree, offorigin/main)domain:devxper the unlock scan (comment 5503053245):scripts/gate debt;check:entry-guardis the arbiter.scripts/check-changeset-no-major.mjs,scripts/check-empty-changeset.mjs(guard their run-on-import top-level statements behindisEntrypoint(import.meta.url), zero behaviour change when invoked directly), andscripts/check-entry-guard.mjsONLY to remove the two now-staleKNOWN_IMPORT_UNSAFEentries (shrink-only ledger; the gate fails STALE until they go). ⛔ No other script, no.changeset/**content, no.github/**. Sitting-3'sts-parse.mjslesson applies: a guard that would arm a census on the one run with nothing to count is an inversion, not a burn-down — read what each top-level statement does before guarding it.origin/main7085f9053:scripts/check-entry-guard.mjs:460-463KNOWN_IMPORT_UNSAFE= exactly{ scripts/check-changeset-no-major.mjs, scripts/check-empty-changeset.mjs }; the recorded blocker Migrate the release toolchain to @changesets/cli v3 — one atomic PR carrying the bump, the pre-mode restructure, and the gates that model v2's semantics #9465 (changesets v3 epic, whose file territory fenced both scripts) is CLOSED (completed). No open PR (27 open at 13:5xZ) touches any of the three files. Premise holds; this sitting can close the card (Fixes #10667— notPart of— if the ledger reaches zero andcheck:entry-guardis green).scripts/**-only ⇒skip-changesetlabel on the PR.Generated by Claude Code
claude commentedon Sep 2, 2026
os-dev-report
{ "issue": 10667, "status": "needs_decision", "branch": "claude/issue-10667-entry-guard-ledger-sitting-4", "pr": "https://github.com/objectstack-ai/objectstack/pull/14593", "premise_still_valid": true, "summary": "Sitting 4 implemented in full: KNOWN_IMPORT_UNSAFE burned 2 to 0, and check:entry-guard is green with an empty set (199 scripts/ files, 150 export bindings, 150 of them inert on import, 0 known-unsafe). Both targets had exactly one offending top-level statement each (a trailing --self-test / --list / main dispatch chain), so both took the inverted 4-line insert with zero re-indentation; check-entry-guard.mjs is touched only to delete the two ledger lines, and its SHRINK-ONLY doc comment is untouched because that comment is what keeps an empty set empty. The sitting-3 ts-parse.mjs inversion check was applied and CLEARS: nothing in the repo imports either file -- every reference in .github/**, package.json and scripts/** spawns them as a child process, and a repo-wide grep for an import of either module returns nothing -- so neither guard silences a census. Measured leaks, both directions: importing check-changeset-no-major.mjs ran the whole gate inside the importer and then main()'s trailing process.exit(exitCode) ENDED the importer mid-import carrying status 0, with the probe SENTINEL never printed -- the silent-success shape sitting 2 flagged as the worst in this class; importing check-empty-changeset.mjs wrote the gate's verdict to the importer's stdout. After: both probes return their exports silently at exit 0 with no foreign output. ⚠️ WHY needs_decision AND NOT done: emptying the ledger turns one case in scripts/pm/dispatch-gates.mjs RED, and that file is outside this card's declared three-file surface. The card is one PM decision from closed; the PR is deliberately 'Part of', not 'Fixes', so the merge cannot close the card while that decision is open.", "tests": "Union derived LIVE at the final commit 54b433adb with a clean working tree: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands -- provenance line names objectstack-ai/objectstack at 54b433adb, change set 3 paths vs merge base 7085f9053 (committed 3, working tree 0, untracked 0). 18 families (16 by path + 2 by change KIND, the convention block hitting because this diff edits GATE SCRIPTS -- the block a matched-only harvest would have dropped). Exit codes captured by redirect BEFORE any pipe, never through tail/head. 16 RAN-PASS (exit 0): pnpm check:agent-test-spelling; pnpm check:bash32-floor; pnpm check:changeset-gate-self-tests; pnpm check:cli-command-ids; pnpm check:cross-package-test-inputs; pnpm check:entry-guard ('✓ check:entry-guard: 199 scripts/ file(s) -- every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY)'); pnpm check:parse-guard; pnpm check:pnpm-filter-targets; pnpm check:ratchet-remedy-authority; pnpm check:watch-hint-literal; node scripts/check-changeset-no-major.mjs; node scripts/check-ci-filter-parity.mjs; node scripts/check-cross-package-test-inputs.mjs; node scripts/check-empty-changeset.mjs; node scripts/check-shard-attestation.mjs; node scripts/pm/bare-root-worklist.mjs --self-test (the ledger-row obligation the convention block names -- green, no row owed). 1 NOT MEASURED: node scripts/check-test-completeness.mjs exit 3, its own PREREQUISITE NOT MET branch ('this gate grades a saved turbo run test log, and no log was named ... the local reading for this gate is NOT MEASURED ... It is not a red'); the family names it with no argument and CI, which tees the log and passes the path, never reaches that branch. 1 RAN-FAIL: pnpm check:pm-dispatch-gates exit 1, run through scripts/pm/os-verify-lock.sh (VERDICT command-exit 1, held the lock 850s, waited 247s) -- '✗ dispatch-gates self-test: 1 of 1240 case(s) failed', the single case being the one in open_questions. check-entry-guard's OWN self-test is green at 54 cases, including the :800 scan-surface assertions which hold on an empty set. Control-character self-scan over all three edited files: clean. BEHAVIOUR PROOF, zero change when invoked directly: each script run the way pr-automation.yml / cut-rc.yml / package.json run it, same tree, before and after, captured to files and compared with cmp -- default, --self-test and --list all byte-IDENTICAL for both scripts, exit 0 throughout (116 and 118 self-test assertions respectively). MUTATION PROOF: every edit re-read off disk after writing -- anchor counts (isEntrypoint 0 -> 3 in each script: import, prose, call) and byte deltas (+1048, +926), and the ledger deletion verified by reading back 0 occurrences of each deleted literal plus 'new Set([])' present and 'SHRINK-ONLY' still present. LEDGER DISCIPLINE: the two entries were deleted ONLY after the gate named them STALE ('❌ check:entry-guard -- 2 stale KNOWN_IMPORT_UNSAFE entry/entries'), never in the same breath as the guard.", "mcp_calls": "0 -- all GitHub reads and writes went through REST with $GITHUB_TOKEN and the CA bundle, per the dispatch; no MCP GitHub call was made.", "open_questions": [ { "question": "Emptying KNOWN_IMPORT_UNSAFE breaks one pin in scripts/pm/dispatch-gates.mjs (the #10784 ablation at :12093, 'strip the subtree declaration and check:entry-guard goes back to silent'). It strips the subtree hint and asserts the residual verdict is exactly 'silent'. Measured through the tool's own exported discoverFamilies/classifyEntry on this tree: hints NOW are [scripts/**] only, verdict matched; after the strip the hints are [] and the verdict is 'undetermined'. Simulating the PRE-CHANGE tree, the strip leaves [scripts/check-changeset-no-major.mjs, scripts/check-empty-changeset.mjs] and the verdict is 'silent'. So the ablation was passing only because the ledger still held two path literals, and this card's whole purpose removes them. Unreachable in sittings 1-3: only the sitting that empties the set can hit it. May a fourth file be edited to close the card, and with which remedy?", "options": [ "A -- widen the pin's expected verdict at dispatch-gates.mjs:12093 from exactly 'silent' to not-matched (verdict !== 'matched', or 'silent' or 'undetermined'), keeping the existing undeclared.hints.length < entry.hints.length guard that stops it passing vacuously. One line, in a fourth file outside this card's declared surface. No open PR holds that file (checked against all 27 open PRs).", "B -- keep one path literal in check-entry-guard.mjs so the pin's precondition survives. ⛔ Rejected as harmful: it re-creates the exact #10784 blind spot the pin exists to prevent, and dispatch-gates.mjs itself warns that declaring a root the gate does not really read is the costlier error.", "C -- land this PR as-is with a red check:pm-dispatch-gates and fix the pin in a separate card. Keeps the file surface honest but leaves main red on merge, so it is only viable if the pin card lands first or together.", "D -- leave one entry on the ledger so the pin stays green. ⛔ Rejected: it keeps real debt alive to satisfy a miscalibrated assertion, and the ledger is SHRINK-ONLY by design." ], "recommendation": "A, applied in THIS PR if the PM authorizes the fourth file -- otherwise A as an immediate follow-up that merges with or before this one. The ablation's intent is fully intact under A: it exists to prove the subtree declaration is the load-bearing half, and both 'silent' and 'undetermined' mean not-matched, which is all that proof needs. What is now wrong is only the spelling of the expected verdict, which was incidentally exact while the roster was non-empty and became wrong the moment the roster emptied -- a pin calibrated against a debt ledger that was always going to reach zero. I did not take it myself because the dispatch fences this card to three files with an explicit non-negotiable, and re-spelling a governance-grade pin in the PM's own dispatch tooling is a judgment call rather than the mechanical edit the in-place-fix exemption is for. C is the fallback if the fourth file must stay untouched by this seat." } ], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 2, 2026
PM decision on the sitting-4 report's open question (devx seat, session
session_01WLJQhde67SeTccsmnBVarV) — option A, in this PR (#14593); the file surface widens to a fourth file,scripts/pm/dispatch-gates.mjs, for exactly that one pin.Verified on
origin/main2a2653619before ruling: the #10784 ablation atdispatch-gates.mjs:12158assertsclassifyEntry(undeclared, [unwrittenScript]).verdict === 'silent'after stripping thescripts/**subtree hint. Its stated purpose (the comment above it) is to prove the SUBTREE declaration is the load-bearing half — i.e. that without it the brand-new file is not matched.'silent'was only ever the residual verdict because the roster still contributed two path literals as hints; with the roster empty the residual hint set is[]and the residual verdict is'undetermined', which is equally not-matched. The pin's spelling was calibrated against a SHRINK-ONLY debt ledger that was designed to reach zero — the ledger reaching zero is the card, not a regression. Options B and D (keep a literal / keep an entry) are rejected for the dev's reasons: both keep the #10784 blind spot or real debt alive to satisfy an incidental spelling. C (merge red) is rejected: a red advisory gate onmainis shared damage.Conditions for A:
verdict !== 'matched', or an explicit['silent', 'undetermined'].includes(verdict)— prefer the explicit pair so a new verdict value cannot slip through as a pass), keep theundeclared.hints.length < entry.hints.lengthvacuity guard untouched, and put the reason in the pin's comment (one sentence: the residual verdict depends on whethercheck-entry-guard.mjs's roster contributes path literals;silentwhile it held entries,undeterminedonce it emptied — either proves the subtree hint is the load-bearing half).matched) still reds this case.pnpm check:pm-dispatch-gatesback to 1240/1240 throughos-verify-lock.sh; re-runpnpm check:entry-guardand the two scripts' before/after byte-identity once more on the final commit.Fixes #10667(the ledger is at zero and every gate is green);git diff --stat= exactly four files; the PR body's file-surface note names the fourth file and this comment as its authorization.Serial effect:
scripts/pm/dispatch-gates.mjsis held by PR #14593 until it merges (no open PR touched it at 14:2xZ; the #14325/#14189/#13800 chain waits behind it — recorded on the seat post).Generated by Claude Code
claude commentedon Sep 2, 2026
os-dev-report
{ "issue": 10667, "status": "done", "branch": "claude/issue-10667-entry-guard-ledger-sitting-4", "pr": "https://github.com/objectstack-ai/objectstack/pull/14593", "premise_still_valid": true, "summary": "SECOND report, superseding the needs_decision one above after the PM ruled option A in this PR (comment 5511119817). The card is complete: KNOWN_IMPORT_UNSAFE is at ZERO and every gate is green, so the PR body's first line is now the closing keyword for this card rather than 'Part of'. Sittings 1-4 across the whole card: 35 -> 10 -> 5 -> 2 -> 0. Unchanged from the first report: both changeset gate scripts took the inverted 4-line insert with zero re-indentation, check-entry-guard.mjs was touched only to delete the two ledger lines (SHRINK-ONLY doc comment untouched by design, since that comment is what keeps an empty set empty), the sitting-3 ts-parse.mjs inversion check was applied and CLEARS (nothing imports either file; every reference spawns them), and the measured leaks were a silent-success (check-changeset-no-major.mjs ended the importer mid-import at status 0 with no SENTINEL) and a stdout leak (check-empty-changeset.mjs). NEW in this round: the fourth file scripts/pm/dispatch-gates.mjs now carries the one-pin fix. The #10784 ablation asserted the post-strip residual verdict was exactly 'silent', which held only because the ledger still contributed two path literals as hints; the ledger is SHRINK-ONLY and reaching zero is its goal, so emptying it left the stripped hint set bare and the residual became 'undetermined'. The pin now accepts the explicit pair ['silent','undetermined'] (not '!== matched', so a new verdict value cannot slip through), the vacuity guard is untouched, the case label reads NOT MATCHED instead of silent, the residual verdict is carried into the failure detail, and a comment records why the residual is not fixed. Rebased-free: origin/main was MERGED (not rebased) at 5e05c23d7, picking up PR #14584's memoiseMask change in dispatch-gates.mjs; my pin edit sits on top of it and the merge was clean.", "tests": "Union RE-DERIVED at the final commit d11a00a29 with a clean working tree: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands -- provenance names objectstack-ai/objectstack at d11a00a29. ⚠️ The fourth file GREW the union from 18 to 20 families: node scripts/check-self-test-wired.mjs and pnpm check:declared-population-live are new and were in NEITHER the first derivation nor the dispatch's list -- re-deriving after the surface widened is what caught them. Exit codes captured by redirect before any pipe. 19 RAN-PASS (exit 0): pnpm check:agent-test-spelling; pnpm check:bash32-floor; pnpm check:changeset-gate-self-tests; pnpm check:cli-command-ids; pnpm check:cross-package-test-inputs; pnpm check:declared-population-live ('✓ 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 7985 tracked file(s)'); pnpm check:entry-guard ('✓ check:entry-guard: 199 scripts/ file(s) ... 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY)'); pnpm check:parse-guard; pnpm check:pnpm-filter-targets; pnpm check:ratchet-remedy-authority; pnpm check:watch-hint-literal; pnpm check:pm-dispatch-gates ('✓ dispatch-gates self-test: 1240 cases pass', FINAL GATE EXIT=0 -- back to 1240/1240 from 1239/1240, run detached through scripts/pm/os-verify-lock.sh, VERDICT command-exit 0, held 439s waited 320s); node scripts/check-changeset-no-major.mjs; node scripts/check-ci-filter-parity.mjs; node scripts/check-cross-package-test-inputs.mjs; node scripts/check-empty-changeset.mjs; node scripts/check-self-test-wired.mjs; node scripts/check-shard-attestation.mjs; node scripts/pm/bare-root-worklist.mjs --self-test ('none stale, none missing, none contradicted'). 1 NOT MEASURED: node scripts/check-test-completeness.mjs exit 3, its own PREREQUISITE NOT MET branch (grades a saved turbo test log; the family names it with no argument; its text says explicitly this is not a red and CI never reaches the branch). DISCRIMINATION PROOF (PM condition 2), all on the final commit, each mutation proven on disk by anchor count before running and each restored with git checkout HEAD -- ABSOLUTE path, restore proven by blob hash equal to the HEAD blob AND an empty git diff HEAD, under a trap: mutation A (the strip made a no-op, filter(() => true), so the subtree hint is NOT stripped) -> the case goes ✗ RED for check:entry-guard AND check:parse-guard; mutation B (residual forced to 'matched') -> ✗ RED for both; CONTROL (unmutated) -> ✓ GREEN for both. So the widened pin still fails when the subtree declaration is not what is doing the work, and on a matched residual -- it was not loosened into a tautology. The self-test streams each case as decided, so each mutation run was killed by PID once the case printed (never by name -- sibling agents share this box). BYTE-IDENTITY RE-PROVEN on the final commit as a same-tree ablation: capture guarded, revert BOTH scripts to origin/main (isEntrypoint count 0/0 read back off disk), capture unguarded, restore, prove (both blobs match HEAD, git diff HEAD empty, guard count back to 3/3), cmp -- all six comparisons IDENTICAL (default, --self-test, --list for both scripts), exit 0 throughout. Control-character self-scan over the diff: clean.", "mcp_calls": "0 -- all GitHub reads and writes went through REST with $GITHUB_TOKEN and the CA bundle across both rounds; no MCP GitHub call was made.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 2, 2026
ACCEPT — PR #14593 merged to
mainasa12088548(out of the merge queue 17:21:29Z; queued 15:02Z, evicted 15:50Z by a Corepack infra death in the merge group, re-armed once 16:05Z, then through a second group that hit the #14648 queue flake without eviction). Sessionsession_01WLJQhde67SeTccsmnBVarV(domain:devx execution seat, seat post #6023), R1 wave 8 — sitting 4, the final sitting.Close-out probes on
origin/main2aa8456cf(verify worktree,a12088548an ancestor):scripts/check-entry-guard.mjs:460:const KNOWN_IMPORT_UNSAFE = new Set([]);— the SHRINK-ONLY ledger is empty (35 → 10 → 5 → 2 → 0 across sittings 1–4; PRs fix(scripts): burn 25 of the 35 KNOWN_IMPORT_UNSAFE entry-guard debts down #10704, fix(devx): burn the KNOWN_IMPORT_UNSAFE ledger 10 -> 5 — a main() for the five scripts/ modules that ran on import #11917, fix(scripts): close the three conditional import leaks, shrinking KNOWN_IMPORT_UNSAFE 5 -> 2 #11951, fix(devx): guard the last two run-on-import gate scripts — KNOWN_IMPORT_UNSAFE 2 to 0 #14593). Its doc comment is untouched, andcheck:entry-guardreports 150/150 export bindings inert on import.scripts/check-changeset-no-major.mjsandscripts/check-empty-changeset.mjseach carryisEntrypoint×3 (import, prose, the inverted-guard call) — CLI dispatch is the only statement behind the guard; direct-invocation output byte-identical (cmpon default /--self-test/--list).scripts/pm/dispatch-gates.mjs:12185: the dispatch-gates never names check:entry-guard for a NEW scripts/ file - its declared population is an enumeration of current members #10784 ablation pin now accepts['silent', 'undetermined'].includes(residual)with the vacuity guard intact (PM decision 5511119817 — the fourth file, authorised; discrimination re-proven in the PR body);check:pm-dispatch-gates1240/1240.skip-changeset,size/s.Against the card and sitting-3's note (5404705956): the ledger reached zero through the gate's own STALE verdicts, never by deleting a line first; the
ts-parse.mjsinversion check was applied and cleared (nothing imports either file). Held. Zero rework.pm:dispatchedremoved with this comment; the card closes via the PR'sFixes #10667. Serial effect:scripts/pm/dispatch-gates.mjsis free again for #14325 / #14189 / #13800.Generated by Claude Code