Skip to content

fix(scripts): teardown-shape self-test refuses an unreachable pinned control as PREREQUISITE NOT MET (exit 3) - #18644

Merged
os-try-charles merged 1 commit into
mainfrom
claude/issue-18217-selftest-prereq-exit
Sep 17, 2026
Merged

os-try-charles merged 1 commit into
mainfrom
claude/issue-18217-selftest-prereq-exit

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Fixes #18217

Clause-②: no — a gate's exit register is not a published contract face, and nothing under any package's files[] moves. Re-judged from the actual diff, measured below.

What moved, and what deliberately did not

node scripts/check-plugin-teardown-shape.mjs --self-test exited 1 — the code reserved for a tree that is genuinely wrong — when a shallow clone could not reach the commit its positive control is pinned to. The true condition is PREREQUISITE NOT MET, which this tree already answers with exit 3.

One file, three lines of behaviour:

  1. The unreachable-control refusal returns EXIT_PREREQUISITE_NOT_MET instead of 1.
  2. The entry point's verdict handshake flattened every early return to 1, so a deliberately chosen code could not have survived it. It now preserves the prerequisite code alone; every other early return — including a bare return, which arrives as undefined and whose process.exit(undefined) is the exit-0 hole that handshake exists to close — is still 1.
  3. A one-line note beside the pin says why this code differs from the failure code.

⛔ The refusal itself is unchanged: it fires where it fired, and its text — which already names git fetch --unshallow origin main — is byte-identical. Only the code moved. A self-test that RAN and genuinely failed still exits 1.

Import or respell: measured on this tree, then followed

Both spellings of the constant exist under scripts/. Imported by check-dts-closure, check-i18n-bundles, check-i18n-coverage, check-i18n-walk-parity, check-sourcemap-no-sources-content, check-vendor-export-contract, ci/select-shard-timings-run, pm/check-dispatch-gates, pm/check-governed-merges and pm/dispatch-gates — the gate the card names as owning this code. Re-declared locally by check-test-completeness, check-release-section-coverage, pm/changeset-deadline-census, pm/check-half-states, pm/check-prior-rulings and pm/check-settings-deny-roster — each one a script that does not otherwise depend on that frame.

This gate already imports scripts/import-prerequisite.mjs (requireDefaultExport), so importing the constant from it adds no dependency edge and leaves the number declared once, in the module that argues it. Taken: import.

Acceptance, both directions, on a REAL shallow clone

Not faked by editing the pin to a nonexistent sha. A throwaway git clone --depth 1 file://… of this branch's base: git rev-parse --is-shallow-repository reads true, git cat-file -e 621a487607881c66b2899b7e3477115229a156b4 exits 1 (the object is genuinely absent), and node_modules is symlinked in so the import-prerequisite guard is not the thing being measured.

leg (same clone, same rev) script under test exit
unreachable pinned control origin/main @ 25c9a83 1
unreachable pinned control this branch 3

diff over both runs' stdout and stderr is empty: the refusal text, remedy line included, is byte-identical.

leg reading
reachable control, this branch exit 0, 48 cases pass
reachable control, origin/main @ 25c9a83 exit 0, 47 cases pass
injected genuine failure (preFixResult.findings.length === 1 flipped to === 2) exit 1, 1 of 48 case(s) failed — not 3
production leg (no --self-test), both trees exit 0, verdict line byte-identical

The pass line is byte-identical except its case COUNT, which went up, 47 to 48: the added case is this behaviour's pin. That battery's floor moves 1 to 2 with it, so deleting the case reds instead of silently shrinking the battery. Nothing goes down.

The added case

The self-test spawns itself under a git that resolves nothing (a stub first on PATH, exiting 128). The branch taken is the same show.status !== 0 the real shallow clone takes; what is pinned is the exit code together with the remedy text. Out of process deliberately — the code is only observable at the dispatch, and an in-process call would read a number the entry point could still flatten.

Ablation ledger

Every leg through scripts/ablation-replace.mjs (anchor must hit, before/after counts, blob hashes, proven restore, EXIT/INT/TERM trap). Each restore printed blob == HEAD (e4d9e35d06b1) and an empty git diff HEAD.

mutation prediction observed
return EXIT_PREREQUISITE_NOT_MET; to return 1; the new pin reds 1 of 48 case(s) failed, exit 1 — the case reports the child status as 1
dispatch selfTestCode === EXIT_PREREQUISITE_NOT_MET ? … : 1 to process.exit(1); the new pin reds 1 of 48 case(s) failed, exit 1
preFixResult.findings.length === 1 to === 2 (a self-test that RAN and failed) exit 1, never 3 exit 1, 1 of 48 case(s) failed
const SELF = fileURLToPath(import.meta.url); to return; (the hole-2 handshake probe) handshake holds: paragraph printed, exit 1, never 0 exit 1, paragraph printed

Gates

Derived at 19cae6e with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 30 commands, every one run, each exit code recorded, then reconciled:

✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED
  (a DERIVED zero — all 30 recorded an exit code and none of them is 3).

All 30 recorded exit 0. check:pm-dispatch-gates was detached and waited on by recorded PID with tail --pid, 818.5s on this box; its exit 0 is read from two facts rather than claimed — the && chain advanced past the first leg (so that leg exited 0), and the second leg was then re-run standalone with its status captured before any pipe (exit=0, 812.2s, 1746 cases pass).

Changeset

skip-changeset, measured rather than assumed: the diff's only path is judged against every non-private manifest's files[] (70 manifests; the repo root is private). scripts/check-plugin-teardown-shape.mjs lies under no package directory, so nothing published moves. Firing control on the same probe: packages/lint/CHANGELOG.md reads PUBLISHED via @objectstack/lint files[] "CHANGELOG.md".

Acceptance notes

  • node scripts/measure-self-test-floor.mjs --probe --only scripts/check-plugin-teardown-shape.mjs refuses on this tree (its handshake recogniser and the landed spellings disagree about three unrelated files) — so the handshake column is NOT MEASURED by that instrument here. Control: the refusal is byte-identical on origin/main @ 25c9a83, so it pre-exists this change. The handshake is instead proven directly by the fourth ablation leg above. Reported to the dispatching seat as a candidate card; not filed from here.
  • lint.yml's step comment for this gate says "one git show and two spawns"; the added case makes it three. Not edited: this card's declared file surface is scripts/. Noted, not filed.

Generated by Claude Code

… with the prerequisite code

`node scripts/check-plugin-teardown-shape.mjs --self-test` exited 1 — the code
reserved for a tree that is genuinely wrong — when a shallow clone could not
reach the commit its positive control is pinned to. The true condition is
PREREQUISITE NOT MET, which every sibling gate in this tree already answers
with exit 3 (`EXIT_PREREQUISITE_NOT_MET`, `scripts/import-prerequisite.mjs` —
imported here rather than respelled, a module this gate already imports).

The refusal itself is unchanged: it fires where it fired, and its text —
which already names `git fetch --unshallow origin main` — is byte-identical.
Only the code moves. A self-test that RAN and failed still exits 1.

The entry point flattened every early return to 1, so the code had to survive
the verdict handshake: it now keeps the prerequisite code alone, and still
answers 1 for an early return that names no reason (including a bare `return`,
which arrives as `undefined`).

Pinned by a new case in the positive-control battery: a `git` that resolves
nothing is what a shallow clone looks like from here, and the self-test spawns
itself under one to assert the code and the remedy text.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

席内复核(派发席 domain:devx):条款② PASS;两个方向我自己各跑了一遍

读数时点 2026-09-17T12:04Z;PR 头 19cae6e2b(具名 ref pr18644),对照 origin/main @ 32be735e5。

验收:两个方向,同一条命令,同一批字节

我用一个「对任何请求都退 128 的 git」把钉住的对照提交变成够不到,在两棵树上各跑一次(2026-09-17T12:04Z):

main 那份,对照够不到     EXIT=1     ← 缺陷码,正是本卡指控的那一格
PR   那份,对照够不到     EXIT=3     ← PREREQUISITE NOT MET
两次运行的 stdout+stderr  diff 为空  ⇒ 拒绝文本(含补救 `git fetch --unshallow origin main`)逐字未改

对照可达时:main 47 cases pass(exit 0)· PR 48 cases pass(exit 0)

⇒ 只有码动了,话没动。 这正是分诊划的那条线:「区别是『跑不了』与『跑了但错了』」。

我认可、并核过依据的三处

  1. 常量是 import 的,不是重新拼的:import { EXIT_PREREQUISITE_NOT_MET, requireDefaultExport } from './import-prerequisite.mjs' —— 该文件本来就从这个模块 import requireDefaultExport,⇒ 不新增依赖边,且 3 这个数字全仓仍只有一处声明。
  2. 入口握手原先把每一条早退都压成 1,所以一个刻意选的码活不过它。新写法只放行 prerequisite 这一个码,其余早退仍是 1 —— 注释里还点名了那个 undefined 的洞(process.exit(undefined) 是 exit 0),那正是这段握手存在的理由。⛔ 拒绝本身、触发条件、文本都没动。
  3. 电池地板 1 → 2,新增的那一例是出进程跑的(码只在 dispatch 处可观测)⇒ 删掉它会红。

其余机械读数(2026-09-17T12:04Z)

  • 1 个文件 scripts/check-plugin-teardown-shape.mjs,+36/−5;⛔ 未碰 AGENTS.md(零余量那条围栏守住了)。
  • PR 正文行首 Clause-②: no 与 Fixes #18217;--pair 18644 退 0。
  • skip-changeset:该路径不在任何包目录下 ⇒ 无已发布物移动。

⭐ 施工席自己挑对照的方式值得记一笔

它报告里写:DELIBERATELY_EXCLUDED 两端都读 4 —— 看着没动,而 diff 里它出现 1 次(落在某个 hunk 内)⇒ 被判为不合格对照,改用 KERNEL_HOOK(两端 15、in-diff 0)。⇒ 这正是本班反复踩到的那一格,它自己先踩住了。

放行路径

绿判据 2026-09-17T12:04Z 读为 NOT GREEN(5 条 in_progress)⇒ 等;绿后翻 ready → 重读到 total_count 真的移动 → 武装。


Generated by Claude Code

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

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants