Skip to content

import-prerequisite.mjs — the frame 20+ gates inherit — prints PREREQUISITE NOT MET and exits 1, while three sibling gates answer the same words with 3 #13983

Closed

Description

@claude

Found while landing #13885 (PR #13982), which moved scripts/check-type-check-coverage.mjs's eleven refusals from exit 1 to exit 3. Filed rather than folded in: it is a different file, a different gate family, and — unlike that card — the current behaviour here is deliberate and argued in the source, so it is a decision to re-open rather than a defect to repair.

Measured at 92d273fe8

scripts/import-prerequisite.mjs is the shared frame for "can this gate load the package it imports?". Twenty-plus root gates import requireDependency / requireDefaultExport from it. It prints a PREREQUISITE NOT MET banner whose closing paragraph every importer inherits verbatim — and it exits 1:

export function reportPrerequisiteNotMet(importerUrl, verdict, measures) {
  console.error(prerequisiteNotMetText(importerUrl, verdict, measures));
  process.exit(1);
}

and the inherited advisory says so in words:

  (Exit code 1 — capture it BEFORE any pipe:

The reasoning is stated at requireDependency, and it is not a throwaway:

Exits 1, the code every real verdict uses: any wrapper treating non-zero as failure keeps behaving identically, and a second failure code would be a new contract nobody asked for. The reading a caller MUST be able to make is not in the code — it is in the printed text, which says nothing was measured.

Why it is worth re-deciding rather than leaving

Three other gates in the same lint family answer the same words with 3, and one of them says the split exists precisely because prose is not what an exit-code reader reads:

So the repo now says PREREQUISITE NOT MET in two exit codes, and the one that says 1 is the one inherited by the most gates. A reader or a gate-reconciling agent that learns "3 means nothing was measured" from three gates will read the twenty-plus that use this frame exactly backwards.

The counter-argument in the source is real and should be weighed, not brushed aside: no consumer today distinguishes 1 from 3 (every CI step treats any non-zero as failure), so the change buys nothing mechanical and costs a contract change. The 2026-08-07 meta-criterion — one operation with two implementations and inconsistent behaviour, the governed side wins — points the other way. That is a judgement, which is why this is a card and not a patch.

Deliberately not claimed

Generated by Claude Code


Generated by Claude Code

Activity

  1. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 31, 2026
  2. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    Claim — domain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Branch claude/issue-13983-prerequisite-exit-code.

    Triage's two pre-work items are carried into Zone 1 as binding, ⛔ not as advice:

    1. ⭐⭐ The exit code and the printed (Exit code 1 — …) advisory change in ONE stroke. Changing only the number leaves 20+ gates inheriting a false advisory — strictly worse than the status quo.
    2. Measure whether any consumer keys on 1 today before editing. If one does ⇒ report a fork and stop; that is a complete delivery on this card, ⛔ not a failure.

    ⚠️ And the delivery must answer the counter-argument written in the source ("a new contract nobody asked for") rather than skip it — while conceding its true half: there is zero mechanical CI benefit today, because every consumer treats any non-zero as failure. The case rests on the reading a human or a gate-reconciling agent makes, ⛔ not on a wrapper changing behaviour.

    Hot-file status: scripts/import-prerequisite.mjs is free. #13885's sibling change landed in a different file (PR #13982, merged) ⇒ no conflict, as triage noted.


    Generated by Claude Code

  3. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 13983,
      "status": "done",
      "branch": "claude/issue-13983-prerequisite-exit-code",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14009",
      "premise_still_valid": true,
      "summary": "The shared PREREQUISITE NOT MET frame scripts/import-prerequisite.mjs now refuses with EXIT_PREREQUISITE_NOT_MET = 3, and the advisory its importers print verbatim moved in the SAME stroke to '(Exit code 3, distinct from a finding's 1 - capture it BEFORE any pipe:' - PR #13982's exact phrasing, so the four files read alike. The doc comment that argued for 1 is replaced by the argument for 3 with the true half of the counter-argument kept, and the self-test went 41 -> 49 cases. The card's premise held on origin/main in every particular except one count: the frame is imported by 45 gates, not '20+' (42 under scripts/**, 3 under packages/lint/scripts/**), and the '3 gates to 1' contract is really SIX sites to one - check-test-completeness, check-dual-build-cjs-loads, check-type-check-coverage, pm/check-half-states (which exports the constant), pm/ci-failure and pm/check-clause2-carriers (which import it rather than re-pick) - and it is read by NUMBER, not merely declared: half-state-patrol.yml branches on exitCode === 3. Zone 1.3 measured NO consumer keyed on 1, so no fork. Scope held to the prerequisite branch: the refusal, its wording, every threshold and a real verdict's exit 1 are untouched; diff is one file, 106 insertions / 8 deletions. Zero mechanical CI benefit is conceded in the source comment, the commit message and the PR body rather than glossed. skip-changeset applied additively by POST (scripts/** publishes nothing, root is private); read-back shows ['size/m','skip-changeset'] - the size-labeler's later write did not strip it. NOTE for the PM: #13826 is open, unassigned and against THIS SAME FILE (the printed 'node scripts/NAME.mjs' command is wrong for the three packages/lint importers); this diff does not touch that line, but whoever takes it should rebase on PR #14009.",
      "tests": "Union run at 9370edc74 = this branch's head (no commit after it). (1) DERIVED FAMILIES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, stderr confirms 'commit 9370edc74' and '--repo ... holds'; 14 families, comm -23 (derived minus run) EMPTY. 12 green (exit 0): check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, node scripts/check-ci-filter-parity.mjs, node scripts/check-cross-package-test-inputs.mjs, node scripts/check-shard-attestation.mjs, and both @objectstack/lint families (green only after building @objectstack/formula and @objectstack/lint under scripts/pm/os-verify-lock.sh; both VERDICT lines read 'command-exit 0'). 1 NOT MEASURED: node scripts/check-test-completeness.mjs exits 3 - its own printed refusal says that is the local reading with no turbo log and 'it is not a red, and there is nothing here to fix'. (2) REPO-WIDE, not narrowed: pnpm lint (eslint . --no-inline-config) through the lock, VERDICT 'command-exit 0 - held the lock 108s'. pnpm check:nul-bytes OK over 7666 tracked files; grep -naP control-byte self-scan on the edited file clean. (3) THE SCRIPT'S OWN SUITE: node scripts/import-prerequisite.mjs --self-test = 49/49 pass (was 41). No vitest suite names this file (git grep over *.test.ts empty), and no workflow or package.json runs its --self-test at all - filed as #14007. (4) PROPAGATION (hypothesis C, run not inferred): in a worktree with NO node_modules, 11 importing gates run with the prerequisite genuinely unmet, exit captured with no pipe (cmd > log 2>&1; echo EXIT=$?): all 6 sampled BEFORE the edit at c0770d0b7 gave EXIT=1; all 11 AFTER gave EXIT=3 (check-doc-frontmatter, check-ci-filter-parity, check-engine-double-contract, check-required-contexts, check-aggregator-roster, check-doc-anchors, check-override-consistency, check-route-envelope, ts-parse, packages/lint check-reference-carrier-shape, packages/lint check-doc-security-posture), and the printed line changed from '(Exit code 1 - capture it BEFORE any pipe:' to '(Exit code 3, distinct from a finding's 1 - ...' in the same runs. The workspace-unbuilt and self-reference branches were exercised live too (check-doc-formula-expressions refused 3 for @objectstack/formula, then for @objectstack/lint, then passed). PASS-THROUGH: all 45 importers on the installed tree - 43 exit 0; check-published-readme-exports exits 1 on its own '160 packages are not built' report; measure-stall-guard-headroom exits 2 on its usage banner; check-query-options-erasure-ratchet hit a local 180s cap and is NOT MEASURED. None of the three reaches the changed branch. (5) ABLATION / reverse verification, direction predicted before running = TURNS RED. NO BUILD LEG EXISTS and that is stated rather than skipped: the subject is a plain .mjs script executed from source with no dist/ and no bundler, so there is no rebuild between mutation and reading and no ablation-dist-preflight to run - node reads the mutated bytes directly. Mutation confirmed ON DISK before each reading, by anchored counts plus the blob hash, not by an editor's exit code: mutation A injected 'process.exit(1);' at the refusal (grep count 1, and 'process.exit(EXIT_PREREQUISITE_NOT_MET)' count 0) and the file hashed 5c8fad30 vs HEAD's 05668e5b; self-test went EXIT=1, '1 of 49 case(s) failed - the refusal exits through the named constant, never a literal'. Mutation B put a literal back in the advisory; self-test EXIT=1, 3 of 49 failed, including 'the advisory carries NO stale spelling of the old code'. Both restore legs used git checkout HEAD -- (absolute path, trap on EXIT INT TERM) and were PROVEN restored: blob 05668e5b == the HEAD blob and git diff HEAD empty, checked after each leg and again at the end. Control leg at HEAD: exit 0, 49/49. (6) The self-test's own two NEGATIVE CONTROLS make the new pins falsifiable: each predicate is also run against a never-called function that does the forbidden thing and must see it - the shape PR #13982 used. The frame DOES have a --self-test, so hypothesis D's 'say so plainly if it has none' did not apply.",
      "mcp_calls": "0 - every GitHub read and write went through repo-scoped REST with curl (the probe GET /issues/13983 returned 200 on this seat). /search/issues is refused for this session ('sessions are bound to their configured repositories'), so the pre-filing dedupe used a bounded REST listing of the 410 open issues (5 pages, per_page=100) plus a local regex grep, not a wide MCP search.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #14007: scripts/import-prerequisite.mjs ships a 49-case --self-test that no workflow and no package.json script runs - the pin for a paragraph 45 gates print. check-self-test-wired is green and correct (its population is scripts CI RUNS; this is a module gates IMPORT; 174 scripts carry the flag, 159 are CI-run). Pre-existing, not caused by this PR.",
        "filed as #14008: three PREREQUISITE NOT MET sites still exit 1 after this card - check-i18n-coverage.mjs (3 refusal paths), check-i18n-bundles.mjs, packages/cli/scripts/check-app-nav-i18n.mjs. They refuse through cli-build-prerequisite.mjs's vocabulary but own their exits, and moving them also falsifies .claude/skills/pm-dispatch/references/lanes/services.md line 22 ('check:i18n ... 退 1 不是漂移') - a governed surface, so it forks the PR. Neighbour of the open #13825, which covers the same banner's wording.",
        "not filed, reported here: #13826 (open, unassigned) is against the same file this PR edits - the printed 'node scripts/NAME.mjs' command is wrong for the three packages/lint importers. Untouched by this diff, but it now needs a rebase on PR #14009."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  4. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions