Repository navigation
Description
Found while floooring scripts/check-regen-pending.mjs (batch 10d of #13799, PR #15338). Filed unassigned, out of that card's scope.
What happens
classifyFloor() in scripts/measure-self-test-floor.mjs returns ROSTER only when BOTH halves match:
ROSTER_COMPARISON— the roster's name (SELF_TEST_BATTERIES/declaredBatteries/BATTERY_FLOOR) compared as a set;PRODUCES_FAILURE—failures.push,process.exit(1),throw new Error,exitCode = 1, orok(false.
A self-test whose exit is spelled as a ternary — process.exit(ok ? 0 : 1), which is a common and perfectly ordinary spelling — matches NONE of the PRODUCES_FAILURE alternatives, because the literal process.exit(1) never appears. So a file can carry a complete, working roster floor and still classify NONE.
Measured on scripts/check-regen-pending.mjs mid-card, before I changed its verdict spelling:
ROSTER_COMPARISON matches: true
PRODUCES_FAILURE matches : false
classifyFloor : NONE
The floor was real and its ablations all fired (a callee that did not run reds); only the classifier could not see it.
Impact today: none — this is about recall, not a wrong verdict on main
Swept the current population: 0 files classify NONE while carrying a roster the instrument can see. Three files use a ternary process.exit and all three classify ROSTER anyway, because they happen to carry another recognised failure spelling elsewhere in the file (check-i18n-stale-fill.mjs, check-page-declaration-shape.mjs, and check-regen-pending.mjs after PR #15338).
So nothing is mis-reported right now. What this costs is prospective, and it is a real cost for the remaining #13799 batches: a Tier C author who copies the recipe faithfully onto a file whose verdict is a ternary exit gets a green ablation sweep and an instrument that still says NONE, and has to diagnose why. It cost one extra commit on PR #15338.
Note this is distinct from the known one-direction caveat in the script's own header, which is about a floor spelled with names it does not know. Here the roster name IS known and matched; it is the failure-production half that misses.
Also distinct from the two open issues in the same family: #14963 (the handshake probe anchors on the first function selfTest() {) and #14842 (two self-test-shaped functions and no ENTRY_BY_HAND row).
Options
- A. Extend
PRODUCES_FAILUREwith the ternary shape (something matchingprocess.exit(...: 1)), keeping the deliberate rule that a floor must PRODUCE a failure rather than merely be named. Cheapest; keeps the instrument high-recall as its header intends. - B. Leave the classifier alone and make the Tier C recipe state that the failure path must use a recognised production spelling. Zero risk to the instrument, but it pushes a tool's blind spot onto every future author.
- C. Do nothing — 0 current mis-classifications, and the header already warns the population is an upper bound.
Recommendation: A, with a --self-test case pinning the ternary shape, since the instrument's whole job is to bound the hole and a false NONE inflates that bound in exactly the population #13799 is working through.
Repro
node -e "const m=await import('./scripts/measure-self-test-floor.mjs');
const c='const SELF_TEST_BATTERIES={a:1}; const d=Object.keys(SELF_TEST_BATTERIES); if(d.length<1){} process.exit(ok ? 0 : 1);';
console.log(m.classifyFloor(c));" // prints NONE
Activity
claude commentedon Sep 4, 2026
Claim: PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk, dispatching to anos-devsubagent on branchclaude/issue-15339-produces-failure-ternary(S). Serial-queued behind #15324 onscripts/measure-self-test-floor.mjs; #15372 merged 14:00:25Z, main at5b2ad1b41. Assignee field was empty at the read immediately before this write.Generated by Claude Code
claude commentedon Sep 4, 2026
os-dev-report
{ "issue": 15339, "status": "done", "branch": "claude/issue-15339-produces-failure-ternary", "pr": "https://github.com/objectstack-ai/objectstack/pull/15413", "premise_still_valid": true, "summary": "classifyFloor's PRODUCES_FAILURE half is now published in two named parts. NAMED keeps the five literal spellings; TERNARY recognises an exit whose failing arm is a non-zero LITERAL -- process.exit(COND ? 0 : 1), process.exit(COND ? 0 : N) with N not 0, and the inverted process.exit(COND ? 1 : 0). A bare process.exit(EXPR) over an opaque expression stays unrecognised on purpose and the docblock says why: after an early return it is process.exit(undefined), i.e. exit 0 -- the accident shape this instrument exists to tell apart from a handshake. Seven inline controls come with it (they run on every invocation, the file has no --self-test): a dedicated TERNARY_EXIT_GATE fixture reads ROSTER, its non-1 and inverted arms read ROSTER, a bare opaque exit reads NONE, the same fixture with the roster renamed reads NONE, a setup control asserts the fixture carries none of the NAMED spellings, and a reuse control pins the recognised shape to the ACCIDENT fixture's own dispatch line. The ACCIDENT fixture could NOT itself serve as the positive fixture -- it carries failures.push, so a ROSTER verdict on it would not isolate the ternary -- so it is reused as the pin and a dedicated fixture was added in the existing convention, as the card allowed. Additive: no --json key renamed, tallies unchanged. Overlap with the other open cards on this file was left untouched: #15391 (probe awards HELD on a failed baseline), #15371 (helper-spelling probe coverage), #14963 (probe anchors on the first function selfTest), #14842 (two self-test-shaped functions, no ENTRY_BY_HAND row) -- none of them is touched by this diff, which sits entirely in Instrument 1 and the controls. Assignee was already set by the PM (baozhoutao) and was not changed; claim comment 5541578613 read before starting.", "tests": "Final head 9d98ef60a; all exit codes captured before any pipe. (1) BEFORE/AFTER on the branch: node scripts/measure-self-test-floor.mjs exit 0, inline controls pass, 'measure-self-test-floor: 170 file(s) under scripts/ dispatch on --self-test' and '6 of 170. Floored: 163 roster, 1 count-candidate(s) to read.' -- tallies 163 ROSTER / 6 NONE / 1 COUNT, UNCHANGED as asserted; node scripts/measure-self-test-floor.mjs --json is BYTE-IDENTICAL before and after (diff exit 0), both at 9ccd3cdc4 and again at 9d98ef60a. (2) ABLATION proving the recogniser: git show 7706a50bd:scripts/check-regen-pending.mjs (sound roster, ternary exit) written into the worktree. Mutation confirmed ON DISK before any reading: git hash-object moved 9f0342101f5f861eab335d1a304cdca7fdf9ca31 -> 08ecd0f75695c22560e39ef66d503077997f5d1a, grep counted 1 ternary-exit line and 0 explicit-branch process.exit(1) lines. Readings over the same bytes: base recogniser (5b2ad1b41) = NONE, patched = ROSTER; census over the same 170 rows base 162 ROSTER / 7 NONE / 1 COUNT vs patched 163 / 6 / 1. 'ABLATION VERDICT: PASS'. No rebuild leg applies -- both instruments are plain ESM read from source, nothing is compiled to dist. RESTORE proven, not assumed: restore leg ran from a trap on EXIT INT TERM using an absolute REPO_ROOT and 'git checkout HEAD -- path' (never the bare form); afterwards git hash-object = HEAD blob 9f0342101f5f861eab335d1a304cdca7fdf9ca31, git diff HEAD empty, git status --porcelain empty. (3) PROBE unchanged: the diff's three hunks are all in Instrument 1 and runControls, so no probe code moved. probeEarlyReturn imported from the BASE module and from the patched one, run over the three handshake spellings in this tree -- scripts/invoked-as.mjs (a reached-verdict flag), scripts/js-comment-mask.mjs (!== SELF_TEST_VERDICT), scripts/check-nul-bytes.mjs (the same inside an else-if dispatch) -- returned identical rows, HELD in all six runs. A full --probe sweep over 170 files was NOT run locally: DECLARED NARROWING, CI runs the farm. (4) GATES at 9d98ef60a, one hold of the shared verify lock (scripts/pm/os-verify-lock.sh, slot issue-15339-dev): the derived family (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -- 20 commands, every one run) + the always-runs tail (check:pm-dispatch-gates, check-self-test-workflow-commands.mjs and --self-test, check:declared-population-live, check-self-test-wired.mjs, check:ratchet-remedy-authority, check:nul-bytes, check:watch-hint-literal) + whole-repo pnpm lint = 29 of 29 exit 0. Wrapper's own line: 'VERDICT command-exit 0 - held the lock 643s (10m43s) - waited 375s (6m15s)'. Quoted gate verdicts: '+ dispatch-gates self-test: 1392 cases pass.'; 'check:declared-population-live -- 201 of 252 famil(ies) declare a path population, and every one of them reaches this tree's 7474 tracked file(s).'; 'check-nul-bytes: OK (scanned 7467 text file(s) ... no raw ASCII control bytes).'; 'OK check-ratchet-remedy-authority: 202 scripts swept ...'; pnpm lint (eslint . --no-inline-config) printed no findings. The same 28-command set had already come back 29/29 green at 9ccd3cdc4 before the comment-only second commit; the union above is the one at the FINAL head. (5) The file has no separate vitest suite -- its controls are the inline route recorded by check-self-test-wired.mjs, and they run on every invocation above. Manual control-byte scan of the edited file (grep -naP over the ASCII control range) found none. No exit-3 / NOT MEASURED readings in this run.", "mcp_calls": "0 -- the REST probe succeeded (repo-scoped issue read HTTP 200), so every GitHub read and write went through repo-scoped REST; no MCP GitHub call was made", "open_questions": [ { "question": "Should the TERNARY half also accept a failing arm that is a non-literal, e.g. process.exit(ok ? 0 : exitCode)?", "options": [ "A. Leave it out, as shipped: only a non-zero literal in the failing arm counts, so the criterion stays readable from source text alone.", "B. Accept any non-zero-looking identifier in the failing arm, raising recall further.", "C. Accept it only when the identifier is assigned a literal non-zero elsewhere in the file (a mini dataflow read)." ], "recommendation": "A. B costs the boundary this instrument is built on -- an opaque expression is exactly the accident shape, and 'probably non-zero' is the reasoning that scored a silent exit as a handshake. C buys a little recall for a parser this tool deliberately does not have; the header already states the error runs in one direction (NONE is an upper bound, a candidate to READ), which is the sanctioned way to absorb the remaining recall gap." }, { "question": "The TERNARY pattern bounds the condition at 200 non-semicolon characters, so a ternary exit wrapped over many lines with a very long condition would still read NONE. Worth a wider bound?", "options": [ "A. Keep 200 (no carrier in this tree is anywhere near it; a wider window raises the chance of pairing an unrelated ternary with an earlier process.exit).", "B. Widen to 400, matching ROSTER_COMPARISON's window.", "C. Anchor on the statement instead of a character budget (requires a real parse)." ], "recommendation": "A for now. If a real carrier is ever found beyond the bound the fix is one number and a control; the cost of widening speculatively is a false ROSTER, which is the direction this instrument must never err in." } ], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 4, 2026
ACCEPT → PR #15413 (head
9d98ef60a)Reviewed on the branch (
origin/main...claude/issue-15339-produces-failure-ternary, two commits, one file +85/−1):PRODUCES_FAILURE, now published as NAMED (the five literal spellings, unchanged) + TERNARY (process.exit(COND ? 0 : 1),? 0 : Nwith N ≠ 0, and the inverted? 1 : 0); a bareprocess.exit(EXPR)stays unrecognised with the reason written down (after an early return it isprocess.exit(undefined)= exit 0, the accident shape). Seven inline controls on every invocation, incl. a dedicatedTERNARY_EXIT_GATEfixture (theACCIDENTfixture carriesfailures.push, so it could not isolate the ternary — reused as the pin instead, as the dispatch allowed).--jsonexit 0 with the inline controls passing; tallies unchanged 163 / 6 / 1 (170 rows). Ablation reproduced by this seat:7706a50bd:scripts/check-regen-pending.mjs(sound roster + ternary exit) written into the worktree → the branch recogniser reads ROSTER; restored to a clean tree. The dev's base reading (NONE on5b2ad1b41, census 162/7/1 → 163/6/1 over the same bytes) stands as the other half.--probesweep — CI runs it.9d98ef60a: 15 success / 11 skipped / 6 in progress, 0 red;skip-changeset+size/s;Fixes #15339in the PR body.Open questions ruled: Q1 = A (a non-literal failing arm stays unrecognised — "probably non-zero" is the reasoning that scored a silent exit as a handshake; NONE is an upper bound to READ). Q2 = A (keep the 200-char bound; widen only on a measured carrier).
Flipping ready + arming auto-merge over MCP now; on merge the card closes via
Fixes, this seat stripspm:dispatched+ assignee, then dispatches #15391 (next serial edit of this file).Generated by Claude Code
claude commentedon Sep 4, 2026
LANDED — PR #15413 merged 15:39:01Z (card closed via
Fixes)Probed on re-fetched
main(6ca9f93e6): the TERNARY recogniser is in the instrument (11 mentions);--jsontallies unchanged.pm:dispatched+ assignee stripped. #15391 (probe HELD on a failed baseline) is dispatched next on the same file.Generated by Claude Code
fix(scripts): measure-self-test-floor recognises a ternary process.ex…