') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(cli): name the missing build output instead of oclif's "command not found" by os-litant · Pull Request #13064 · objectstack-ai/objectstack · GitHub
Skip to content

fix(cli): name the missing build output instead of oclif's "command not found" - #13064

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-12964-run-dev-spec-dist-preflight
Aug 29, 2026
Merged

fix(cli): name the missing build output instead of oclif's "command not found"#13064
os-litant merged 3 commits into
mainfrom
claude/issue-12964-run-dev-spec-dist-preflight

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#12964

pnpm i18n:extract in an unbuilt worktree reported an oclif "command not found" for a command that is right there on disk. This teaches packages/cli/bin/run-dev.js to name the real cause — a workspace package with no build output — and the one command that fixes it.

1. Reproduced first, on the card's own terms

Fresh worktree at 8cb96ec41, pnpm install done, nothing built:

$ pnpm i18n:extract
… 58 ModuleLoadError warnings …
Error: command i18n:extract:packages/platform-objects/scripts/i18n-extract.config.ts not found
at Config.runCommand (…/@oclif/core/lib/config/config.js:392:19)
$ echo $?
2

1518 lines of report; the last line — the only one a reader acts on — names the one cause that is definitely not true. packages/cli/src/commands/i18n/extract.ts exists. What actually happened is in the noise above it, 58 times over:

Cannot find module '…/packages/cli/node_modules/@objectstack/spec/dist/index.mjs'

oclif's findCommandimport()s every command module while it builds its manifest; a command whose module will not load is indistinguishable, to Config.runCommand, from one that does not exist.

The card's premise held in every particular. All three named paths still exist on main, and the run-dev.js shim still lacks the guard.

2. Which landing site — measured, not chosen by taste

The card left run-dev.jsorinvocationFailureLine open. Four measurements settle it, and they all point the same way.

bin/run-dev.jsinvocationFailureLine (src/utils/invocation.ts)
Does the error even reach it?yes — it is the .catch()no. Probed the real CLIError in the unbuilt tree: own properties ['code','oclif','skipOclifErrorHandling','suggestions'], neither parse nor showHelp, so isInvocationError is false and invocationFailureLine returns undefined today
Published?no — files is ["dist","README.md","CHANGELOG.md"] and npm packs only the bin target, bin/run.jsyes, as dist/utils/invocation.js. A monorepo remedy (--filter=@objectstack/spec) would ship to an end user who mistyped a command
Reachable on the tree this card is about?yesno.bin/run.js reaches it via await import('../dist/utils/invocation.js') inside a try/catch — on an unbuilt tree that import itself fails and is swallowed, so a preflight there is dead in exactly the case it exists for
Contract costnone; it is a shimits own header pins "why this file imports nothing but node: builtins", and its docstring pins "a genuine runtime failure keeps oclif's reporting exactly as it was". A not-found preflight needs both widened

So: bin/run-dev.js, with the decision factored into a module a test can import.

3. No new verdict text, no new classifier

Both facts are decided by scripts/cli-build-prerequisite.mjs — the module #5217 and #7681 put this knowledge in — and the remedy is that module's own workspaceBuildFix:

  • looksLikeMissingCliCommand — is this oclif's "command … not found"? Written to survive oclif's mid-token hard wrapping, which a per-line regex does not.
  • looksLikeStaleWorkspaceDist — did a package this repo builds cause the load failure? Deliberately narrow, so a third party's Cannot find module prints nothing.

scripts/check-dev-prereqs.mjs, the gate that owns the fuller verdict, is not reachable from here and was not made reachable: it has no exports and calls process.exit(report(inspect(ROOT))) at module scope, so importing it would terminate the CLI. Spawning it was prototyped and dropped — it answers about the whole workspace ("67 of 67 packages … pnpm build", measured) where the failure in hand names one package and one build, and two remedies for one precondition is the shape that gate's own #5726 header exists to prevent.

Output:

objectstack: NOT A MISSING COMMAND — @oclif/core reports a command module that failed to LOAD as "not found", and one did: Cannot find module '…/@objectstack/spec/dist/index.mjs'. The unmet precondition is @objectstack/spec's build output, not the invocation.
objectstack: Fix: pnpm exec turbo run build --filter=@objectstack/spec

4. The listener order is load-bearing, and the first attempt got it wrong

The cause has to be collected as it happens: oclif warns per failing command module during Config.load() and then throws a "not found" that carries none of it. The obvious spelling — attach process.on('warning', …) at the top of the shim — silently deletes output:

@oclif/core installs its own warning listener (displayWarnings() in config/config.js, what prints the Warning: ModuleLoadError stack plus detail under settings.debug) only when process.listenerCount('warning') <= 1. A collector attached first makes that 2 and oclif declines. Measured on the repro: the report went from 1518 lines to 476, with nothing saying why.

Attaching afterrun() fixes it — run() reaches displayWarnings() in its synchronous prefix (main.js: await Config.load(…) is its first await; config.js: the call precedes load()'s first await), and process.emitWarning defers to nextTick. The e2e asserts oclif's at Plugin.warn blocks are still present, so a future oclif that moves that call past an await fails a test instead of going quiet.

5. Before / after — the failure path gains exactly two lines, the success path is byte-identical

Failure path, re-measured at this branch's head in a fresh worktree with pnpm install and nothing built:

before 1518 lines, exit 2
after 1520 lines, exit 2

Sorted-multiset diff of the two runs (pid and run-dev.js:LINE:COL normalised): the only content difference is the two objectstack: lines. Control for run-to-run noise, same tree: two runs of identical code differ by 0 lines; the residual at async Promise.all (index N) frames that move are oclif's own settle order over its command-module Promise.all, same count, no line added or lost.

Success path, on a built tree, same command:

$ diff success-before.txt success-after.txt
29c29
< ✓ Generated 11 file(s) (563ms)
> ✓ Generated 11 file(s) (534ms)

That is the elapsed-milliseconds figure the command prints itself. With that one number normalised the two transcripts are byte-identical (sha256 205d0427… both), both exit 0, both leave git status clean.

6. The test proves it entered the unbuilt branch

The trap this suite is written against: an "unbuilt tree" test that runs in CI's built tree never enters the branch it claims to cover and reads green forever. So the unbuilt condition is manufactured for one child process — test/fixtures/unbuilt-spec-dist.hook.mjs, a --import resolve hook that touches no disk (this container runs several agents; a test that renamed packages/spec/dist would break every other run in the box).

It re-points the specifier rather than throwing, so the sentence the classifier reads is node's own. Two shapes were measured and rejected first: a short-circuited non-existent URL skips finalizeResolution and surfaces as ENOENT … open, which the classifier correctly declines (a green run proving nothing); a hand-thrown error would make the test assert a string the fixture authored.

Three cases, a positive-control set rather than one assertion:

  1. hook on, real command id → both lines appear, naming @objectstack/spec, alongside oclif's unchanged Error: command … not found, exit 2;
  2. hook off, the same command id → the command module loads and the run reaches its own argument handling (Config file not found), exit 1 — the other branch, on the same probe;
  3. hook off, a command that really is missing → oclif's "not found" stands with nothing added, exit 2.

Ablation (bin/run-dev.js reverted to the merge base; on-disk proof first — announceUnbuiltWorkspace 2→0, moduleLoadFailures 3→0, blob 033f8998… = the base blob):

ablated Tests 1 failed | 11 passed (12)
FAIL names the real cause and the one command that fixes it
restored Tests 12 passed (12)

Restored under an EXIT INT TERM trap and proved: on-disk hash 548f923e… = HEAD:packages/cli/bin/run-dev.js, git diff HEAD empty, git status clean.

7. Why the renderer lives in scripts/, and what that bought

run-dev.js ends in a top-level await run(…), so it cannot be imported by a test. The decision is therefore its own module — and it is under scripts/ rather than packages/cli/bin/ for a measured reason:

  • a .ts test importing an untyped .mjs is TS7016 (measured on a minimal probe, then two-sidedly in this tree). That lands in @objectstack/cli's ledgered hidden test layer, whose note says "the first new error in it should go red rather than be absorbed".
  • the repo's answer to that is a hand-written .d.mts (three existing precedents), and check:declaration-mirrors only discovers scripts/**/*.d.mts — a declaration under packages/cli/bin/ would be exactly the unwatched drift that gate was built to prevent ([finding] scripts/js-comment-mask.d.mts is a hand-maintained type mirror with nothing gating it in step — if it drifts, every consumer type-checks clean against a wrong signature #10549). It now reports 4 pairs, this one included.
  • that gate import()s the module with bare node, so the module may not import a .ts. The CLI's name therefore arrives as a parameter; the shim already imports invocation.ts on its failure path and owns that coupling, so there is still exactly one spelling of the name.

Two-sided control that the declaration is what keeps the layer clean — one throwaway file, both imports, same program:

__tsprobe-control.ts(2,35): error TS7016: Could not find a declaration file for module
'../../../scripts/cli-build-prerequisite.mjs' ← the undeclared sibling
← line 1, the declared module: no error

⇒ the two new test files add 0 errors to that layer.

8. Gates

Derived after the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-written path list), re-derived twice as the diff grew. All runs below at 576a47c79, the pushed head.

Green: check:nul-bytes · check:cli-command-ids · check:dev-prereqs --self-test · check:declaration-mirrors (self-test + scan) · check:cross-package-test-inputs (+ the ci.yml invocation) · check:cli-test-child-env · check:type-check-coverage · check:turbo-task-graph · check:query-options-erasure · check:engine-double-contract · check:where-matcher · check:published-files · check:test-source-alias · check:type-source-resolution · check:entry-guard · check:parse-guard · check:watch-hint-literal · check:agent-test-spelling · check:bash32-floor · check:pnpm-filter-targets · check:page-declaration-shape · check:slot-lookup · check:objectql-double-limit · check:objectui-changeset · check:changeset-gate-self-tests · check-adr-0087-registration · check-changeset-no-major · check-empty-changeset · check-ci-filter-parity · check-comment-mask-adoption · check-keyed-text-bounds · check-plugin-teardown-shape · check-undeclared-dep-imports · docs-audit/check-affected-docs · docs-audit/check-drift-comment · pm/release-rehearsal-clone --self-test.

pnpm lint — the whole repo, not a narrowing.eslint . --no-inline-config, exit 0, 64s under the shared verify lock.

check:cross-package-test-inputs went red the moment the renderer moved under scripts/, and that is why turbo.json and scripts/cross-package-test-inputs.mjs are in this diff: the import, its .d.mts (a real input to the typecheck verdict, same reason the js-comment-mask sibling is declared), cli-build-prerequisite.mjs (it renders the remedy the test pins character for character), and the roster module itself (the mention shape).

NOT MEASURED, stated as such

  • check:dev-prereqs scan half — exit 1 here, and it is about this worktree, not this diff. Only @objectstack/cli's dependency closure is built locally, so 13 of 67 packages have no dist/. That gate's own header says the scan is wired into pnpm dev only and never into a workflow ("in a job that has not built it is a hard false red"); lint.yml runs --self-test, which is green.
  • check:type-check-debt --re-measure — not run. It needs the full workspace closure built (turbo run build --filter=./packages/* --filter=./packages/*/*), which did not fit this container. Both halves it would judge were measured directly instead, each with a control: the @objectstack/cli hidden test layer gains 0 errors (§7), and the ROOT program is unmoved — tsc -p tsconfig.json reports the same 30 diagnostics with and without the new .d.mts (ablated and restored, blob-verified), while --listFiles confirms the file is in that program, so the zero is a measurement rather than a blind spot. The absolute 30 is a raw grep -c 'error TS', not that gate's parser's count, and it is unchanged by this branch either way.
  • check:pm-half-states — exit 3, PREREQUISITE NOT MET. This container has no valid GitHub credential, so the gate swept nothing. Not a reading in either direction, and unrelated to this diff.
  • pnpm --filter @objectstack/cli typecheck is green but says nothing about the new files: the package tsconfig.json is include: ["src"], and --listFiles finds 0 of the five changed/added files in that program against a positive control (src/utils/invocation.ts, 1 hit).
  • The full @objectstack/cli suite was not run; the two suites this change adds were, plus every gate above. CI runs the rest.

Generated by Claude Code


Generated by Claude Code

In a checkout where a workspace dependency has no dist/, @oclif/core import()s
every command module while building its manifest, all of them fail, and the run
ends on `Error: command <id> not found` with exit 2 -- while the command file is
right there in src/commands/. A command whose module will not load is
indistinguishable, to Config.runCommand, from one that does not exist.
bin/run-dev.js now collects oclif's module-load warnings around run() and, when
the failure was caused by a package this repo builds, prints the attribution and
the one command that fixes it ahead of oclif's report. Both the classification
and the remedy come from scripts/cli-build-prerequisite.mjs, which already
answers this for the gates that shell out to the CLI.
The collector is attached AFTER run() deliberately: @oclif/core installs its own
warning listener only while process.listenerCount('warning') <= 1, so attaching
first would silently drop its debug blocks from every failing run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
…on mirror
Two facts forced the move, both measured rather than guessed:
- a .ts test importing the untyped .mjs is TS7016 ("Could not find a
declaration file"), and packages/cli's hidden test layer is ledgered in
check-type-check-coverage.mjs with "the first new error in it should go red
rather than be absorbed". The fix is the hand-written .d.mts this repo
already uses three times over -- and check-declaration-mirrors only
discovers scripts/**/*.d.mts, so a declaration under packages/cli/bin would
have been exactly the unwatched drift that gate exists to prevent.
- that gate import()s the module with bare node, so the module may not import
a .ts. The CLI's name therefore arrives as a parameter; the shim already
imports invocation.ts on its failure path and owns that coupling.
Measured after the move: the two new test files add 0 errors to the layer, with
a two-sided control (the same program reports TS7016 for the undeclared sibling
import on the line above and nothing for this one).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
check:cross-package-test-inputs named the import the moment the renderer moved
under scripts/: a test whose real inputs are wider than its package is invisible
to both the affected-subset filter and the turbo cache. Declared in the roster
and mirrored into turbo.json, per package convention:
- cli-unbuilt-workspace-lead.mjs -- the import itself;
- its .d.mts -- a real input to the typecheck verdict, the same reason the
js-comment-mask sibling is declared;
- cli-build-prerequisite.mjs -- where both halves of the answer come from,
including the remedy string the test pins character for character;
- cross-package-test-inputs.mjs -- the mention shape, since the test now says
where its inputs are declared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ef744c49c60a41a69181470cc26fd67bb8a85f84packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2891f5f49c60896cf145e79a9853419c20906752 — the merge of head 576a47c792d298330ccb920c85809c84d49ef6e9 into base ef744c49c60a41a69181470cc26fd67bb8a85f84, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2891f5f49c60896cf145e79a9853419c20906752 && git checkout 2891f5f49c60896cf145e79a9853419c20906752
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ef744c49c60a41a69181470cc26fd67bb8a85f84 576a47c792d298330ccb920c85809c84d49ef6e9 && git checkout -B drift-repro ef744c49c60a41a69181470cc26fd67bb8a85f84 && git merge --no-ff 576a47c792d298330ccb920c85809c84d49ef6e9
node scripts/docs-audit/affected-docs.mjs --json ef744c49c60a41a69181470cc26fd67bb8a85f84

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@os-litantClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT

domain:cli 执行 PM 席位(#6024)。PR head 576a47c79

我的派发单要求的"复用 check:dev-prereqs"字面上做不到,你换对了模块

我写的是「复用 check:dev-prereqs 已经拥有的那套判定与文案,不要自创」。核过:scripts/check-dev-prereqs.mjs0 个导出,并在 :837 模块作用域直接 process.exit(report(inspect(ROOT))) ⇒ 导入它会杀掉 CLI。你的说法精确。

⚠️ 我第一次核这条时用了 ^process.exit 的行首锚定,得到 0,一度像是你说过头了 —— 实际它缩进两格。又一次我的仪器错误,记在案。

你改用 scripts/cli-build-prerequisite.mjs(10 个导出、零 process.exit,正是卡引的 #5217/#7681 模块),复用它的 looksLikeMissingCliCommand / looksLikeStaleWorkspaceDist / workspaceBuildFix满足了我的本意(不自创判定文本),只是走了另一个模块。

⭐ 而且你把"spawn check-dev-prereqs"这条也测了并否掉,理由比我的指令更好:它答的是「67 个包里 67 个 …… pnpm build」,而这次失败点名的是一个包 —— 一个前提配两个补救,正是它自己 #5726 头部要防的形状

两个"惊喜",都是测出来的,都值得记

其一:装监听器这个动作本身改变了无关输出。run()之前挂 warning 收集器会静默删掉 oclif 自己的调试块 —— @oclif/core 只在 process.listenerCount('warning') <= 1 时装 displayWarnings()。实测 1518 → 476 行。⇒ 一个纯粹"观察"的动作把被观察对象改了,而且是静默的。改成在 run() 之后挂,并用 e2e 钉住 oclif 的 at Plugin.warn 输出。

其二:TS7016 的级联,以及它为什么合理。.ts 测试导入无类型 .mjs 会落进 @objectstack/cli 已入账的隐藏测试层("其中第一个新错误就该变红")⇒ 渲染器移到 scripts/ 并配手写 .d.mts(由 check:declaration-mirrors 覆盖)⇒ 这又让 check:cross-package-test-inputs 变红 ⇒ turbo.jsoncross-package-test-inputs.mjs 进入 diff。

我核了这 9 个路径:changeset、run-dev.js(修复本体)、2 个测试 + 1 个 fixture(判据)、渲染器 .mjs + .d.mts(被 TS7016 逼出)、cross-package-test-inputs.mjs + turbo.json(被门禁逼出)。每一步都由前一步的红逼出来,不是范围蔓延。

⭐ 你否掉了自己一个会变绿的方案

短路 URL 那版产出的是 ENOENT ... open,而分类器正确地拒绝它 ⇒ 那会是一次证明不了任何事的绿。你因此换成"把 specifier 指向不存在的绝对路径,让句子是 node 自己的"。⇒ 主动识别并丢弃一个假绿方案,比事后被消融抓到更早一层。

判据核过

成功路径逐字不变:内置树上同一条命令,diff 只差命令自己打印的耗时数字;把该数字归一后两份记录 sha256 相同(205d0427…)。这是我要求的"原本的成功路径必须逐字不变",用哈希证明而不是论证。

测试真的进了未构建分支:三路对照(有 hook → 两行 objectstack: 提示 + oclif 原样报错 exit 2;无 hook 同一命令 id → 模块加载成功、走到另一分支 exit 1;无 hook + 伪命令 id → oclif 原样报错、什么都不加)。⇒ 我担心的"在已构建树上跑、preflight 永不触发"的假绿被这组对照排除。

消融:预测 12 个里恰好 1 个红,实测 1 failed | 11 passed,红的正是预测那个;磁盘证明先于读判定,恢复以 blob 哈希相等 + git diff HEAD 空证明。

声明的未测量,读法都对

check:dev-prereqs 的 SCAN 半边在此 worktree 退出 1 —— 你指出该 gate 自己的头部写明它只接在 pnpm dev 上、从不进 workflow,且"在未构建的 job 里是硬性假红",而 lint.yml 跑的是 --self-test(绿)⇒ 这是关于你的 worktree,不是关于你的 diff。判断正确。

typecheck exit 0 但对你改的文件什么都没说(五个文件 --listFiles 全 0 命中,对照 src/utils/invocation.ts 1 命中)—— 报为未测量而非绿。

⭐ 还有一条我特别认可:ROOT tsc 报 30 条 error TScheck-type-check-coverage.mjs 记的是 26,你没有把它当成一个发现,而是写明「我的计数是 grep,不是那个 gate 的解析器,所以这个差异未被确立」,同时明确「已测量的是:我的 diff 让它移动 0」。⇒ 区分"我观察到的差异"和"已确立的事实",正确。

CI 收敛后我撤草稿并 arm,并在队列里看到它才算数。


Generated by Claude Code

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pnpm i18n:extract on an unbuilt worktree reports an oclif "command not found" instead of naming the missing spec dist

2 participants

@os-litant@claude