()
| 71 | case '--seed': |
| 72 | args.seed = Number(argv[++i]); |
| 73 | break; |
| 74 | case '--check': |
| 75 | args.check = true; |
| 76 | break; |
| 77 | default: |
| 78 | console.error(`Unknown argument: ${argv[i]}`); |
| 79 | process.exit(2); |
| 80 | } |
| 81 | } |
| 82 | return args; |
| 83 | } |
| 84 | |
| 85 | /** Read the committed report's Stage-1 failing-entry id set. */ |
| 86 | function committedFailureIds(): Set<string> { |
| 87 | if (!fs.existsSync(REPORT_PATH)) |
| 88 | throw new Error( |
| 89 | `No committed report at ${REPORT_PATH}; run without --check to generate one.` |
| 90 | ); |
| 91 | const report = JSON.parse(fs.readFileSync(REPORT_PATH, 'utf8')); |
| 92 | const failures: { id: string }[] = report?.stage1?.failures ?? []; |
| 93 | return new Set(failures.map((f) => f.id)); |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Drift gate (`--check`): compare the fresh Stage-1 failure set against the |
| 98 | * committed report WITHOUT rewriting it. This is the hard CI gate that the |
| 99 | * old soft pass-rate threshold (PASS_RATE_GATE) let a multi-entry drift slip |
| 100 | * under. Exit codes: |
| 101 | * 1 newly-failing entries (regressions) — investigate and fix. |
| 102 | * 1 only newly-passing entries — the committed report is stale; regenerate |
| 103 | * it (`npx tsx scripts/fungrim/validate.ts --corpus data/fungrim`) and |
| 104 | * commit. Newly-passing is never a correctness problem, but it is failed |
| 105 | * (not merely warned) on purpose: a soft warning is exactly what allowed |
| 106 | * the baseline to silently drift out of date. The remedy is mechanical. |
| 107 | * 0 fresh failure set matches the committed report exactly. |
| 108 | * The `--check` run itself never writes the report, so CI stays clean. |
| 109 | */ |
| 110 | function runCheckGate(freshFailureIds: Set<string>): never { |
| 111 | const committed = committedFailureIds(); |
| 112 | const newlyFailing = [...freshFailureIds].filter((id) => !committed.has(id)).sort(); |
| 113 | const newlyPassing = [...committed].filter((id) => !freshFailureIds.has(id)).sort(); |
| 114 | |
| 115 | if (newlyFailing.length === 0 && newlyPassing.length === 0) { |
| 116 | console.log( |
| 117 | `\n[--check] OK — Stage-1 failure set matches the committed report ` + |
| 118 | `(${freshFailureIds.size} failing).` |
| 119 | ); |
| 120 | process.exit(0); |
| 121 | } |
| 122 | if (newlyFailing.length > 0) { |
| 123 | console.error( |
| 124 | `\n[--check] REGRESSION — ${newlyFailing.length} entr` + |
| 125 | `${newlyFailing.length === 1 ? 'y' : 'ies'} newly failing Stage 1:` |
no test coverage detected