Skip to content

Burn down the 35 scripts/ modules that run on import — the KNOWN_IMPORT_UNSAFE ledger from PR #10665 #10667

Description

@claude

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

  1. self-assigned this
    on Aug 25, 2026
  2. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    Claim: PM dispatch (devx seat) — sitting 2

    • Session: session_015ahemw8RcTgqtxrj15PEZx
    • Branch: claude/issue-10667-entry-guard-burndown-2

    Re-assigning per os-zhuang's 2026-08-23 note, which cleared sitting 1's residual assignee and left the card in pm:queue for exactly this: "sitting 2 的认领席重新 assign + 声明即可".

    Scope of this sitting: the five that need a real main() extraction

    check-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.mjs and scripts/check-empty-changeset.mjs sit 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.mjs additionally sits at lint.yml:189, and lint.yml is 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 are fix(pm)/skills(pm)); if any of them turns out to hold scripts/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

  3. claude commented on Aug 25, 2026

    @claude
    ContributorAuthor
    {
      "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

  4. removed their assignment
    on Aug 25, 2026
  5. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    Sitting 2 landed — KNOWN_IMPORT_UNSAFE 10 → 5. Verified by content on origin/main, landing commit located by (#11917) in its subject:

    200fc8244  fix(devx): burn the KNOWN_IMPORT_UNSAFE ledger 10 → 5 (#11917)
    

    ⚠️ pm:dispatched stripped by hand and the assignee cleared — this is a Part of card 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

    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 and process.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-guard is unreachable-by-construction for an import-edge diff — on sitting 2's diff dispatch-gates.mjs matched it directly via the scripts/** gate source.


    Generated by Claude Code

  6. self-assigned this
    on Aug 25, 2026
  7. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    Claim: PM dispatch (devx seat) — sitting 3

    • Session: session_015ahemw8RcTgqtxrj15PEZx
    • Branch: claude/issue-10667-entry-guard-burndown-3

    Sitting 2 landed as PR #11917 (200fc8244), ledger 10 → 5, and I stripped pm:dispatched by hand at 01:5xZ — this is a Part of card, so the merge neither closes it nor clears the label. (⚠️ 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.)

    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.mjs and scripts/check-empty-changeset.mjs sit 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 under scripts/.

    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.mjs re-exec'd and process.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.mjs is live at lint.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-paths declares 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

  8. claude commented on Aug 25, 2026

    @claude
    ContributorAuthor
    {
      "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

  9. removed their assignment
    on Aug 25, 2026
  10. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    Sitting 3 landed — KNOWN_IMPORT_UNSAFE 5 → 2. Verified by content on origin/main, landing commit located by (#11951) in its subject:

    8e39bbc32  fix(scripts): close the three conditional leakers … 5 → 2 (#11951)
    

    pm:dispatched stripped and the assignee cleared by hand — this is a Part of card, so the merge neither closes it nor clears the label. (⚠️ I missed that step twice earlier tonight on this card and on #9788; it is now the second item of my standing check-in, and this time it was done at the landing.)

    ⛔ State is pm:blocked, NOT pm:queue — there is no sitting 4 to take

    Blocked-by: #9465
    Restart-when: #9465 lands, or its epic seat hands the two files over
    Unlock-action: burn down the final 2 entries
    

    The ledger's entire remaining content is the pair fenced by the changesets-v3 epic:

    • scripts/check-changeset-no-major.mjs
    • scripts/check-empty-changeset.mjs

    Both sit inside #9465's declared file territory and #9465 is still open. Returning this card to pm:queue would 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 PR ledger
    1 #10704 35 → 10
    2 #11917 10 → 5
    3 #11951 5 → 2

    ⭐ Sitting 3's finding is the one worth carrying forward: ts-parse.mjs did not take a guard. It is a library imported by eleven gates that 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 — 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 --help builds its usage text from all 79 column-0 // lines, so any later comment silently rewrites the CLI's help).


    Generated by Claude Code

  11. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Unlock scan (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): the recorded blocker #9465 is CLOSED (completed). Card face on origin/main (a39b02a6): scripts/check-entry-guard.mjs still carries the KNOWN_IMPORT_UNSAFE set (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. ⚠️ 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.

    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

  12. added theissue type on Sep 2, 2026
  13. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    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).

    • Branch: claude/issue-10667-entry-guard-ledger-sitting-4
    • Worktree: ../objectstack-10667 (dedicated per-task worktree, off origin/main)
    • Domain: domain:devx per the unlock scan (comment 5503053245): scripts/ gate debt; check:entry-guard is the arbiter.
    • File surface: scripts/check-changeset-no-major.mjs, scripts/check-empty-changeset.mjs (guard their run-on-import top-level statements behind isEntrypoint(import.meta.url), zero behaviour change when invoked directly), and scripts/check-entry-guard.mjs ONLY to remove the two now-stale KNOWN_IMPORT_UNSAFE entries (shrink-only ledger; the gate fails STALE until they go). ⛔ No other script, no .changeset/** content, no .github/**. Sitting-3's ts-parse.mjs lesson 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.
    • Premise re-verified on origin/main 7085f9053: scripts/check-entry-guard.mjs:460-463 KNOWN_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 — not Part of — if the ledger reaches zero and check:entry-guard is green).
    • Container & model: subagent lane (S/M mechanical with a known inversion trap), opus.
    • Clause ②: not reached — import-safety of two gate scripts; their CLI behaviour is unchanged.
    • Serial constraints: none. Changeset: scripts/**-only ⇒ skip-changeset label on the PR.

    Generated by Claude Code

  14. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    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

  15. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    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/main 2a2653619 before ruling: the #10784 ablation at dispatch-gates.mjs:12158 asserts classifyEntry(undeclared, [unwrittenScript]).verdict === 'silent' after stripping the scripts/** 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 on main is shared damage.

    Conditions for A:

    • Widen the expectation to not-matched (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 the undeclared.hints.length < entry.hints.length vacuity guard untouched, and put the reason in the pin's comment (one sentence: the residual verdict depends on whether check-entry-guard.mjs's roster contributes path literals; silent while it held entries, undetermined once it emptied — either proves the subtree hint is the load-bearing half).
    • Keep the pin discriminating: show in the PR body a one-off mutation where the subtree hint is NOT stripped (or the verdict is forced to matched) still reds this case.
    • pnpm check:pm-dispatch-gates back to 1240/1240 through os-verify-lock.sh; re-run pnpm check:entry-guard and the two scripts' before/after byte-identity once more on the final commit.
    • PR body first line becomes 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.mjs is 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

  16. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    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

  17. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    ACCEPT — PR #14593 merged to main as a12088548 (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). Session session_01WLJQhde67SeTccsmnBVarV (domain:devx execution seat, seat post #6023), R1 wave 8 — sitting 4, the final sitting.

    Close-out probes on origin/main 2aa8456cf (verify worktree, a12088548 an ancestor):

    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.mjs inversion check was applied and cleared (nothing imports either file). Held. Zero rework.

    pm:dispatched removed with this comment; the card closes via the PR's Fixes #10667. Serial effect: scripts/pm/dispatch-gates.mjs is free again for #14325 / #14189 / #13800.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions