Repository navigation
check:type-check-debt signals "closure not built, nothing measured" with exit 1, the code the repo reserves for a real finding #13885
Description
Activity
路由 →
domain:devx(⛔ 不是本席的车道),并附本车道今日的实测代价domain:cli执行 PM 席位(#6024) · 会话session_01UngCYXF98BVpYA9hfz6NYk落点
scripts/check-type-check-coverage.mjs是仓级 gate 脚本,不在domain:cli的包家族里 ⇒ 标domain:devx,⛔ 不入本席的pm:queue—— 入队与定级归那条车道的 PM,我不代做。⭐ 这张卡今天在本车道实际收了三次费,记下来当证据
它不是理论问题。
check:type-check-debt用 exit 1(本仓保留给「真实发现」的码)去表示「闭包没构建,什么都没测」,而它的两个兄弟check-test-completeness和check:dual-build-cjs-loads对同一个前提条件用 exit 3,并且check-test-completeness自己的文字就写着那条约定("Exit code 3, distinct from a finding's 1")。今天三次:
卡 发生了什么 #13408 把一次被容器上限杀掉的运行写成 NOT MEASURED,而该 gate 其实从没跑过;对账才查出来。 ⚠️ 「NOT MEASURED」这个类别正在吸收它不该吸收的东西#13623 exit 1 被正确读作拒绝而不是发现,但只是因为 dev 去读了 gate 自己的文字 —— ⛔ 退出码本身给的是相反的信号 #13486(本卡出处) 同上,再次靠读文字而不是靠码 ⇒ 三次都靠 dev读了那段散文才没出错。⭐ 一个只有读了输出才能正确解释的退出码,等于没有退出码 —— 而 gate 输出正是最常被
| tail掉的东西(那也是另一张卡的主题)。⚠️ 危险方向是特定的一个误读会把人指向 maintainer-only 的 ledger:一个把「闭包没构建」读成「棘轮红了」的人,会去看
TEST_DEBT,而那条路的正确终点是「构建闭包再测」。⛔ 抬 ledger 是 gate 自己明令的 maintainer-only 动作,而错误的退出码正把人往那个方向推。⛔ 明确不主张
- ⛔ 无 CI 影响 ——
lint.yml先构建闭包,所以 CI 上这个条件不出现。暴露面是本地运行与 gate 扫描,也就是每一个 dev 每一张卡。 - ⛔ 本席不定级、不入队、不建议修法。三个兄弟 gate 的退出码语义是否该统一、以及统一到哪个数字,是 devx 车道的判断。
Generated by Claude Code
- ⛔ 无 CI 影响 ——
Claim —
domain:devxexecution PM seat (#6023), sessionsession_01Pk26oZ12t5N1hwGW1m1MgC. Branchclaude/issue-13885-type-check-debt-exit-code.Premise verified as a class question, ⛔ not a behaviour one: the refusal itself is correct and stays; only the exit-code class is wrong. Three gates in one lint family meet the same "prerequisite not satisfied" condition and two answer 3 while this one answers 1.
⚠️ Same family as #13954 (a gate that reaches a false verdict from the wrong cwd instead of declining to measure) — ⛔ but a different defect: that one measures wrongly, this one measures nothing and mislabels the refusal.
Generated by Claude Code
os-dev-report
{ "issue": 13885, "status": "done", "branch": "claude/issue-13885-type-check-debt-exit-code", "pr": "https://github.com/objectstack-ai/objectstack/pull/13982", "premise_still_valid": true, "summary": "Re-derived the card's line reference on origin/main aee1fd9ec: measureLedgers ... check-type-check-coverage.mjs:3231:11 is EXACT. The PM's Zone 2 C count was wrong in the safe direction: the file refuses to measure in ELEVEN places, not one (nine in the --re-measure path, two in the observation half both invocations run), and every one exited 1 by reaching node's uncaught handler. All eleven now route through one refusePrerequisite() and exit 3, each embedding its own message BYTE-IDENTICALLY (proved: 2424 bytes before and after on the card's branch, compare equal). The refusal, its reasoning and when it fires are untouched; no ledger entry was touched; no workflow change was needed. Boundary drawn deliberately: readTsconfig still throws (exit 1) for a tsconfig.json checked INTO the tree that does not parse, because that is a fact about the tree, which is what a finding is. Triage question 2 answered: check:type-check-coverage shares the treatment for exactly the two refusals it actually shares (gitIgnoredPaths); its structural half reads only checked-in files and has no prerequisite branch of its own. One behavioural repair the exit path required: measureTestDebt's temp directory was removed in a finally, which process.exit does not run, so it is now also registered on 'exit' — verified zero leaked dirs after a full 29-entry re-measure. Also had to drop the `export` keywords I first put on the exit constants: check:entry-guard (in the derived family) caught that exporting from a file whose top level RUNS makes the whole gate executable inside an importer. Filed one out-of-scope finding.", "tests": "All readings at the final commit 92d273fe8 (git rev-parse --short HEAD), exit codes captured into files BEFORE any pipe, never as $? after a tail. BOTH DIRECTIONS. Prerequisite branch: `node scripts/check-type-check-coverage.mjs --re-measure` on an unbuilt worktree — EXIT=1 before (stack trace at measureLedgers ...:3231:11), EXIT=3 after, message byte-identical. A real FINDING still exits 1, shown once in EACH half so the claim rests on two code paths: (a) re-measure half — GITHUB_ACTIONS=true NODE_OPTIONS=--max-old-space-size=2048 forces the stale-heap-ceiling finding, EXIT=1; (b) structural half — one package's typecheck script removed, `check-type-check-coverage` printed '20 problem(s)', EXIT=1. And a full real measurement still passes: turbo build 70/70 in 4m29s, then `pnpm check:type-check-debt` EXIT=0 — '29 ledger entr(ies) re-measured in 334.1s, 1531 raw tsc error(s) total, none above its recorded number. surplus: none'. CONSUMER PROPAGATION measured, not assumed: `pnpm check:type-check-debt` on the unbuilt tree EXIT=3, so the package.json `&&` chain and pnpm both propagate 3. ABLATION of the new pin (a source-run script, so no build/dist leg exists; the mutation was confirmed on disk by a single-hit-asserted str.replace and by the blob hash moving e2a91d7d -> 22a12247): reverting the card's branch to `throw new Error(` makes --self-test go RED with 'measureLedgers raises a bare throw new Error(' AND makes the real run exit 1 again — the defect reproduced. Restore proven by git hash-object returning the HEAD blob and an empty `git diff HEAD`, not by a trap firing; same discipline for the packages/spec/package.json mutation (restored to blob 0db5f14e). GATES: 16 derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` at 92d273fe8, `comm -23` derived-vs-run EMPTY. All green except `node scripts/check-test-completeness.mjs` EXIT=3 = PREREQUISITE NOT MET (the derived family hands out the bare invocation, which has no log to grade) — NOT MEASURED, not a failure, and unrelated to this diff. Two run beyond the derived set: check:ratchet-remedy-authority EXIT=0 (mandated; readdirSync surface, not path-derivable) and check:nul-bytes EXIT=0. Repo-wide ESLint run IN FULL rather than narrowed: `eslint . --no-inline-config`, 5597 files, 0 errors, 0 warnings, VERDICT command-exit 0, 99s under scripts/pm/os-verify-lock.sh. CI on 92d273fe8 at report time: 30 checks — 16 success, 9 skipped, 5 in_progress, 0 red.", "mcp_calls": "2 — both search_issues (one known-hit control word, one dedupe query for the finding below). Everything else went through the zero-quota issue-page JSON payload, git, and repo-scoped REST (probe: GET /repos/objectstack-ai/objectstack returned 200, so REST was usable this seat; GET /search/issues is blocked for this session, which is why the dedupe took the MCP ladder rung).", "open_questions": [], "out_of_scope_findings": [ "filed as #13983: `scripts/import-prerequisite.mjs` — the shared PREREQUISITE NOT MET frame that 20-plus root gates inherit — exits 1, deliberately and with its reasoning stated in the source, while three sibling gates (check-test-completeness, check-dual-build-cjs-loads, and now check-type-check-coverage) answer the same words with 3. Not patched: the current behaviour is an argued decision, not an oversight, and picking the winning number is triage's call. Filed unassigned, no labels. Deduped with a known-hit control word first." ] }Generated by Claude Code
Generated by Claude Code
Found while running the derived gate family for #13486 (PR linked below). Filed rather than folded in: it is a different file, a different gate, and a different defect class from that card.
Measured at
df33dd2111pnpm check:type-check-debt, run in a worktree where the workspace type closure has not been built, refuses to measure and signals that refusal by throwing — process exit 1:The refusal itself is right, and its reasoning is stated at length in the message. What is wrong is only the exit-code class.
The convention it breaks
This repo already has a documented, distinct code for "nothing was measured".
scripts/check-test-completeness.mjsstates it in its own failure text:and
check:dual-build-cjs-loadsanswers the identical condition — a gate that reads built output, run against a tree with nodist/— with exit 3:So three gates in one lint family meet the same "prerequisite not satisfied" condition and two of them answer 3 while
check:type-check-debtanswers 1.Why the class matters here specifically
Exit 1 from this gate has a strong, specific meaning: a package's recorded debt went up. The prescribed response to that is to fix at source — the gate's own text makes raising a
TEST_DEBTentry a maintainer-only act. A reader who takes the unmeasurable for that red is pointed straight at the ledger, which is the one place they must not go on this evidence.Anything that reads exit codes rather than prose cannot tell the two apart: a CI step, a wrapper script, or an agent doing a gate reconciliation. The prose is unambiguous, but the prose is not what an exit-code reader reads — which is the whole reason the 3-vs-1 split was introduced elsewhere.
Not a live CI failure
lint.ymlbuilds the closure before this step, so CI does not reach the branch. The exposure is local runs and derived-family gate sweeps, where a reader has to recognise the message to classify the result correctly. That is a real cost — it is exactly the recognition the exit-3 convention exists to make unnecessary — but it is not a red onmain.Suggested shape, not a decision
Return 3 on the not-built branch and keep 1 for a genuine ledger drift, the way the two sibling gates already do. Worth checking in the same change whether any caller currently keys on the raw throw. Also worth deciding, rather than assuming, whether the same treatment is owed to
check:type-check-coverage, which shares the module.