@afokapu/atdd-bun 0.10.58 → 0.10.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +258 -36
  2. package/detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs +6 -2
  3. package/detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs +10 -1
  4. package/detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs +11 -10
  5. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/e2e/smoke/resolve-match.smoke.test.ts +8 -0
  6. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/plan/_trains/_interlockings/match.yaml +9 -0
  7. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/server.ts +10 -0
  8. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/interlocking.ts +12 -0
  9. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/journey.ts +8 -0
  10. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/runner.ts +6 -0
  11. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/scenario-compilation/compile.routes.test.ts +14 -0
  12. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/session-replay/replay.routes.test.ts +14 -0
  13. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/source-corpus-admission/refuse.routes.test.ts +14 -0
  14. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/smoke/runtime-station-master.test.ts +44 -0
  15. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/support/production-train.ts +8 -0
  16. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/create-world-pack.yaml +18 -0
  17. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/play-world-scenario.yaml +24 -0
  18. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/replay-world-session.yaml +18 -0
  19. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/scenario-compilation.yaml +14 -0
  20. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/session-replay.yaml +14 -0
  21. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/source-corpus-admission.yaml +14 -0
  22. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/server.ts +85 -0
  23. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/interlocking.ts +64 -0
  24. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/journey.ts +71 -0
  25. package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/runner.ts +28 -0
  26. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/e2e/smoke/resolve-match.smoke.test.ts +8 -0
  27. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/plan/_trains/_interlockings/match.yaml +9 -0
  28. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/server.ts +21 -0
  29. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/interlocking.ts +12 -0
  30. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/journey.ts +8 -0
  31. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/runner.ts +6 -0
  32. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/e2e/smoke/resolve-match.smoke.test.ts +8 -0
  33. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/plan/_trains/_interlockings/match.yaml +9 -0
  34. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/server.ts +11 -0
  35. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/interlocking.ts +12 -0
  36. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/journey.test.ts +8 -0
  37. package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/runner.ts +6 -0
  38. package/detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs +5 -0
  39. package/detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs +5 -1
  40. package/integrity.json +41 -7
  41. package/lib/journey-chain.mjs +677 -0
  42. package/package.json +3 -2
@@ -3,10 +3,12 @@
3
3
  // afokapu/atdd#1248 route space + #1251 runner call model). Each check under ../checks/*.mjs imports
4
4
  // this module and scans a consumer tree for ONE tester.bun.interlocking-* rule.
5
5
  //
6
- // ZERO third-party deps — node builtins only. The interlocking route space is stack-neutral planner
6
+ // Node builtins only, except lib/journey-chain.mjs, which proves the journey-mediated chain on the
7
+ // TypeScript compiler AST. The interlocking route space is stack-neutral planner
7
8
  // data (snake_case, plan/_trains/_interlockings/**); the e2e tests are Bun/TS under e2e/**.
8
9
 
9
10
  import { excludedPath } from "../../../lib/scan.mjs";
11
+ import { entryDelegatedAction, provenJourneyAction } from "../../../lib/journey-chain.mjs";
10
12
  import { existsSync, readFileSync, statSync, readdirSync, writeFileSync } from "node:fs";
11
13
  import { dirname, join, resolve, sep } from "node:path";
12
14
 
@@ -338,41 +340,239 @@ function productionReceivers(text) {
338
340
  // [{ resolution, execution }] means an actual resolution value feeds actual train execution.
339
341
  // `execution` is null for an unassigned execute result; route/runner proof may assert resolution,
340
342
  // while trace and sequence proof require the named execution result.
341
- export function hasResolvedProductionRunnerImports(text, file, croot) {
342
- const imported = new Set();
343
+ function resolvedLocalNamedImport(text, file, croot, symbol) {
344
+ if (!file || !croot) return null;
343
345
  const re = /^\s*import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/gm;
344
346
  for (const match of text.matchAll(re)) {
345
347
  if (!match[2].startsWith(".")) continue;
346
348
  const names = match[1].split(",").map(name => name.trim().split(/\s+as\s+/)[0]);
349
+ if (!names.includes(symbol)) continue;
347
350
  const target = resolve(dirname(file), match[2]);
348
351
  const candidates = [target, `${target}.ts`, `${target}.tsx`, `${target}.js`, `${target}.mjs`, join(target, "index.ts")];
349
352
  const source = candidates.find(candidate => existsSync(candidate) && candidate.startsWith(resolve(croot) + sep) && !/\.(test|spec)\.[cm]?[jt]sx?$/.test(candidate));
350
- if (!source) continue;
351
- for (const name of names) if (name === PROD_INTERLOCKING || name === PROD_TRAIN) imported.add(name);
353
+ if (source) return source;
354
+ }
355
+ return null;
356
+ }
357
+
358
+ export function hasResolvedProductionRunnerImports(text, file, croot) {
359
+ return Boolean(
360
+ resolvedLocalNamedImport(text, file, croot, PROD_INTERLOCKING) &&
361
+ resolvedLocalNamedImport(text, file, croot, PROD_TRAIN),
362
+ );
363
+ }
364
+
365
+ // Index just past a string or comment starting at `i`, or `i` itself when `i` starts code.
366
+ function skipLiteral(text, i) {
367
+ const c = text[i], next = text[i + 1];
368
+ if (c === "/" && next === "/") { const end = text.indexOf("\n", i + 2); return end < 0 ? text.length : end; }
369
+ if (c === "/" && next === "*") { const end = text.indexOf("*/", i + 2); return end < 0 ? text.length : end + 2; }
370
+ if (c === "'" || c === '"' || c === "`") {
371
+ for (let j = i + 1; j < text.length; j++) {
372
+ if (text[j] === "\\") { j++; continue; }
373
+ if (text[j] === c) return j + 1;
374
+ }
375
+ return text.length;
376
+ }
377
+ return i;
378
+ }
379
+
380
+ // Index of the first bracket that closes a scope opened before `text` begins.
381
+ function enclosingEnd(text) {
382
+ let depth = 0;
383
+ for (let i = 0; i < text.length; i++) {
384
+ const skip = skipLiteral(text, i);
385
+ if (skip !== i) { i = skip - 1; continue; }
386
+ if ("([{".includes(text[i])) depth++;
387
+ else if (")]}".includes(text[i]) && --depth < 0) return i;
388
+ }
389
+ return text.length;
390
+ }
391
+
392
+ // A `return` statement ends at `;`, at the scope's closing bracket, or at a newline that cannot continue it.
393
+ function returnStatementEnd(text, start) {
394
+ let depth = 0;
395
+ for (let i = start + "return".length; i < text.length; i++) {
396
+ const skip = skipLiteral(text, i);
397
+ if (skip !== i) { i = skip - 1; continue; }
398
+ const c = text[i];
399
+ if ("([{".includes(c)) depth++;
400
+ else if (")]}".includes(c)) { if (--depth < 0) return i; }
401
+ else if (depth === 0 && c === ";") return i;
402
+ else if (depth === 0 && c === "\n") {
403
+ const head = text.slice(start + "return".length, i).trim();
404
+ const next = text.slice(i + 1).trimStart()[0] ?? "";
405
+ if (!head || (!/[=+\-*/%&|^?:,.!<>]$/.test(head) && !/^[.?:+\-*/%&|^,=<>)\]]/.test(next))) return i;
406
+ }
407
+ }
408
+ return text.length;
409
+ }
410
+
411
+ // True when a statement starting at `index` is the body of a braceless `if (…)`, `for (…)`, `while (…)`, or `else`.
412
+ function conditionalStatement(text, index) {
413
+ const before = maskComments(text.slice(0, index)).trimEnd();
414
+ if (/(?:^|[^\w$.])else$/.test(before)) return true;
415
+ if (!before.endsWith(")")) return false;
416
+ let depth = 0;
417
+ for (let i = before.length - 1; i >= 0; i--) {
418
+ if (before[i] === ")") depth++;
419
+ else if (before[i] === "(" && --depth === 0) return /(?:^|[^\w$.])(?:if|for|while)\s*$/.test(before.slice(0, i));
420
+ }
421
+ return false;
422
+ }
423
+
424
+ // [[start, end]] of the first `return` that always runs when control reaches it at the top level of `text`
425
+ // (or [] when none does); anything after it is unreachable. Nested blocks, callbacks, and braceless
426
+ // conditional bodies are excluded; `try`/`finally` bodies are not.
427
+ function topLevelReturns(text) {
428
+ const ranges = [], stack = [];
429
+ for (let i = 0; i < text.length; i++) {
430
+ const skip = skipLiteral(text, i);
431
+ if (skip !== i) { i = skip - 1; continue; }
432
+ const c = text[i];
433
+ if ("([{".includes(c)) stack.push(c === "{" && /(?:^|[^\w$.])(?:try|finally)\s*$/.test(text.slice(Math.max(0, i - 16), i)) ? "always" : c);
434
+ else if (")]}".includes(c)) { if (!stack.length) break; stack.pop(); }
435
+ else if (stack.every((entry) => entry === "always") && text.startsWith("return", i) &&
436
+ !/[\w$.]/.test(text[i - 1] ?? "") && !/[\w$]/.test(text[i + 6] ?? "") && !conditionalStatement(text, i)) {
437
+ ranges.push([i, returnStatementEnd(text, i)]);
438
+ break;
439
+ }
440
+ }
441
+ return ranges;
442
+ }
443
+
444
+ // Operators that make an operand conditional, discarded, or replaced by another value: comma, ternary,
445
+ // short-circuit logic, arrows, unary/binary arithmetic and comparison, and `void`/`typeof`/`delete`/
446
+ // `instanceof`/`in`. A non-null `!` may follow a value, so only a prefix `!` counts.
447
+ const PREFIX_REPLACING = /^(?:\?(?![.?])|\?\?|&&|\|\||=>|[,:!~+\-*/%<>=&|^])|^(?:void|typeof|delete|instanceof|in)\b/;
448
+ const SUFFIX_REPLACING = /^(?:\?(?![.?])|\?\?|&&|\|\||[,:~+\-*/%<>=&|^])|^(?:instanceof|in)\b/;
449
+ const replacing = (pattern, text, i) => !/[\w$]/.test(text[i - 1] ?? "") || !/^[a-z]/.test(text[i]) ? pattern.exec(text.slice(i, i + 10)) : null;
450
+
451
+ function returnsValueAt(text, start, end, index) {
452
+ const stack = [{ kind: "group", operator: false }];
453
+ for (let i = start + "return".length; i < index; i++) {
454
+ const skip = skipLiteral(text, i);
455
+ if (skip !== i) { i = skip - 1; continue; }
456
+ const c = text[i];
457
+ if (c === "(") {
458
+ const prev = text.slice(start, i);
459
+ const group = /(?:^|[^\w$])(?:return|await|typeof|void|yield|in|of|case)\s*$/.test(prev) || !/[\w$)\]]\s*$/.test(prev);
460
+ stack.push({ kind: group ? "group" : "call", operator: false });
461
+ } else if ("[{".includes(c)) stack.push({ kind: c, operator: false });
462
+ else if (")]}".includes(c)) stack.pop();
463
+ else {
464
+ const op = replacing(PREFIX_REPLACING, text, i);
465
+ if (op) { stack.at(-1).operator = true; i += op[0].length - 1; }
466
+ }
467
+ }
468
+ for (let level = stack.length - 1; level >= 0 && stack[level].kind === "group"; level--) if (stack[level].operator) return false;
469
+ let depth = 0;
470
+ for (let i = index; i < end; i++) {
471
+ const skip = skipLiteral(text, i);
472
+ if (skip !== i) { i = skip - 1; continue; }
473
+ const c = text[i];
474
+ if ("([{".includes(c)) depth++;
475
+ else if (")]}".includes(c)) {
476
+ if (depth > 0) depth--;
477
+ else { stack.pop(); if (!stack.length || stack.at(-1).kind !== "group") return true; }
478
+ } else if (depth === 0 && stack.at(-1).kind === "group" && replacing(SUFFIX_REPLACING, text, i)) return false;
479
+ }
480
+ return true;
481
+ }
482
+
483
+ // True when a match of `pattern` ending by `limit` is returned at the top level of `text`, inline or
484
+ // through a variable assigned from it.
485
+ function returnsMatch(text, pattern, limit = text.length) {
486
+ const ranges = topLevelReturns(text);
487
+ const returned = (index) => ranges.some(([start, end]) => index > start && index < end && returnsValueAt(text, start, end, index));
488
+ for (const m of text.matchAll(new RegExp(pattern, "g"))) {
489
+ if ((m.index ?? 0) + m[0].length <= limit && returned(m.index ?? 0)) return true;
352
490
  }
353
- return imported.has(PROD_INTERLOCKING) && imported.has(PROD_TRAIN);
491
+ const assigned = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${pattern}`, "g");
492
+ for (const m of text.matchAll(assigned)) {
493
+ const after = (m.index ?? 0) + m[0].length;
494
+ if (after > limit) continue;
495
+ for (const ref of text.matchAll(new RegExp(`(?<![\\w$.])${escaped(m[1])}(?![\\w$])`, "g"))) {
496
+ if ((ref.index ?? 0) >= after && returned(ref.index ?? 0)) return true;
497
+ }
498
+ }
499
+ return false;
500
+ }
501
+
502
+ // Index of the first use of `name` that could rebind or mutate it. Allowed: member-chain reads (not
503
+ // assigned or called), object keys, `void`/`console.*` arguments, the execute argument matched by
504
+ // `executeArgument`, and object/array values inside a returned expression. An object or array holding the
505
+ // resolution anywhere else is an alias whose later member writes are invisible here, so it ends the binding.
506
+ function firstRebinding(text, name, executeArgument, returns) {
507
+ for (const m of text.matchAll(new RegExp(`(?<![\\w$.])${escaped(name)}(?![\\w$])`, "g"))) {
508
+ const index = m.index ?? 0;
509
+ const before = text.slice(0, index), rest = text.slice(index + m[0].length);
510
+ const chain = /^(?:\s*\??\.\s*[A-Za-z_$][\w$]*|\s*\[[^\]]*\])+/.exec(rest);
511
+ if (chain) {
512
+ if (/^\s*(?:=(?![=>])|\+\+|--|(?:\*\*|[-+*/%&|^]|<<|>>>?|\?\?|&&|\|\|)=|\()/.test(rest.slice(chain[0].length))) return index;
513
+ continue;
514
+ }
515
+ if (new RegExp(`(?:${executeArgument})$`).test(before)) continue;
516
+ if (/[{,]\s*$/.test(before) && /^\s*:/.test(rest)) continue; // an object key, not a reference
517
+ if (/\bvoid\s+$/.test(before)) continue;
518
+ if (/\bconsole\s*\.\s*(?:log|info|warn|error|debug|trace)\s*\(\s*$/.test(before) && /^\s*\)/.test(rest)) continue;
519
+ if (/(?:[{,]\s*(?:[A-Za-z_$][\w$]*\s*:\s*)?|\[\s*)$/.test(before) && /^\s*[,}\]]/.test(rest) &&
520
+ returns.some(([start, end]) => index > start && index < end)) continue;
521
+ return index;
522
+ }
523
+ return text.length;
524
+ }
525
+
526
+ // `after` starts inside the `resolveTrain(` call that produced `resolution`. The binding holds only inside
527
+ // the block that declared the resolution, and only until the resolution is passed, aliased, assigned, or
528
+ // mutated anywhere other than the execution itself.
529
+ function resolutionBinding(after, resolution, executeArgument) {
530
+ const statement = after.slice(enclosingEnd(after) + 1);
531
+ const scope = statement.slice(0, enclosingEnd(statement));
532
+ return { scope, bound: scope.slice(0, firstRebinding(scope, resolution, executeArgument, topLevelReturns(scope))) };
533
+ }
534
+
535
+ function trainExecutionForms(resolution) {
536
+ const r = escaped(resolution);
537
+ const legacy = `new\\s+${PROD_TRAIN}\\s*\\(\\s*${r}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`;
538
+ // The declaration-driven runner form constructs from THIS resolution's trainPath (optionally followed
539
+ // by handler/context args) and executes that same resolution object.
540
+ const constructed = `new\\s+${PROD_TRAIN}\\s*\\(\\s*${r}\\.trainPath\\s*(?:,[^()]*)?\\)\\s*\\.execute\\s*\\(\\s*`;
541
+ return { legacy, constructed, declaration: `${constructed}${r}\\s*(?:,|\\))` };
542
+ }
543
+
544
+ function resolutionTrainExecution(after, resolution) {
545
+ const { legacy, constructed, declaration } = trainExecutionForms(resolution);
546
+ const { scope, bound } = resolutionBinding(after, resolution, constructed);
547
+ const assignment = `\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?`;
548
+ const assigned = new RegExp(`${assignment}${legacy}`).exec(after) ?? new RegExp(`${assignment}${declaration}`).exec(bound);
549
+ const returns = `\\breturn\\s+(?:await\\s+)?`;
550
+ return {
551
+ execution: assigned?.[1] ?? null,
552
+ found: Boolean(assigned || new RegExp(legacy).test(after) || new RegExp(declaration).test(bound)),
553
+ returned: new RegExp(`${returns}${legacy}`).test(after) || returnsMatch(scope, declaration, bound.length) ||
554
+ Boolean(assigned && new RegExp(`${returns}${escaped(assigned[1])}\\s*;?`).test(after)),
555
+ };
354
556
  }
355
557
 
356
558
  export function productionExecutionProofs(text, file, croot) {
357
559
  if (file && croot && !hasResolvedProductionRunnerImports(text, file, croot)) return [];
358
560
  const receivers = productionReceivers(text);
359
561
  const proofs = [];
360
- const resolutions = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*([^\\n]{0,240}?)\\.resolveTrain\\s*\\(`, "g");
562
+ const resolutions = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*([^=;{}\\n]{0,240}?)\\.resolveTrain\\s*\\(`, "g");
361
563
  for (const m of text.matchAll(resolutions)) {
362
564
  const [whole, resolution, receiver] = m;
363
565
  const direct = new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(receiver);
364
566
  const named = [...receivers].some((name) => receiver.trim().endsWith(name));
365
567
  if (!direct && !named) continue;
366
- const after = text.slice((m.index ?? 0) + whole.length);
367
- const execute = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).exec(after);
368
- const anonymous = new RegExp(`new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).test(after);
369
- if (execute || anonymous) proofs.push({ resolution, execution: execute?.[1] ?? null });
568
+ const proof = resolutionTrainExecution(text.slice((m.index ?? 0) + whole.length), resolution);
569
+ if (proof.found) proofs.push({ resolution, execution: proof.execution });
370
570
  }
371
571
  return proofs;
372
572
  }
373
573
 
374
574
  export function assertsExpression(text, expression, expected = null) {
375
- const pat = new RegExp(`\\bexpect\\s*\\(\\s*(?:await\\s+)?${expression}\\s*\\)\\s*\\.(?:to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|Match|BeTruthy)|not\\.to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|Match))\\b([\\s\\S]{0,220})`);
575
+ const pat = new RegExp(`\\bexpect\\s*\\(\\s*(?:await\\s+)?${expression}\\s*\\)\\s*\\.(?:to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|MatchObject|Match|BeTruthy)|not\\.to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|MatchObject|Match))\\b([\\s\\S]{0,220})`);
376
576
  const m = pat.exec(text);
377
577
  return Boolean(m && (!expected || tokenCovered(expected, m[0])));
378
578
  }
@@ -451,33 +651,55 @@ function exportedActionBody(text, name) {
451
651
  return null;
452
652
  }
453
653
 
454
- function exportedActionReturnsProductionExecution(stationText, action) {
455
- const body = exportedActionBody(stationText, action);
456
- if (!body) return false;
457
- const receivers = productionReceivers(stationText);
458
- const resolutions = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*([^\\n]{0,240}?)\\.resolveTrain\\s*\\(`, "g");
459
- for (const m of body.matchAll(resolutions)) {
460
- const [whole, resolution, receiver] = m;
461
- const direct = new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(receiver);
462
- const named = [...receivers].some((name) => receiver.trim().endsWith(name));
463
- if (!direct && !named) continue;
464
- const after = body.slice((m.index ?? 0) + whole.length);
465
- const directReturn = new RegExp(`\\breturn\\s+(?:await\\s+)?new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).test(after);
466
- if (directReturn) return true;
467
- const execution = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).exec(after)?.[1];
468
- if (execution && new RegExp(`\\breturn\\s+(?:await\\s+)?${escaped(execution)}\\s*;?`).test(after)) return true;
654
+ // As on main, the exported action itself must resolve on a recognized InterlockingRunner and
655
+ // return that resolution's execution; a discarded or module-local execution is not evidence.
656
+ function directProductionModuleExecution(body, moduleText = body) {
657
+ if (!new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(moduleText) || !new RegExp(`new\\s+${PROD_TRAIN}\\b`).test(moduleText)) return false;
658
+ const receivers = productionReceivers(moduleText);
659
+ const resolutions = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*([^=;{}\\n]{0,240}?)\\.resolveTrain\\s*\\(`, "g");
660
+ return [...body.matchAll(resolutions)].some(match => {
661
+ const recognized = new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(match[2]) ||
662
+ [...receivers].some((name) => match[2].trim().endsWith(name));
663
+ return recognized && resolutionTrainExecution(body.slice((match.index ?? 0) + match[0].length), match[1]).returned;
664
+ });
665
+ }
666
+
667
+ // HTTP/module entrypoints do not expose a StationMaster instance to the test. Their equivalent
668
+ // witness is an asserted result from the same exported action the test invokes. That action must
669
+ // either directly execute the selected train or return a JourneyRunner execution whose local
670
+ // InterlockingRunner then executes the same resolution's declaration path.
671
+ function stationModuleCall(text, action) {
672
+ return new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?(?:${ident}\\.)?(dispatch|handleAction|executeAction)\\s*\\(\\s*["']${escaped(action)}["']`).exec(text);
673
+ }
674
+
675
+ export function stationModuleExecutionProof(text, stationText, action, stationFile, croot) {
676
+ if (!stationText || !stationFile || !croot) return false;
677
+ const call = stationModuleCall(text, action);
678
+ if (call && assertsExpression(text, `${escaped(call[1])}(?:\\.[A-Za-z_$][\\w$]*)?`)) {
679
+ // The journey-mediated chain is proven on the TypeScript AST (lib/journey-chain.mjs).
680
+ if (provenJourneyAction(stationText, stationFile, croot, call[2])) return true;
681
+ const body = exportedActionBody(stationText, call[2]);
682
+ if (body && directProductionModuleExecution(body, stationText)) return true;
469
683
  }
470
- return false;
684
+ return stationEntryExecutionProof(text, stationText, action, stationFile, croot);
685
+ }
686
+
687
+ // An HTTP-shaped entry (e.g. `stationMaster(command)`) whose asserted result depends on its awaited
688
+ // call of a proven journey action with this literal action name.
689
+ function stationEntryExecutionProof(text, stationText, action, stationFile, croot) {
690
+ const calls = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?(${ident})\\s*\\(`, "g");
691
+ return [...text.matchAll(calls)].some(([, result, entry]) => {
692
+ const delegated = entryDelegatedAction(stationText, stationFile, entry);
693
+ return Boolean(delegated && delegated.action === action &&
694
+ assertsExpression(text, `${escaped(result)}(?:\\.[A-Za-z_$][\\w$]*)?`) &&
695
+ provenJourneyAction(stationText, stationFile, croot, delegated.callee));
696
+ });
471
697
  }
472
698
 
473
- // HTTP/module entrypoints do not expose a StationMaster instance to the test. The asserted result
474
- // must therefore come from the SAME exported action the test invokes, and that action's own return
475
- // path must resolve then execute the selected train. Module-wide runner tokens are not evidence.
476
- export function stationModuleExecutionProof(text, stationText, action) {
477
- if (!stationText) return false;
478
- const call = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*await\\s+(?:${ident}\\.)?(dispatch|handleAction|executeAction)\\s*\\(\\s*["']${escaped(action)}["']`).exec(text);
479
- return Boolean(call && assertsExpression(text, `${escaped(call[1])}(?:\\.[A-Za-z_$][\\w$]*)?`) &&
480
- exportedActionReturnsProductionExecution(stationText, call[2]));
699
+ export function routeHasStationModuleProof(route, text, action, stationText, stationFile, croot) {
700
+ const call = stationModuleCall(text, action);
701
+ return Boolean(call && stationModuleExecutionProof(text, stationText, action, stationFile, croot) &&
702
+ assertsExpression(text, `${escaped(call[1])}\\.selectedTrainId`, route.trainId));
481
703
  }
482
704
 
483
705
  // Bun's native `expect` vocabulary provides a compact mutation witness: exact ordered equality
@@ -39,7 +39,9 @@ for (const scanRoot of roots) {
39
39
  // A test that imports the Station Master drives the runners it composes: the call model's entry point
40
40
  // (FWS #sEW3F9b49iA7). It counts only when the Station Master module itself references both production runners;
41
41
  // substitutes are still caught by FORBIDDEN_PATTERNS above.
42
- const station = ["server.ts", join("src", "server.ts")].map((name) => readText(join(croot, name))).find((t) => t && tokenCovered(PROD_INTERLOCKING, maskComments(t)) && tokenCovered(PROD_TRAIN, maskComments(t)));
42
+ const station = ["server.ts", join("src", "server.ts")]
43
+ .map((name) => ({ file: join(croot, name), text: readText(join(croot, name)) }))
44
+ .find((entry) => entry.text);
43
45
  const drivesStationMaster = (text) => Boolean(station) && /\bfrom\s+["'][^"']*\bserver(?:\.ts)?["']|import\(\s*["'][^"']*\bserver(?:\.ts)?["']\s*\)/.test(text);
44
46
  const records = interlockingFiles(croot)
45
47
  .map((f) => parseInterlocking(readText(f)))
@@ -72,7 +74,9 @@ for (const scanRoot of roots) {
72
74
 
73
75
  const semantic = hasProductionExecutionProof(text, file, croot) ||
74
76
  records.some((rec) => rec.actions.some((action) => stationMasterExecutionProof(text, action))) ||
75
- (drivesStationMaster(text) && records.some((rec) => rec.actions.some((action) => stationModuleExecutionProof(text, station, action))));
77
+ (drivesStationMaster(text) && records.some((rec) => rec.actions.some((action) =>
78
+ stationModuleExecutionProof(text, station.text, action, station.file, croot),
79
+ )));
76
80
  if (!semantic) {
77
81
  violations.push(
78
82
  mk(
@@ -6,6 +6,7 @@
6
6
  // exercises it — referenced by its routeId or resolved trainId in an `e2e/**/*.ts` test. An admissible
7
7
  // route with no covering e2e test is a silent-green route-control branch. Bun mirror of core
8
8
  // tester.interlocking.route-coverage.
9
+ import { join } from "node:path";
9
10
  import {
10
11
  parseJsonEnv,
11
12
  readText,
@@ -14,6 +15,7 @@ import {
14
15
  e2eFiles,
15
16
  parseInterlocking,
16
17
  routeHasProductionProof,
18
+ routeHasStationModuleProof,
17
19
  maskComments,
18
20
  rel,
19
21
  mk,
@@ -31,10 +33,17 @@ for (const scanRoot of roots) {
31
33
  .map((f) => ({ file: f, rec: parseInterlocking(readText(f)) }))
32
34
  .filter((x) => x.rec);
33
35
  const e2eTexts = e2eFiles(croot).map((file) => ({ file, text: maskComments(readText(file)) }));
36
+ const station = ["server.ts", "src/server.ts"]
37
+ .map((name) => ({ file: join(croot, name), text: readText(join(croot, name)) }))
38
+ .find((entry) => entry.text);
34
39
 
35
40
  for (const { file, rec } of records) {
36
41
  for (const route of rec.routes) {
37
- if (e2eTexts.some(({ file, text }) => routeHasProductionProof(route, text, rec.interlockingId, file, croot))) continue;
42
+ const covered = e2eTexts.some(({ file: testFile, text }) =>
43
+ routeHasProductionProof(route, text, rec.interlockingId, testFile, croot) ||
44
+ (station && rec.actions.some((action) => routeHasStationModuleProof(route, text, action, station.text, station.file, croot))),
45
+ );
46
+ if (covered) continue;
38
47
  if (unbuiltWagons(croot, route.trainId).length) continue; // pending: a wagon on its train has no source yet
39
48
  const cat =
40
49
  route.category !== null
@@ -7,6 +7,7 @@
7
7
  // (exposed:false) interlockings have no Station Master action and are OUT OF SCOPE. Bun mirror of
8
8
  // core tester.interlocking.smoke-coverage-for-station-master. The enforced minimum smoke signature is
9
9
  // an `e2e/**/*.ts` file referencing the action name AND a Station Master reference AND both runners.
10
+ import { join } from "node:path";
10
11
  import {
11
12
  parseJsonEnv,
12
13
  readText,
@@ -26,7 +27,6 @@ import {
26
27
  mk,
27
28
  writeReport,
28
29
  } from "../_shared/interlocking.mjs";
29
- import { join } from "node:path";
30
30
 
31
31
  const RULE = "tester.bun.interlocking-smoke-coverage-for-station-master";
32
32
  const roots = parseJsonEnv("ATDD_SCAN_ROOTS", []);
@@ -36,11 +36,12 @@ const violations = [];
36
36
  // it under the smoke rules (no substituted collaborators, observable outcome). A local E2E that names the action beside
37
37
  // the runners is not a smoke and no longer clears the rule (FWS #TzSCGS5ajYhP).
38
38
  const SMOKE_PHASE = /^\s*\/\/\s*Phase:\s*SMOKE\b/m;
39
- function actionSmokeCovered(action, e2eFiles, stationModule) {
39
+ function actionSmokeCovered(action, e2eFiles, station, croot) {
40
40
  return e2eFiles.some(
41
- ({ raw, text: t }) =>
42
- SMOKE_PHASE.test(raw) && tokenCovered(action, t) && STATION_MASTER.test(t) && t.includes(PROD_INTERLOCKING) && t.includes(PROD_TRAIN) &&
43
- (stationMasterExecutionProof(t, action) || stationModuleExecutionProof(t, stationModule, action)),
41
+ ({ raw, text: t, file }) =>
42
+ SMOKE_PHASE.test(raw) && tokenCovered(action, t) && STATION_MASTER.test(t) &&
43
+ (stationMasterExecutionProof(t, action) ||
44
+ (station && stationModuleExecutionProof(t, station.text, action, station.file, croot))),
44
45
  );
45
46
  }
46
47
 
@@ -49,15 +50,15 @@ for (const scanRoot of roots) {
49
50
  const records = interlockingFiles(croot)
50
51
  .map((f) => ({ file: f, rec: parseInterlocking(readText(f)) }))
51
52
  .filter((x) => x.rec);
52
- const e2eTexts = e2eFiles(croot).map((f) => { const raw = readText(f); return { raw, text: maskComments(raw) }; });
53
- const stationModule = ["server.ts", join("src", "server.ts")]
54
- .map((name) => readText(join(croot, name)))
55
- .find((text) => text && text.includes(PROD_INTERLOCKING) && text.includes(PROD_TRAIN));
53
+ const e2eTexts = e2eFiles(croot).map((file) => { const raw = readText(file); return { file, raw, text: maskComments(raw) }; });
54
+ const station = ["server.ts", "src/server.ts"]
55
+ .map((name) => ({ file: join(croot, name), text: readText(join(croot, name)) }))
56
+ .find((entry) => entry.text);
56
57
 
57
58
  for (const { file, rec } of records) {
58
59
  if (!rec.exposed) continue;
59
60
  for (const action of rec.actions) {
60
- if (actionSmokeCovered(action, e2eTexts, stationModule)) continue;
61
+ if (actionSmokeCovered(action, e2eTexts, station, croot)) continue;
61
62
  const [line, src] = lineOf(rec.rawText, new RegExp("^\\s*-\\s*['\"]?" + action + "['\"]?\\s*$"));
62
63
  violations.push(
63
64
  mk(
@@ -0,0 +1,8 @@
1
+ // Phase: SMOKE
2
+ import { expect, test } from "bun:test";
3
+ import { dispatch } from "../../server";
4
+
5
+ test("resolve_match traverses the exported journey dispatch", () => {
6
+ const result = dispatch("resolve_match", { voted: true }, { handlers: {}, seed: {} });
7
+ expect(result.selectedTrainId).toBe("train:match:nominal");
8
+ });
@@ -0,0 +1,9 @@
1
+ interlocking_id: interlocking:match
2
+ entrypoint:
3
+ exposed: true
4
+ actions:
5
+ - resolve_match
6
+ routes:
7
+ - route_id: nominal
8
+ train_id: train:match:nominal
9
+ train_path: plan/_trains/train:match:nominal.yaml
@@ -0,0 +1,10 @@
1
+ import { JourneyRunner } from "./src/trains/journey";
2
+
3
+ export const JOURNEY_MAP = {
4
+ resolve_match: { journeyId: "journey:match", path: "plan/_journeys/match.yaml" },
5
+ };
6
+
7
+ export function dispatch(action: keyof typeof JOURNEY_MAP, inputs: object, context: { handlers: object; seed: object }) {
8
+ const mapping = JOURNEY_MAP[action];
9
+ return new JourneyRunner(mapping.path).execute(action, inputs, context);
10
+ }
@@ -0,0 +1,12 @@
1
+ import { TrainRunner } from "./runner";
2
+
3
+ export class InterlockingRunner {
4
+ constructor(private readonly interlockingPath: string) {}
5
+ resolveTrain(_action: string, _inputs: object) {
6
+ return { routeId: "nominal", trainId: "train:match:nominal", trainPath: "plan/_trains/train:match:nominal.yaml" };
7
+ }
8
+ execute(action: string, inputs: object, context: { handlers: object; seed: object }) {
9
+ const resolution = this.resolveTrain(action, inputs);
10
+ return new TrainRunner(resolution.trainPath, context.handlers).execute(resolution, context.seed);
11
+ }
12
+ }
@@ -0,0 +1,8 @@
1
+ import { InterlockingRunner } from "./interlocking";
2
+
3
+ export class JourneyRunner {
4
+ constructor(private readonly journeyPath: string) {}
5
+ execute(action: string, inputs: object, context: { handlers: object; seed: object }) {
6
+ return new InterlockingRunner(this.journeyPath).execute(action, inputs, context);
7
+ }
8
+ }
@@ -0,0 +1,6 @@
1
+ export class TrainRunner {
2
+ constructor(private readonly trainPath: string, private readonly handlers: object = {}) {}
3
+ execute(resolution: { trainId: string }, seed: object) {
4
+ return { selectedTrainId: resolution.trainId, trainPath: this.trainPath, handlers: this.handlers, seed };
5
+ }
6
+ }
@@ -0,0 +1,14 @@
1
+ import { expect, test } from "bun:test";
2
+ import { InterlockingRunner } from "../../../src/trains/interlocking.ts";
3
+ import { TrainRunner } from "../../../src/trains/runner.ts";
4
+ import { testTrainContext } from "../../support/production-train.ts";
5
+
6
+ test("create_scenario selects the declared compile train of interlocking:scenario-compilation", async () => {
7
+ const runner = new InterlockingRunner("plan/_trains/_interlockings/scenario-compilation.yaml");
8
+ const resolution = await runner.resolveTrain("create_scenario", { scenario: { declared: true } });
9
+ const context = await testTrainContext(resolution.trainPath);
10
+ const executed = await new TrainRunner(resolution.trainPath, context.handlers).execute(resolution, context.seed);
11
+ expect(resolution.routeId).toBe("compile");
12
+ expect(resolution.trainId).toBe("train:scenario:compile");
13
+ expect(executed.selectedTrainId).toBe("train:scenario:compile");
14
+ });
@@ -0,0 +1,14 @@
1
+ import { expect, test } from "bun:test";
2
+ import { InterlockingRunner } from "../../../src/trains/interlocking.ts";
3
+ import { TrainRunner } from "../../../src/trains/runner.ts";
4
+ import { testTrainContext } from "../../support/production-train.ts";
5
+
6
+ test("replay_session selects the declared replay train of interlocking:session-replay", async () => {
7
+ const runner = new InterlockingRunner("plan/_trains/_interlockings/session-replay.yaml");
8
+ const resolution = await runner.resolveTrain("replay_session", { replay: { declared: true } });
9
+ const context = await testTrainContext(resolution.trainPath);
10
+ const executed = await new TrainRunner(resolution.trainPath, context.handlers).execute(resolution, context.seed);
11
+ expect(resolution.routeId).toBe("replay");
12
+ expect(resolution.trainId).toBe("train:session:replay");
13
+ expect(executed.selectedTrainId).toBe("train:session:replay");
14
+ });
@@ -0,0 +1,14 @@
1
+ import { expect, test } from "bun:test";
2
+ import { InterlockingRunner } from "../../../src/trains/interlocking.ts";
3
+ import { TrainRunner } from "../../../src/trains/runner.ts";
4
+ import { testTrainContext } from "../../support/production-train.ts";
5
+
6
+ test("create_world selects the declared refuse train of interlocking:source-corpus-admission", async () => {
7
+ const runner = new InterlockingRunner("plan/_trains/_interlockings/source-corpus-admission.yaml");
8
+ const resolution = await runner.resolveTrain("create_world", { source: { declared: true } });
9
+ const context = await testTrainContext(resolution.trainPath);
10
+ const executed = await new TrainRunner(resolution.trainPath, context.handlers).execute(resolution, context.seed);
11
+ expect(resolution.routeId).toBe("refuse");
12
+ expect(resolution.trainId).toBe("train:source:refuse");
13
+ expect(executed.selectedTrainId).toBe("train:source:refuse");
14
+ });
@@ -0,0 +1,44 @@
1
+ // Phase: SMOKE
2
+ import { expect, test } from "bun:test";
3
+ import {
4
+ executeAction,
5
+ JOURNEY_MAP,
6
+ stationMaster,
7
+ } from "../../src/server.ts";
8
+ import { InterlockingRunner } from "../../src/trains/interlocking.ts";
9
+ import { TrainRunner } from "../../src/trains/runner.ts";
10
+ import { testTrainContext } from "../support/production-train.ts";
11
+
12
+ const proveProductionRoute = async (path: string, action: string, inputs: Record<string, unknown>) => {
13
+ const runner = new InterlockingRunner(path);
14
+ const resolution = await runner.resolveTrain(action, inputs);
15
+ const context = await testTrainContext(resolution.trainPath);
16
+ const executed = await new TrainRunner(resolution.trainPath, context.handlers).execute(resolution, context.seed);
17
+ expect(resolution.trainId).toBeDefined();
18
+ expect(executed).toBeDefined();
19
+ };
20
+
21
+ test("Station Master returns an incomplete Scenario continuation", async () => {
22
+ await proveProductionRoute("plan/_trains/_interlockings/scenario-compilation.yaml", "create_scenario", { scenario: { valid: true } });
23
+ const result = await executeAction("create_scenario", { scenario: { valid: true } });
24
+ expect(result).toMatchObject({ disposition: "INCOMPLETE_CONTINUATION", status: 202 });
25
+ expect(JOURNEY_MAP.create_scenario.interlockingId).toBe("interlocking:scenario-compilation");
26
+ expect(InterlockingRunner).toBeDefined();
27
+ expect(TrainRunner).toBeDefined();
28
+ });
29
+
30
+ test("Station Master returns an incomplete replay continuation", async () => {
31
+ await proveProductionRoute("plan/_trains/_interlockings/session-replay.yaml", "replay_session", { replay: { requested: true } });
32
+ const result = await executeAction("replay_session", { replay: { requested: true } });
33
+ expect(result).toMatchObject({ disposition: "INCOMPLETE_CONTINUATION", status: 202 });
34
+ expect(JOURNEY_MAP.replay_session.interlockingId).toBe("interlocking:session-replay");
35
+ expect(stationMaster).toBeDefined();
36
+ });
37
+
38
+ test("Station Master executes the bounded source refusal terminal", async () => {
39
+ await proveProductionRoute("plan/_trains/_interlockings/source-corpus-admission.yaml", "create_world", { source: { required_supported: false } });
40
+ const response = await stationMaster({ submission: { id: "smoke" }, supported: false });
41
+ expect(response.status).toBe(422);
42
+ expect(response.headers.get("x-journey-disposition")).toBe("TERMINAL");
43
+ expect(await response.text()).toContain("UNSUPPORTED_SOURCE_FORMAT");
44
+ });
@@ -0,0 +1,8 @@
1
+ // Supplies test-only wagon handlers for production runner topology proofs.
2
+ import type { WagonHandlerRegistry } from "../../src/trains/runner.ts";
3
+
4
+ /** Builds an inert test handler for the declared train without composing domain services. */
5
+ export async function testTrainContext(trainPath: string): Promise<Readonly<{ seed: Record<string, unknown>; handlers: WagonHandlerRegistry }>> {
6
+ const wagon = trainPath.replace(/^plan\/_trains\/|\.yaml$/g, "");
7
+ return { seed: {}, handlers: { [wagon]: () => ({ inert: true }) } };
8
+ }