Uh oh!
There was an error while loading. Please reload this page.
fix(dispatch-gates): stop inheriting the CLI package as a population from the build prerequisite - #12841
Conversation
…from the build prerequisite `scripts/cli-build-prerequisite.mjs` spells `packages/cli` as a join base and as the vocabulary of its consumers' rerun commands. Followed as an import (#11190), that one literal reached check:i18n and check:i18n-coverage as a whole-package subtree claim: all 322 tracked files of the package, 210 of them unable to change a byte of the dist/ those gates spawn. Declare what a caller really inherits with the #11556 marker — the stub the gates spawn, the manifest they parse, and the src/ the spawned command is compiled from. Measured: 322 -> 214 covered files per gate, 216 fabricated pairs withdrawn, all 112 files the gates really read still named. The self-test's reconstruction re-scanned a followed module's raw literals, so it could not model the declaration its own case name claims; fixed at the same seam discoverFamilies reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
Ablating the marker left this one case green: with nothing inherited, nothing inherits the join base either. Require a non-empty declaration, so the case asserts the narrowing rather than an absence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
os-zhuang
commented
Aug 28, 2026
✅ ACCEPT — PM 复核( |
| 本席转述 | 实测 |
|---|---|
packages/cli/src/** | ✅ 是,但只是三分之一 |
| 「加语言包」 | ⛔ 根本不在这个模块的 population 里 —— 它们由 findExtractConfigs 在运行期走,门禁通过 SURFACE_MODULE 声明,且住在各自的宿主包下,从不在 packages/cli 下 |
| (漏) | ⛔ packages/cli/bin/run.js —— 两个门禁 spawn 的 stub |
| (漏) | ⛔ packages/cli/package.json —— resolveCliCommandFile 读它取 oclif.commands.target |
⇒ 按本席那句话收窄,会丢掉两个真实输入 —— 正是 Zone 1 第 2 条(回归约束)禁止的那种新 under-naming。 这是今晚第二次「PM 转述的东西比 dev 实测的差」,而两次都靠把它标成 Zone 2 而不是硬约束才没造成损害。
⭐⭐ 「声明只能收窄到模块真正拼写过的路径,绝不发明一个」
packages/cli/package.json 原来只以 join(CLI_PKG, 'package.json') 存在 —— 于是它必须先被拼写出来(atRepoRoot(CLI_PKG_JSON),渲染同样的字节,连错误消息里都一样)才有资格被声明。
⭐ 这条纪律本席没写进派发单,是你自己带来的,而且它正是这类 marker 不退化成「随便声明点什么」的原因。已抄进座位贴。
⭐⭐ 落点选择:marker 落在模块上,不落在 hint 解析上
"The literal is correct and load-bearing where it is written; what was wrong was a caller inheriting it."
⇒ 因此 hintCovers / extractWatchHints一个字节没动 —— 而这正是镜像轴纹丝不动的原因:回归约束不是靠小心翼翼避开,是按构造满足的。这比我在 Zone 3 里留的两个候选都好。
⭐⭐⭐ 两条既有自测用例被这次改动弄红了 —— 而你修的是它们,不是绕开它们
- 一条在重建时重扫被跟随模块的 RAW 字面量,而它自己的名字却声称「共享枚举器可以为它的调用者携带声明」。⇒ 名实不符,在
discoverFamilies读的同一个接缝(hintsOfModule)修好。 exactly one module in the scripts tree carries the declaration—— 一条蔓延棘轮(spread ratchet)。你把它从数到一改成点名那两个已定价的模块,于是第三个仍然会红、并且必须带着自己的实测价格到来。
⭐⭐ 第 2 条尤其好:一个「计数式棘轮」被换成「具名集合式棘轮」。它保住了守卫(第三个照样红),同时让守卫对它守的是什么变得诚实。⛔ 这不是削弱 —— 数字 1 从来不是要守的东西,「每一个都得定价」才是。
⭐ 空集合陷阱 —— 今晚第三个独立踩到的 dev
"The first pass found one of them satisfiable by the empty set — with nothing inherited, nothing inherits the join base either."
前两位是 #12815 的 dev(自己新写的两条断言被空列表满足)与 #12749 的 dev(sweep 找不到 root 时守卫恒真)。三种形状,一个根:只钉离场会被缺陷满足。你这条又多一层 —— 空集合同时满足「没有继承」与「没有错误继承」。
收益已量,且零召回损失
packages/cli 322 个受跟踪文件,每个门禁:
被继承 hint 覆盖 322 → 214
门禁真正读的 112 → 112 ← 零丢失
假线索 210 → 102
引入的 under-naming 0 → 0
⇒ 108 文件 × 2 门禁 = 216 个虚构配对被撤回,每一个都在向碰到它的卡收一次 CLI closure build
残留:说出来而不是藏起来
packages/cli/src 是前缀而 hintCovers 匹配子树 ⇒ src/ 下交错的约 101 个 *.test.ts 仍被点名。它们不编进任何一个门禁跑的东西(tsconfig.build.json 排除),但一条声明表达不了「这棵树里被编译的那个子集」。构建配置也刻意不声明:一次 tsconfig-only 的改动若改变了命令面,代价是丢一条线索 = 一张卡一轮 CI,对比每条假线索收的约 10 分钟 —— 这个推导在每一处都往这个方向偏。✅ 方向正确且理由成文。
其余
- 成本自觉执行到位:没有为验证自己而跑
check:i18n的 closure build。✅ 本卡的主题就是那次 build,付掉它会很讽刺。 - 消融:marker 行替换,磁盘上以 grep 计数 +
git hash-object与 HEAD blob 不等确认变异落地,EXIT trap 绝对路径还原并以哈希相等证明,7 of 7 变红。✅ - ESLint 收窄被证明:仓库自己的
eslint.config.mjs机械上零个parserOptions.project⇒ 未触碰文件的判词不可能移动。✅ - changeset ⛔ 无,
skip-changeset。✅
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12500
A comment-only diff to
packages/cli/vitest.config.tsderivedcheck:i18nandcheck:i18n-coverage, and both refuse to run without a built CLI. The refusaltext is exemplary, so a dev that skipped the build read NOT MEASURED rather than
green: the cost was never a false verdict, it was a full CLI closure build
bought per card to measure two gates the diff provably could not move.
Where it came from
scripts/cli-build-prerequisite.mjsspellspackages/cliasCLI_PKG, a joinbase (
join(CLI_PKG, 'package.json'), and thedist/commandspathoclifCommandFileForbuilds) and the vocabulary of every rerun command itsconsumers print. Both i18n gates import that module, so #11190's first-party
import follow hands them its module-body literals — and from outside, a join
base is indistinguishable from a whole-package subtree claim. That is the exact
class #11556 built the
inherited-populationmarker for, and this modulecarried no marker.
Zone 2 — the transcribed population is INCOMPLETE, so it was not used verbatim
The dispatch flagged "
packages/cli/src/**plus the locale bundles" astranscribed from triage and unverified. Read against the sources
(
resolveCliCommandFile,oclifCommandFileFor, thespawnSyncin each gate),what a consumer of the prerequisite module really touches under
packages/cliis three things, and narrowing to the transcribed sentence would have dropped
two of them — a NEW under-naming, which is what Zone 2 warned about:
packages/cli/bin/run.jspackages/cli/package.jsonresolveCliCommandFileforoclif.commands.targetpackages/cli/srcdist/commandsthat spawn resolvesThe locale bundles are not in this module's population at all: they are walked
at runtime by
findExtractConfigs, which the gate declares throughSURFACE_MODULE, and they live under the owning packages, never underpackages/cli.packages/cli/package.jsonhad to be SPELLED before it could be declared (adeclaration may only narrow to paths its module really spells, never invent
one), so the read is now
atRepoRoot(CLI_PKG_JSON)instead ofjoin(CLI_PKG, 'package.json'). Both render the same bytes, including in theerror message.
CLI_SRCis a bare coupling constant in the shape #9144 alreadyuses one file over.
Landing site (Zone 3)
The marker, on
scripts/cli-build-prerequisite.mjs— not the hint parsing indispatch-gates.mjs. The literal is correct and load-bearing where it iswritten; what was wrong was a caller inheriting it. A declaration on the module
protects every caller, including one written next year, and this is the second
live specimen of a mechanism that already exists rather than a new rule. Nothing
in
hintCoversorextractWatchHintsmoves, which is also what keeps themirror axis below untouched.
Measured — both axes, before and after
Readings taken with the tool at the merge base (
f907fbe9e, in a secondworktree) and at this branch head. Each run carries a positive control that
DOES return non-zero under the same condition, and a negative control.
Over-naming axis (this card), specimen
packages/cli/vitest.config.ts:Under-naming mirror recorded in #12322 (regression constraint), specimen
packages/plugins/plugin-email/test/send.test.ts:#12322 remains open on its own terms; nothing here touches it.
Population sweep over the 322 tracked files of
packages/cli, per gate:108 files x 2 gates = 216 fabricated pairs withdrawn, each of which was charging
the card that touched it a CLI closure build.
Residual, stated rather than hidden
packages/cli/srcis a prefix andhintCoversmatches subtrees, so the ~101*.test.tsfiles interleaved undersrc/are still named. They compile intonothing either gate runs (
tsconfig.build.jsonexcludes them), so they are thesame species of false lead — a declaration simply cannot express "the compiled
subset of this tree". Build configuration is also deliberately left undeclared:
a tsconfig-only edit that changes the shipped command surface loses one lead,
which is one card one CI round, against the ~10 minutes each false lead charged
— the direction this derivation errs in everywhere.
Self-test: 803 -> 810, and two pre-existing cases that this change turned red
Adding the second live declaration to the tree reddened two cases that had
encoded "no live module narrows":
a family's hints are exactly those of the scripts its COMMAND names PLUS those of the first-party modules those scripts import— its reconstructionre-scanned a followed module's RAW literals while its own name claims a shared
enumerator can carry a declaration for its callers. Fixed at the same seam
discoverFamiliesreads (hintsOfModule).exactly one module in the scripts tree carries the declaration— a spreadratchet. It now NAMES the two priced modules instead of counting to one, so a
third still reddens and arrives with its own measured price.
Seven new cases, both directions, against real tracked files. Ablation (marker
line replaced, mutation confirmed on disk by grep counts and a
git hash-objectmismatch against the HEAD blob, restored via an EXIT trap withabsolute paths and proved by hash equality afterwards): 7 of 7 go red. The
first pass found one of them satisfiable by the empty set — with nothing
inherited, nothing inherits the join base either — and that case now requires a
non-empty declaration. No build or
dist/is involved on either leg: this toolruns from source.
Gates run, all on this head (
399cf6971, working tree clean)Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackagainst the real change set, then run with the exit code captured before any
pipe. Each verdict below is the line the gate printed for itself.
check:pm-dispatch-gatesdispatch-gates self-test: 810 cases pass.check:cross-package-test-inputsOK: 20 package(s) read outside themselves, all declaredcheck:entry-guard170 scripts/ file(s) - every entry guard goes through invoked-as.mjscheck:parse-guard169 scripts/ file(s) - every TypeScript parse goes through ts-parse.mjscheck:cli-command-ids287 command-id literal(s) ... all resolve to a real command pathcheck:self-test-wired(script)every one of the 140 script(s) CI runs that ship a --self-test has that self-test run by CIcheck:bash32-floor22 tracked shell file(s) ... name no bash 4+ constructcheck:agent-test-spelling0 violations - 389 file(s)check:pnpm-filter-targets140/177 --filter occurrence(s) ... resolvecheck-ci-filter-parity.mjsOK: all 109 declared cross-package glob(s) ... coveredcheck:nul-bytesOK (scanned 7138 text file(s) ... no raw ASCII control bytes)bare-root-worklist.mjs --self-test46 live row(s) ... none stale, none missing, none contradictedLint is a DECLARED NARROWING, not a skipped gate:
eslint --no-inline-configonthe two changed files, 0 errors and 0 warnings, 2 files counted from
--format json; the population came from the repo's owneslint.config.mjsrather than from a guess about which files count; and that config states, with a
measured positive control, that it "never enables type-aware linting (no
parserOptions.project, no typed@typescript-eslintrules) for ANY file" — sothis diff cannot move the verdict on any file it does not touch. The repo-wide
run is CI's.
No changeset: root
scripts/tooling publishes nothing, so this ridesskip-changeset(an empty-frontmatter changeset is a red gate here, not asubstitute).
Generated by Claude Code
Generated by Claude Code