1010import { writeFileSync } from "node:fs" ;
1111import { fileURLToPath } from "node:url" ;
1212import { dirname , join } from "node:path" ;
13- import { runFastCheckLane , resolveModules } from "./fast-check-lane.mjs" ;
13+ import { selectLane } from "./lanes/index.mjs" ;
14+ import { fastCheckSeed } from "./lanes/fast-check.mjs" ;
1415import { buildReport , decideVerdict , summarize } from "../core/report.mjs" ;
1516import { changedFiles , seedFromHead , repoHead } from "../core/diff.mjs" ;
1617import { runAuthorMode , summarizeAuthor } from "./author-mode.mjs" ;
@@ -19,37 +20,79 @@ const HERE = dirname(fileURLToPath(import.meta.url));
1920
2021function parseArgs ( argv ) {
2122 const a = { numRuns : 500 , minRuns : 100 } ;
23+ const fail = ( msg ) => {
24+ console . error ( `einsiedlerkrebs: ${ msg } ` ) ;
25+ process . exit ( 64 ) ; // EX_USAGE
26+ } ;
2227 for ( let i = 0 ; i < argv . length ; i ++ ) {
2328 const k = argv [ i ] ;
24- const next = ( ) => argv [ ++ i ] ;
25- if ( k === "--repo" ) a . repo = next ( ) ;
26- else if ( k === "--modules" ) a . modules = next ( ) ;
27- else if ( k === "--base" ) a . base = next ( ) ;
28- else if ( k === "--seed" ) a . seed = parseInt ( next ( ) , 10 ) ;
29- else if ( k === "--num-runs" ) a . numRuns = parseInt ( next ( ) , 10 ) ;
30- else if ( k === "--min-runs" ) a . minRuns = parseInt ( next ( ) , 10 ) ;
29+ // Value getter for flags that take an argument. Fail closed on a MISSING value or one that
30+ // looks like the next flag (e.g. a dangling `--lane`, or `--lane --validate`): a swallowed
31+ // value must never silently fall back to a default and hand back a green that skipped it.
32+ // A leading-dash value that is a negative number (e.g. a seed "-5") is still allowed.
33+ const val = ( ) => {
34+ const v = argv [ i + 1 ] ;
35+ if ( v == null || ( v . length > 1 && v . startsWith ( "-" ) && ! / ^ - \d / . test ( v ) ) ) fail ( `${ k } requires a value` ) ;
36+ i ++ ;
37+ return v ;
38+ } ;
39+ if ( k === "--repo" ) a . repo = val ( ) ;
40+ else if ( k === "--modules" ) a . modules = val ( ) ;
41+ else if ( k === "--base" ) a . base = val ( ) ;
42+ // Keep the seed as an OPAQUE string reproduction token — do NOT parseInt it here, which would
43+ // corrupt a non-numeric or numeric-prefixed token. Each lane interprets it (fast-check parses
44+ // an int, the hypothesis runner hashes the string), so it must reach them intact.
45+ else if ( k === "--seed" ) a . seed = val ( ) ;
46+ else if ( k === "--num-runs" ) a . numRuns = parseInt ( val ( ) , 10 ) ;
47+ else if ( k === "--min-runs" ) a . minRuns = parseInt ( val ( ) , 10 ) ;
3148 else if ( k === "--allow-empty" ) a . allowEmpty = true ;
32- else if ( k === "--report-file" ) a . reportFile = next ( ) ;
49+ else if ( k === "--report-file" ) a . reportFile = val ( ) ;
3350 else if ( k === "--validate" ) a . validate = true ;
3451 else if ( k === "--author" ) a . author = true ;
35- else if ( k === "--engine" ) a . engine = next ( ) ;
36- else if ( k === "--out-dir" ) a . outDir = next ( ) ;
52+ else if ( k === "--engine" ) a . engine = val ( ) ;
53+ else if ( k === "--out-dir" ) a . outDir = val ( ) ;
54+ else if ( k === "--lane" ) a . lane = val ( ) ;
3755 else if ( k === "--no-keep-authored" ) a . keepAuthored = false ;
56+ // Strict: an unrecognized token (a typo like `--lan`) must fail loudly, never be ignored into
57+ // a default run that could exit 0 having skipped what the user asked for.
58+ else fail ( `unrecognized argument '${ k } '` ) ;
3859 }
3960 return a ;
4061}
4162
4263async function main ( ) {
4364 const a = parseArgs ( process . argv . slice ( 2 ) ) ;
4465
45- // validate-provider: the lane MUST report the planted-false canary as FALSIFIED.
66+ // validate-provider: the selected lane MUST report its planted-false canary as FALSIFIED.
67+ // An absent runtime/lib is QUARANTINED (fail-closed) — never a silent pass.
4668 if ( a . validate ) {
47- const canary = resolveModules ( join ( HERE , ".." , "fixtures" , "canary" ) ) ;
48- const results = await runFastCheckLane ( { modules : canary , seed : 1 , numRuns : 100 , minRuns : 1 } ) ;
69+ let lane ;
70+ try {
71+ // Honor auto-detection: `--validate --modules <python-dir>` should validate the lane those
72+ // modules belong to (e.g. hypothesis), not silently default to fast-check and certify the
73+ // wrong engine. Pass both the explicit --lane and --modules, like the normal run path.
74+ lane = selectLane ( { lane : a . lane , modules : a . modules } ) ;
75+ } catch ( e ) {
76+ // An unknown --lane is a usage error here too (exit 64), same as the normal run path — not
77+ // the top-level fatal (70). Fail loudly rather than through a confusing generic crash.
78+ console . error ( `einsiedlerkrebs: ${ e . message } ` ) ;
79+ process . exit ( 64 ) ;
80+ }
81+ const probe = await lane . probe ( ) ;
82+ if ( ! probe . available ) {
83+ const report = buildReport ( { tool : "einsiedlerkrebs" , engine : lane . id , lane : lane . id , repo : "<canary>" , results : [ ] , engineValidated : false } ) ;
84+ const decision = { verdict : "QUARANTINED" , code : 5 } ;
85+ console . log ( summarize ( report , decision ) ) ;
86+ console . log ( ` (lane '${ lane . id } ' runtime unavailable: ${ probe . detail ?? "?" } )` ) ;
87+ process . exit ( decision . code ) ;
88+ }
89+ const canary = lane . resolveModules ( lane . canaryDir ) ;
90+ const results = await lane . run ( { modules : canary , seed : "1" , numRuns : 100 , minRuns : 1 } ) ;
4991 const caught = results . length > 0 && results . every ( ( r ) => r . status === "FALSIFIED" ) ;
50- const report = buildReport ( { tool : "einsiedlerkrebs" , engine : "fast-check" , lane : "fast-check" , repo : "<canary>" , results, engineValidated : caught } ) ;
92+ const report = buildReport ( { tool : "einsiedlerkrebs" , engine : lane . id , lane : lane . id , repo : "<canary>" , results, engineValidated : caught } ) ;
5193 const decision = caught ? { verdict : "ENGINE_OK" , code : 0 } : { verdict : "QUARANTINED" , code : 5 } ;
5294 console . log ( summarize ( report , decision ) ) ;
95+ console . log ( ` (lane=${ lane . id } ${ probe . detail ?? "" } )` ) ;
5396 process . exit ( decision . code ) ;
5497 }
5598
@@ -60,7 +103,17 @@ async function main() {
60103 console . error ( "einsiedlerkrebs --author: --repo <path> and --base <ref> are both required" ) ;
61104 process . exit ( 64 ) ;
62105 }
63- const authorSeed = a . seed ?? seedFromHead ( a . repo , 42 ) ;
106+ // Author mode generates fast-check `*.eks.mjs` invariants — it only supports the fast-check lane.
107+ // Reject `--author --lane <other>` loudly (EX_USAGE) rather than silently authoring for the wrong
108+ // engine and "succeeding" against a lane the user did not ask for.
109+ if ( a . lane && a . lane !== "fast-check" ) {
110+ console . error ( `einsiedlerkrebs --author: only the fast-check lane is supported (got --lane ${ a . lane } )` ) ;
111+ process . exit ( 64 ) ;
112+ }
113+ // Author mode drives fast-check directly (runFastCheckLane), which wants an integer seed — so
114+ // convert the opaque token the same way the fast-check lane does, or a non-numeric --seed would
115+ // reach the engine unconverted and fail or replay differently.
116+ const authorSeed = fastCheckSeed ( a . seed ?? seedFromHead ( a . repo , 42 ) ) ;
64117 const authored = await runAuthorMode ( {
65118 repo : a . repo ,
66119 base : a . base ,
@@ -104,12 +157,20 @@ async function main() {
104157 let changed = null ;
105158 if ( a . repo && a . base ) changed = changedFiles ( a . repo , a . base ) ;
106159
107- const modules = resolveModules ( a . modules ) ;
108- const results = await runFastCheckLane ( { modules, seed, numRuns : a . numRuns , minRuns : a . minRuns } ) ;
160+ // Select the lane (explicit --lane, else auto-detect from the module extension).
161+ let lane ;
162+ try {
163+ lane = selectLane ( { lane : a . lane , modules : a . modules } ) ;
164+ } catch ( e ) {
165+ console . error ( `einsiedlerkrebs: ${ e . message } ` ) ;
166+ process . exit ( 64 ) ;
167+ }
168+ const modules = lane . resolveModules ( a . modules ) ;
169+ const results = await lane . run ( { modules, seed : String ( seed ) , numRuns : a . numRuns , minRuns : a . minRuns } ) ;
109170 const report = buildReport ( {
110171 tool : "einsiedlerkrebs" ,
111- engine : "fast-check" ,
112- lane : "fast-check" ,
172+ engine : lane . id ,
173+ lane : lane . id ,
113174 repo : a . repo ?? "(cwd)" ,
114175 base : a . base ,
115176 sha,
0 commit comments