Skip to content

measure-self-test-floor: PRODUCES_FAILURE does not recognise a ternary process.exit(cond ? 0 : 1), so a sound roster floor classifies NONE #15339

Closed

Description

@baozhoutao

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, or ok(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_FAILURE with the ternary shape (something matching process.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

  1. added
    domain:devxDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    on Sep 4, 2026
  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 4, 2026
  3. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk, dispatching to an os-dev subagent on branch claude/issue-15339-produces-failure-ternary (S). Serial-queued behind #15324 on scripts/measure-self-test-floor.mjs; #15372 merged 14:00:25Z, main at 5b2ad1b41. Assignee field was empty at the read immediately before this write.


    Generated by Claude Code

  4. claude commented on Sep 4, 2026

    @claude
    Contributor

    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

  5. claude commented on Sep 4, 2026

    @claude
    Contributor

    ACCEPT → PR #15413 (head 9d98ef60a)

    Reviewed on the branch (origin/main...claude/issue-15339-produces-failure-ternary, two commits, one file +85/−1):

    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 strips pm:dispatched + assignee, then dispatches #15391 (next serial edit of this file).


    Generated by Claude Code

  6. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 4, 2026
  7. claude commented on Sep 4, 2026

    @claude
    Contributor

    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); --json tallies unchanged. pm:dispatched + assignee stripped. #15391 (probe HELD on a failed baseline) is dispatched next on the same file.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxDomain lane — seat card indexed by label:pm:seattoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions