Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): close the three conditional import leaks, shrinking KNOWN_IMPORT_UNSAFE 5 -> 2 - #11951
Conversation
`qa-rollup.mjs` exports bindings and tested `--self-test` BEFORE the entry guard, so the branch read the IMPORTER's argv: any tool that imported this module for its exports while carrying `--self-test` in its own argv ran qa-rollup's entire self-test inside itself. The leak is invisible to the two signals a caller usually has. The self-test does not exit on success, so the importer survives with status 0 and finishes its own work; the only trace is 82 bytes of foreign output on the importer's stdout. Measured with a probe that prints a sentinel AFTER the dynamic import and counts bytes that are not the sentinel: before LEAK scripts/qa/qa-rollup.mjs argv --self-test FOREIGN-BYTES=82 after CLEAN scripts/qa/qa-rollup.mjs argv --self-test Guard first, mode second. CLI behaviour is unchanged — `--self-test` output is byte-identical before and after (85 bytes, `cmp` clean), because the guard is true for every direct invocation. `check:entry-guard` names the file STALE once it is inert, so its KNOWN_IMPORT_UNSAFE line goes in this same commit: the ledger shrinks 5 -> 4 and the gate's own count moves to `108 of them inert on import (4 known-unsafe)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
`objectui-range.mjs` exports bindings and answered `-h`/`--help` from a bare top-level `if`, so the test read the IMPORTER's argv. An importer carrying either flag got this file's 4666-byte `//` header written to its stdout and then `process.exit(0)`. That is the worst-reading shape in this class: the importer's process ends mid-import with a SUCCESS status, so a caller holding only `result.status` cannot tell it from a clean import. Measured with a probe that prints a sentinel AFTER the dynamic import — the sentinel is what makes the two distinguishable, exit status alone is not: before LEAK argv --help NO-SENTINEL(status=0) FOREIGN-BYTES=4666 before LEAK argv -h NO-SENTINEL(status=0) FOREIGN-BYTES=4666 after CLEAN argv --help after CLEAN argv -h The help text is unchanged, and deliberately so: it is read back out of this file's own `//` lines, so a comment added at column 0 would rewrite it. The new rationale is a `/** */` block for that reason and the column-0 `//` count is still 79. All four CLI surfaces are byte-identical before and after — `--help` 4693 bytes, `-h` 4693, no-args 93 bytes on stderr with status 1, and `--self-test` 1578 bytes, every one `cmp`-clean on both streams. `check:entry-guard` names the file STALE once it is inert, so its KNOWN_IMPORT_UNSAFE line goes in this same commit: the ledger shrinks 4 -> 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
`ts-parse.mjs` registered its `OS_TOOLING_PARSE_CENSUS` exit report from a top-level `if (process.env...)`. This module is a LIBRARY — eleven gates in `scripts/` import it for its exports — so with the env set, that registration ran inside every one of those importers and wrote a line to a process whose only involvement was having loaded it. The entry-point guard is NOT the fix here, and that is the interesting half. As an entrypoint this module parses nothing, so `if (isEntrypoint(...))` would arm the census on the one run that has nothing to count and leave it silent on every run that does — import-safe and permanently empty. The condition had to MOVE, not acquire a guard: it is now armed, once, by the first parse through any of the three doors. One measurable consequence, stated rather than left to be discovered: a process that imports this module and never parses now prints nothing where it used to print `0 parse(s)`. Nothing read that line — `OS_TOOLING_PARSE_CENSUS` appears in no other file in the tree — and a census whose numerator is zero is the case with nothing to report. The self-test pins BOTH directions, so neither the leak nor the over-correction returns unnoticed: a child that parses still gets `[ts-parse census] 1 parse(s)` on stderr, and a child that only imports gets no census line. The child harness grew an optional env argument for it. Measured with a sentinel-after-import probe: before LEAK scripts/ts-parse.mjs env CENSUS=1 FOREIGN-BYTES=102 after CLEAN scripts/ts-parse.mjs env CENSUS=1 CLI: `node scripts/ts-parse.mjs` is byte-identical (45 bytes). `--self-test` is NOT, by design — it reports `30 cases pass` where it reported `28`, and the case count is the only difference in the line. `check:entry-guard` names the file STALE once it is inert, so its KNOWN_IMPORT_UNSAFE line goes in this same commit: the ledger shrinks 3 -> 2 and now holds only the two files fenced by the changesets-v3 epic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 25, 2026
ACCEPT, and option A on the open question — confirmed on my own measurement, not on yours. Flipping out of draft ( Verified against the branch
⭐ The ruling rests on a fact I re-measured, because it is the whole argumentOption A drops the One file, itself. So the reading you removed had no consumer, and A costs nothing real. B would reinstate the exact defect this card exists to remove; C buys back an unconsumed reading at eleven call sites that can rot individually. Shipping A and flagging it was right, and the revert path being one commit is what makes flagging it sufficient rather than requiring a pre-ruling. ⭐ The |
Uh oh!
There was an error while loading. Please reload this page.
…refix --help built its usage text by keeping every column-0 `//` line in the whole file, not just the header. Six mid-file implementation comments (the pinAt() helper's rationale, and this file's own self-test section banner) leaked into the usage text a reader pastes. The header IS a contiguous prefix of the file (right after the shebang), so printHelp() now walks lines in order and stops at the first non-`//` line once the block has started -- a takeWhile, not a filter -- so a column-0 comment added anywhere later in the file can never reach --help again. Pinned as a self-test assertion (the file's own #4843 idiom): --help still carries the real header, and none of the six measured stray lines. Reverse-verified: mutating printHelp() back to the old unbounded filter flips the new pin red; rebuilding the fix restores green. column-0 `//` count is unchanged at 79 -- the new rationale uses a `/** */` block, per PR #11951's byte-identity concern for this file.
Part of #10667 — sitting 3 of the
KNOWN_IMPORT_UNSAFEburn-down.Sittings 1 and 2 burned files that leaked unconditionally, so a clean-argv
import probe saw them. These three leak on the importer's own argv or env,
which the earlier probe is structurally blind to. So the probe was the work
here, not the guard insert.
The probe, and why exit status alone is not enough
Sitting 2's exhibit was a gate that re-exec'd and
process.exit()'d on import,returning exit 0 with no sentinel — a silent success indistinguishable from
a clean import. This probe therefore reads three signals per run:
ended or replaced the importer, even at status 0;
All three were load-bearing: one target needs signal 2, one needs signal 3, and
neither alone covers both. Every target is probed twice, once with clean
argv/env and once with the trigger planted.
Before — the probe seeing the leaks
The four
cleanrows are the point: with clean argv/env all three targetsread CLEAN, which is exactly why the earlier sitting's probe reported nothing
here.
After
The two remaining LEAKs are the two fenced files, untouched on purpose (see
below). They are the positive control: they hold the LEAK reading open in the
same run that reports the six flips, so no zero here is vacuous.
scripts/invoked-as.mjsis the matching control for the CLEAN reading.The three mechanisms, each verified rather than inherited
scripts/qa/qa-rollup.mjs--self-testscripts/objectui-range.mjs-h/--helpprocess.exit(0)— importer ends mid-import carrying successscripts/ts-parse.mjsOS_TOOLING_PARSE_CENSUSAll three match what the card described. Nothing was inherited.
ts-parse.mjs: the guard is the wrong fix, and that is the interesting halfts-parse.mjsis a library — eleven gates inscripts/import it for itsexports — and as an entrypoint it parses nothing at all. Wrapping the census in
if (isEntrypoint(...))would arm it on the one run that has nothing to countand leave it silent on every run that does: import-safe and permanently empty.
So the condition moved instead: the report is armed, once, by the first
parse through any of the three doors. The trigger becomes "this module was
USED" rather than "this module was LOADED", which is the event the number was
always about.
One measurable behaviour change, stated rather than left to be discovered:
a process that imports this module and never parses now prints nothing where it
printed
0 parse(s).OS_TOOLING_PARSE_CENSUSappears in no other file inthe tree, so nothing read that line, and a census whose numerator is zero is
the case with nothing to report.
The self-test pins both directions so neither the leak nor the
over-correction returns unnoticed, and both pins were ablated:
1 of 30 case(s) failed—…a run that only IMPORTED this module writes no census line at all, detail carrying the leaked[ts-parse census] 0 parse(s)…1 of 30 case(s) failed—with the census env set, a run that PARSED still reports at exitBoth legs ran with a
trap … EXIT INT TERMrestore and anchored on-disk countsbefore and after. The first attempt at leg A was a no-op and is reported as
one: it reinstated the block with an
[ablation]marker while the assertionkeys on the literal
[ts-parse census], so it exercised nothing and read green.Re-run with the exact original bytes, it reds. No build is involved anywhere —
node executes these
.mjssources directly, there is nodist/for a mutationto fail to reach.
Behaviour preservation
These are behaviour-preserving edits for the first two files, so identical CLI
output is the proof —
cmp, both streams:qa-rollup.mjs --self-testobjectui-range.mjs --helpobjectui-range.mjs -hobjectui-range.mjs(no args, status 1)objectui-range.mjs --self-testts-parse.mjs(no args)ts-parse.mjs --self-test30 cases passwhere it read28. The case count is the only difference in the line.One trap worth recording:
objectui-range.mjs's help text is read back out ofits own column-0
//lines (all 79 of them, not just the header), so any//comment added at column 0 would silently rewrite--help. The newrationale is a
/** */block for that reason, and the column-0//count isstill 79.
The ledger shrank 5 → 2, one line per commit
check:entry-guardnamed each file STALE before its line was deleted, andeach fix carries its own ledger line in the same commit so a revert stays
self-contained. The gate's own count moved
107 of them inert on import (5 known-unsafe)→110 of them inert on import (2 known-unsafe).The remaining two —
scripts/check-changeset-no-major.mjsandscripts/check-empty-changeset.mjs— are fenced: both sit in the declaredfile territory of the changesets-v3 epic (#9465), which is open. Not touched.
Verified against that epic's territory list before any edit.
Gates
Derived live with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(4 paths, three-dot against merge basece744bcdf),then all nine families run. Union run at
e253ab930, the final commit:Also run, though not in the derived family:
pnpm lint(repo-wide,eslint . --no-inline-config) exit 0 in 53s, andcheck-nul-bytes—OK (scanned 6654 text file(s) … no raw ASCII control bytes).skip-changesetapplied: this PR publishes nothing. It changes only theimport-time behaviour of three
scripts/tools and shrinks a gate's internaldebt ledger; no package's behaviour or public surface moves.
Generated by Claude Code
Generated by Claude Code