fix(cli): one space after the generator-load separator, on both faces - #16873
Conversation
`os lint --eval --generator ""` refused with `Failed to load generator "": is
not a valid JS file` — two spaces. `bundle-require` composes its own message as
`${filepath} is not a valid JS file`, so an empty filepath contributes nothing
and the fragment's leading space lands against the space in our own `": "`
separator. Neither side is wrong alone, and the seam was unreachable before the
truthiness-guard repair: that guard skipped the whole load block.
The composed message now drops leading spaces from the detail, so the separator
carries exactly one. This reads the seam and never `flags.generator`, so the
empty string still answers through the door an unresolvable path already
answers through, and every detail that does not open with a space is
byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3eaa82dc3b5378216268e13760d8c42aaad3003d && git checkout 3eaa82dc3b5378216268e13760d8c42aaad3003d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b37f0b1767a129b5cf50ddd2102a8027039f5abe eb3336e2a105301159471c68724a8969a68df98d && git checkout -B drift-repro b37f0b1767a129b5cf50ddd2102a8027039f5abe && git merge --no-ff eb3336e2a105301159471c68724a8969a68df98d
node scripts/docs-audit/affected-docs.mjs --json b37f0b1767a129b5cf50ddd2102a8027039f5abe
|
Fixes #16359
Clause-②: no
os lint --eval --generator ""refused with two spaces after our own separator:bundle-requirecomposes its own refusal asFILEPATH + ' is not a valid JS file'. An emptyfilepath contributes no characters, so that fragment arrives with a leading space and lands
against the space in our own
": "separator. Neither side is wrong alone, and the seam wasunreachable before PR #16341 — the truthiness guard skipped the whole load block, so nothing was
printed at all. A defect newly made reachable by a correct fix, not a regression it
introduced, and not a reason to touch that guard.
The decision this card exists to make — Route A, and why
Triage's binding criterion: 哪一种修法之后,空串与不可解析路径仍然经由同一扇门作答?
Route A — trim our own separator — is what landed. The composed message drops leading spaces
from the detail, so the separator carries exactly one:
That is the whole source change: one line becomes two. It reads the seam and never
flags.generator, which is the property the criterion asks about. After it, the empty stringand an unresolvable path still reach the same
try, the samecatch, the same compositionexpression, the same envelope, the same exit code and the same key set — the empty string stays
"a path that names no module", exactly as #16161 ruled. Measured, not asserted: see same door
below.
Route B — special-case the empty value — was rejected. It re-introduces the branch #16161
deliberately refused: a hand-written message for the empty value is the second refusal shape that
card exists to avoid. A Route B repair would satisfy every spacing assertion in the new pin and
still fail its
the same doorcase, which is why that case is in the file.Leading spaces only, deliberately not
trimStart(): a detail that opens with a newline is adifferent shape (our space, then a line break), not a doubled separator, and stays exactly as it
prints today.
验收 — evidence against each item
1. Re-driven by BYTES at the branch point, both faces
Not inherited from the card. Driven at
923caede80throughbin/run-dev.jswithNO_COLOR=1,read through
od -candcat -A:":"--generator ""--json --generator ""--generator UNRESOLVABLE_PATH--json --generator UNRESOLVABLE_PATH⇒ the card's transcription holds, and the defect is on both faces, which was a measurement
rather than an assumption: PR #16855 changed the
--jsonpath only, and the two faces do printthe same composed message. After the repair, exactly one space on both, and the stdout byte counts
drop by exactly one each (59→58, 67→66).
2. The exact bytes are pinned, not a satisfiable substring
packages/cli/test/lint-eval-generator-refusal-separator.test.tsasserts the whole composedmessage by equality on both faces:
A double space fails both lines. Ablation: reverting only
commands/lint.tsto its pre-fixcontent and re-running the same command gives
Tests 3 failed | 2 passed (5)— same file, samefive tests, three of them red. The two that stay green are the negative controls, which is the
predicted direction. Restoration proven by blob hash (
ab85602c…before, mutated94079798…,restored
ab85602c…), emptygit diff HEADand cleangit status --porcelain.3. Tier — this pin is queue tier, on purpose
The trap reproduced exactly as described. Naming the existing nightly file to
vitest run --project integrationwithoutOS_TEST_TIERS=nightly:— the same exit code a failing suite gives. Guarded against here by asserting the count, not
the colour: green run
Test Files 1 passed (1) · Tests 5 passed (5); ablated runTest Files 1 failed (1) · Tests 3 failed | 2 passed (5). Five collected in both.The new pin avoids the trap rather than working around it. The nightly cut is by filename
(
scripts/nightly-tiers.mjs) and theunit/integrationcut is by behaviour(
packages/cli/vitest-tiers.ts), so a spawning file that carries no.e2ename is collected onevery PR and every merge-queue entry. Derived from the two classifiers on this tree:
Why not the unit tier, as the dispatch asked to be told: a unit-tier pin cannot spawn, so it
would have to test a pure helper extracted out of
runEval— a new exported surface created tohost a whitespace fix, asserting a function's return value instead of the bytes an operator sees.
The queue-tier file buys the same per-PR protection while pinning the real emitted bytes on the
real command, so the extraction buys nothing here. There are 14 precedent queue-tier spawning
files in
packages/cli/test/.4. Negative control — the non-empty unresolvable path, byte for byte
All four streams compared before/after with
cmp:Widened past the card's ask to every reachable load-failure class, driven through the same
entry on the pre-fix and post-fix source:
BuildFailure(unresolvable path)FORBIDDEN/ 403)ENOENT)None of them opens its detail with a space, so
replace(/^ +/, '')is a no-op on all four. Theempty value is the only shape whose bytes move.
Same door, pinned: the third describe block asserts the empty string and the unresolvable path
answer with the same exit code, the same
Object.keys(payload)(['error']) and the sameFailed to load generator "…":+ non-space shape. That is the assertion Route B would fail.5. Existing pins untouched
git diff --statfortest/lint-eval-generator-load-envelope.e2e.test.ts: no entry — the newpin is a new file beside it, and none of that file's four
stderr).toBe('')assertions isweakened, rewritten or moved. Re-driven anyway, because the repair touches the string the
--jsonface emits:
OS_TEST_TIERS=nightly … --project integrationon that file givesTest Files 1 passed (1) · Tests 9 passed (9).6. Gates
content/docs/releases/untouched; no test skipped, disabled or quarantined.node scripts/pm/dispatch-gates.mjs --ran … --repo objectstack-ai/objectstackreports✓ 58 derived famil(ies) accounted for. Four first readPREREQUISITE NOT MET(exit 3/1, nodist/):check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:i18n-walk-parity. That is NOT MEASURED, not red — cleared by building the CLI, then allfour exit 0.
pnpm --filter @objectstack/cli typecheck→ exit 0 (tsc --noEmit+check:test-typecheck;the shrink-only test-typecheck debt is unchanged at 3 files / 28 errors, so the new file adds
none).
pnpm --filter @objectstack/cli exec vitest run --project unit→Test Files 186 passed (186) · Tests 2555 passed | 6 expected fail (2561).pnpm exec eslint . --no-inline-config→ exit 0 over the whole repo, 6367 files, 0 errors,0 warnings, counted from
--format json. No narrowing was needed, so none is claimed.check:nul-bytesgreen, plus a directgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over all three changed files — no matches.All test and gate figures above were taken at
eb3336e2a1, the final commit on this branch.Changeset
patchfor@objectstack/cli— the message is composed insrc/commands/lint.ts, which ships inthat package's
dist. Not breaking, so no ADR-0087 disposition marker.验收备注
Noted, not filed:
": " + upstream-fragmentseam exists in shape at the other threebundleRequirecallers in this package (
utils/config.ts,utils/scaffold-validate.ts,commands/serve.ts),but none of them can be reached with an empty filepath — the paths they pass are resolved config
or scaffold paths, never raw flag text — so there is no reproducible defect to file. Scope here
is this one call site.
os lint --eval --generatorscores a generator returning{ objects: [] }at 100/100, grade A,exit 0. Met while building fixtures; already documented as the consequence of a pinned property
at
packages/cli/src/lint/metadata-eval.ts, ⛔ not a defect and not re-filed.Docs drift advisory — the reading
⛔ A READ, not a code change.
content/docs/releases/is untouched by this PR and stays untouched.1. Re-derived on the tree the bot named, from a confirmed-clean worktree
The advisory's own run declared a provenance defect ("That checkout carried uncommitted changes,
so the commit above does not fully identify what was read."), so its list was treated as a prompt,
not as the reading. Re-derived in a separate worktree at
eb3336e2a1withorigin/mainmerged in,git status --porcelainempty before the run:Same two parents as the bot's merge, so the same tree; different merge sha only because it is a
different merge commit.
dirty: false— the thing the bot's run could not say.Real count: 24 docs — 20 hand-written + 4 release-owned. The bot's numbers reproduce exactly;
there is no under-report on this PR (the sibling's 28-vs-33 gap does not recur here). Two anchors,
runEval(symbol) andos lint(command);anchorlessChangesempty; 1 changed package.2. The four release-owned pages — read, and nothing is falsified
releases/v13.mdx,v15.mdx,v16.mdx,v17.mdx. No fact in any of them is falsified by thisdiff, and there is nothing to hand back for a docs-only PR or an issue. They were listed on the
os lintcommand anchor alone:v15namesos lintzero times;v13twice (thesecurity-role-wordrule, and errors gatingos compile);v16once (built-in i18n gaps);v17eight times (react pages, functional completeness, an unparseable source, hook bodies, label
warnings).
runEvalappears in none of the four;--eval,--generatorand the refusal stringappear in none of the four. This is the over-inclusion the tool's own header describes — a page
that names the command need not document the changed surface.
3. Token sweep over all of
content/— the concrete falsifier is absentcontent/Failed to load generatoris not a valid JS file--generator--evalos lintThe two-space spelling searched for on its own, as asked, since a single-space search would
miss it:
": is not a valid JS file"→ 0;": is not"→ 0. The one-space form": is not a valid JS file"→ 0 as well.⭐ Zeros paired with controls sharing the failing queries' vocabulary, because a zero from one
phrasing is not a zero from the area:
generator→ 12 hits,not a valid→ 5,os lint --→ 7,lint --→ 8. The sweep reachescontent/, so the zeros are readingsand not silence. Every one of the 12
generatorhits is something else (an SBOM generator, agrant generator, a REST route generator, a QR field, an async generator); none of the 7
os lint --hits documents--evalor--generator.Repo-wide,
is not a valid JS fileandFailed to load generatoroccur only inpackages/cli/src/commands/lint.ts, this PR's new pin, this PR's changeset, and one pre-existingnegative assertion in
test/lint-generator-requires-eval.e2e.test.ts(not.toContain('Failed to load generator'), unaffected by spacing — re-driven underOS_TEST_TIERS=nightlyanyway:Test Files 1 passed (1) · Tests 7 passed (7)).⇒ No page reproduces the old two-space spelling as expected output. Nothing in
content/isfalsified by this change.
4. The advisory's own blind spot, answered by hand
The advisory cannot list a page that states a rule by its inputs, because such a page shares no
identifier with the emitter that implements it — and this diff is emitter-side, so the anchor
list structurally could not cover it. The token sweep in (3) is what covers it: the emitted string
itself, in every spelling this change could have moved, is absent from
content/entirely.content/docs/deployment/cli.mdxlists the
os lintflags as--score,--fix,--strict,--jsonand documents neither--evalnor
--generator. That is an undocumented capability, not an over-claimed one — the oppositedirection from Prime Directive #10 — and it predates this diff, which neither widens nor narrows
what those flags accept. Successor: whoever next edits that page's
os lintsection.Generated by Claude Code