@afokapu/atdd-bun 0.10.54 → 0.10.55
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.
- package/detectors/bun_interlocking_binding/checks/interlocking_runtime_executes.mjs +46 -1
- package/detectors/bun_interlocking_binding/detect.mjs +2 -2
- package/detectors/bun_interlocking_binding/fixtures/clean/server.ts +2 -2
- package/detectors/bun_interlocking_binding/fixtures/clean/src/trains/runner.ts +21 -3
- package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/server.ts +2 -2
- package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/src/trains/runner.ts +21 -3
- package/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +132 -2
- package/detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs +9 -4
- package/detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs +4 -4
- package/detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs +2 -1
- package/detectors/bun_interlocking_coverage/checks/interlocking_trace_binds.mjs +7 -5
- package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +8 -13
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_route_coverage/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_route_coverage/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage_module/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage_module/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_trace_binds/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_trace_binds/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs +19 -0
- package/detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs +27 -0
- package/detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs +30 -0
- package/detectors/bun_interlocking_infrastructure/checks/wagon_contract_honoured.mjs +112 -12
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_runner_boundary/src/trains/journey.ts +17 -4
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_station_routing/src/trains/journey.ts +16 -3
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/plan/_journeys/checkout.yaml +19 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/server.ts +13 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/src/trains/journey.ts +36 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/wagon_contract_honoured/src/trains/runner.ts +5 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_dead_read/plan/_journeys/checkout.yaml +14 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_dead_read/src/trains/journey.ts +15 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_returns_early/plan/_journeys/checkout.yaml +19 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_returns_early/src/trains/journey.ts +16 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_self_loop/plan/_journeys/checkout.yaml +14 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_self_loop/src/trains/journey.ts +18 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/plan/_journeys/public.yaml +14 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/server.ts +16 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/src/trains/journey.ts +9 -0
- package/integrity.json +42 -19
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ export const RULE_EXECUTES = "coder.bun.runtime-executes-the-declaration";
|
|
|
29
29
|
const LOADS_DECLARATION =
|
|
30
30
|
/\breadFileSync\s*\(|\breadFile\s*\(|\bBun\s*\.\s*file\s*\(|\bcreateReadStream\s*\(|\bfs\s*\.\s*promises\s*\.\s*readFile\b/;
|
|
31
31
|
const RESOLVES = /\bInterlockingResolution\b|\bresolveTrain\s*\(/;
|
|
32
|
+
const TRAIN_RUNNER = /\bclass\s+TrainRunner\b/;
|
|
32
33
|
const EXCLUDES = ["node_modules", "dist", "build", ".next", "_generated"];
|
|
33
34
|
const PLAN_ROOT = process.env.ATDD_PLAN_ROOT || "plan";
|
|
34
35
|
|
|
@@ -159,10 +160,54 @@ export function scanExecution(scanRoot) {
|
|
|
159
160
|
return violations;
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
// TrainRunner is the other half of declaration execution. A correctly data-driven
|
|
164
|
+
// InterlockingRunner can select a train while its executor silently ignores that
|
|
165
|
+
// train and returns a hand-written trace. Require the executor itself to read a
|
|
166
|
+
// supplied train declaration, iterate its declared sequence, and feed each wagon's
|
|
167
|
+
// production result into the next call. These deliberately structural checks admit
|
|
168
|
+
// ordinary direct functions (executeWagon/runWagon/runTrain) without prescribing a
|
|
169
|
+
// framework, while rejecting the no-op and literal-trace shapes they replace.
|
|
170
|
+
export function scanTrainRunnerExecution(scanRoot) {
|
|
171
|
+
const violations = [];
|
|
172
|
+
for (const croot of findConsumerRoots(scanRoot)) {
|
|
173
|
+
const trainDeclarations = walk(join(croot, PLAN_ROOT, "_trains"), (f) => /train:.*\.ya?ml$/.test(f));
|
|
174
|
+
if (!trainDeclarations.length) continue;
|
|
175
|
+
const rtFiles = walk(join(croot, "src/trains"), (f) => /\.(ts|tsx|mjs|js)$/.test(f))
|
|
176
|
+
.map((f) => ({ file: f, text: readText(f) }));
|
|
177
|
+
for (const runner of rtFiles.filter((x) => TRAIN_RUNNER.test(x.text))) {
|
|
178
|
+
const readsDeclaration = LOADS_DECLARATION.test(runner.text);
|
|
179
|
+
const usesSuppliedDeclaration = /\b(?:parse|load)(?:Train|Declaration)\s*\(\s*this\.\w*(?:train|declaration|path)\w*\s*\)/i.test(runner.text) ||
|
|
180
|
+
/\b(?:readFileSync|readFile|Bun\s*\.\s*file)\s*\(\s*this\.\w*(?:train|declaration|path)\w*/i.test(runner.text);
|
|
181
|
+
const parsesSequence = /\bsequence\b/.test(runner.text);
|
|
182
|
+
const derivesOrder = /for\s*\(\s*const\s+\w+\s+of\s+\w+\.sequence\s*\)/.test(runner.text);
|
|
183
|
+
// The reassignment is the key data-flow proof: a production execution call
|
|
184
|
+
// receives the prior result and its output becomes the next result.
|
|
185
|
+
const carriesResults = /\b(\w+)\s*=\s*(?:await\s+)?(?:execute|run)\w*\s*\(\s*\w+\s*,\s*\1\s*\)/.test(runner.text);
|
|
186
|
+
const fabricatesTrace = /\b(?:trace|steps?|sequence)\s*:\s*\[\s*["'`]/.test(maskComments(runner.text));
|
|
187
|
+
if (readsDeclaration && usesSuppliedDeclaration && parsesSequence && derivesOrder && carriesResults && !fabricatesTrace) continue;
|
|
188
|
+
const missing = [
|
|
189
|
+
!readsDeclaration && "read the supplied train declaration",
|
|
190
|
+
!usesSuppliedDeclaration && "pass its supplied declaration into that parser or reader",
|
|
191
|
+
!parsesSequence && "parse its sequence",
|
|
192
|
+
!derivesOrder && "derive ordered iteration from train.sequence",
|
|
193
|
+
!carriesResults && "carry each production execution result into the next wagon",
|
|
194
|
+
fabricatesTrace && "avoid fabricating literal trace or step data",
|
|
195
|
+
].filter(Boolean).join("; ");
|
|
196
|
+
const rel = runner.file.startsWith(croot + sep) ? runner.file.slice(croot.length + 1) : runner.file;
|
|
197
|
+
violations.push({
|
|
198
|
+
rule_id: RULE_EXECUTES, file: rel, line: 1, col: 0,
|
|
199
|
+
evidence: `TrainRunner must ${missing}; declaration-selected wagon execution cannot be satisfied by a no-op or transcribed trace`,
|
|
200
|
+
source_line: "",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return violations;
|
|
205
|
+
}
|
|
206
|
+
|
|
162
207
|
if (import.meta.main ?? process.argv[1]?.endsWith("interlocking_runtime_executes.mjs")) {
|
|
163
208
|
let roots = [];
|
|
164
209
|
try { roots = JSON.parse(process.env.ATDD_SCAN_ROOTS || "[]"); } catch {}
|
|
165
|
-
const out = roots.flatMap((r) => scanExecution(r));
|
|
210
|
+
const out = roots.flatMap((r) => [...scanExecution(r), ...scanTrainRunnerExecution(r)]);
|
|
166
211
|
const rp = process.env.ATDD_VIOLATIONS_REPORT;
|
|
167
212
|
if (rp) writeFileSync(rp, JSON.stringify({ violations: out }, null, 2), "utf8");
|
|
168
213
|
process.stderr.write(`bun-executes: ${out.length} violation(s)\n`);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { excludedPath } from "../../lib/scan.mjs";
|
|
25
25
|
import { parseInterlocking as parseInterlockingDoc } from "../../lib/interlocking.mjs";
|
|
26
26
|
import { readFileSync, writeFileSync, statSync, readdirSync } from "node:fs";
|
|
27
|
-
import { scanExecution } from "./checks/interlocking_runtime_executes.mjs";
|
|
27
|
+
import { scanExecution, scanTrainRunnerExecution } from "./checks/interlocking_runtime_executes.mjs";
|
|
28
28
|
import { join, sep, resolve } from "node:path";
|
|
29
29
|
|
|
30
30
|
const RULE = "coder.bun.interlocking-bilateral-binding";
|
|
@@ -544,7 +544,7 @@ function main() {
|
|
|
544
544
|
// coder.bun.runtime-executes-the-declaration — no longer staged. This family is
|
|
545
545
|
// monolithic rather than checks/-collecting, so it is called here rather than
|
|
546
546
|
// discovered.
|
|
547
|
-
violations.push(...scanExecution(scanRoot));
|
|
547
|
+
violations.push(...scanExecution(scanRoot), ...scanTrainRunnerExecution(scanRoot));
|
|
548
548
|
}
|
|
549
549
|
writeFileSync(reportPath, JSON.stringify({ violations }, null, 2), "utf8");
|
|
550
550
|
process.stderr.write(`bun-interlocking-binding: ${violations.length} violation(s)\n`);
|
|
@@ -12,8 +12,8 @@ export const JOURNEY_MAP = {
|
|
|
12
12
|
|
|
13
13
|
export function dispatch(action: string, inputs: Record<string, unknown>) {
|
|
14
14
|
const mapping = (JOURNEY_MAP as Record<string, unknown>)[action];
|
|
15
|
-
if (typeof mapping === "string") return new TrainRunner(mapping).execute(inputs);
|
|
15
|
+
if (typeof mapping === "string") return new TrainRunner(`plan/_trains/${mapping}.yaml`, mapping).execute(inputs);
|
|
16
16
|
const { path } = mapping as { interlockingId: string; path: string };
|
|
17
17
|
const res = new InterlockingRunner(path).resolveTrain(action, inputs);
|
|
18
|
-
return new TrainRunner(res.trainId).execute(inputs);
|
|
18
|
+
return new TrainRunner(res.trainPath, res.trainId).execute(inputs);
|
|
19
19
|
}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
// TrainRunner — the
|
|
1
|
+
// TrainRunner — reads the selected train declaration and executes its declared wagons in order.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
type Result = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function parseTrain(path: string): { sequence: string[] } {
|
|
7
|
+
const text = readFileSync(path, "utf8");
|
|
8
|
+
return { sequence: [...text.matchAll(/^\s*-\s*([^#\s]+)\s*$/gm)].map((match) => match[1]) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function executeWagon(wagon: string, result: Result): Result {
|
|
12
|
+
return { ...result, [wagon]: "executed" };
|
|
13
|
+
}
|
|
14
|
+
|
|
2
15
|
export class TrainRunner {
|
|
3
|
-
constructor(private readonly
|
|
16
|
+
constructor(private readonly trainDeclarationPath: string, private readonly trainId?: string) {}
|
|
4
17
|
execute(inputs: Record<string, unknown>) {
|
|
5
|
-
|
|
18
|
+
const train = parseTrain(this.trainDeclarationPath);
|
|
19
|
+
let result: Result = inputs;
|
|
20
|
+
for (const wagon of train.sequence) {
|
|
21
|
+
result = executeWagon(wagon, result);
|
|
22
|
+
}
|
|
23
|
+
return { selectedTrainId: this.trainId, result };
|
|
6
24
|
}
|
|
7
25
|
}
|
|
@@ -12,8 +12,8 @@ export const JOURNEY_MAP = {
|
|
|
12
12
|
|
|
13
13
|
export function dispatch(action: string, inputs: Record<string, unknown>) {
|
|
14
14
|
const mapping = (JOURNEY_MAP as Record<string, unknown>)[action];
|
|
15
|
-
if (typeof mapping === "string") return new TrainRunner(mapping).execute(inputs);
|
|
15
|
+
if (typeof mapping === "string") return new TrainRunner(`plan/_trains/${mapping}.yaml`, mapping).execute(inputs);
|
|
16
16
|
const { path } = mapping as { interlockingId: string; path: string };
|
|
17
17
|
const res = new InterlockingRunner(path).resolveTrain(action, inputs);
|
|
18
|
-
return new TrainRunner(res.trainId).execute(inputs);
|
|
18
|
+
return new TrainRunner(res.trainPath, res.trainId).execute(inputs);
|
|
19
19
|
}
|
package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/src/trains/runner.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
// TrainRunner — the
|
|
1
|
+
// TrainRunner — reads the selected train declaration and executes its declared wagons in order.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
type Result = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function parseTrain(path: string): { sequence: string[] } {
|
|
7
|
+
const text = readFileSync(path, "utf8");
|
|
8
|
+
return { sequence: [...text.matchAll(/^\s*-\s*([^#\s]+)\s*$/gm)].map((match) => match[1]) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function executeWagon(wagon: string, result: Result): Result {
|
|
12
|
+
return { ...result, [wagon]: "executed" };
|
|
13
|
+
}
|
|
14
|
+
|
|
2
15
|
export class TrainRunner {
|
|
3
|
-
constructor(private readonly
|
|
16
|
+
constructor(private readonly trainDeclarationPath: string, private readonly trainId?: string) {}
|
|
4
17
|
execute(inputs: Record<string, unknown>) {
|
|
5
|
-
|
|
18
|
+
const train = parseTrain(this.trainDeclarationPath);
|
|
19
|
+
let result: Result = inputs;
|
|
20
|
+
for (const wagon of train.sequence) {
|
|
21
|
+
result = executeWagon(wagon, result);
|
|
22
|
+
}
|
|
23
|
+
return { selectedTrainId: this.trainId, result };
|
|
6
24
|
}
|
|
7
25
|
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// data (snake_case, plan/_trains/_interlockings/**); the e2e tests are Bun/TS under e2e/**.
|
|
8
8
|
|
|
9
9
|
import { excludedPath } from "../../../lib/scan.mjs";
|
|
10
|
-
import { readFileSync, statSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { join, sep } from "node:path";
|
|
10
|
+
import { existsSync, readFileSync, statSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
12
12
|
|
|
13
13
|
export const DEFAULT_EXCLUDES = ["_generated", "node_modules", "dist", "build", ".next"];
|
|
14
14
|
export const PLAN_ROOT = process.env.ATDD_PLAN_ROOT || "plan";
|
|
@@ -315,6 +315,136 @@ export function isInterlockingTest(text, tokens) {
|
|
|
315
315
|
return tokens.some((t) => tokenCovered(t, text));
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// Semantic witnesses for the production call model. Tokens get us to a candidate test; these
|
|
319
|
+
// small, deliberately conservative recognisers decide whether the candidate actually connects
|
|
320
|
+
// `InterlockingRunner.resolveTrain()` to `TrainRunner.execute()` and asserts evidence from that
|
|
321
|
+
// chain. We keep this in the shared layer so route, runner, trace, smoke, and sequence checks
|
|
322
|
+
// cannot quietly drift back to accepting a different descriptive proxy.
|
|
323
|
+
const ident = "[A-Za-z_$][\\w$]*";
|
|
324
|
+
|
|
325
|
+
function escaped(value) {
|
|
326
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function productionReceivers(text) {
|
|
330
|
+
const out = new Set();
|
|
331
|
+
const allocated = new RegExp(`\\b(?:const|let|var)\\s+(${ident})\\s*=\\s*new\\s+${PROD_INTERLOCKING}\\b`, "g");
|
|
332
|
+
for (const m of text.matchAll(allocated)) out.add(m[1]);
|
|
333
|
+
const factories = new RegExp(`\\bfunction\\s+(${ident})\\s*\\([^)]*\\)\\s*\\{[\\s\\S]{0,600}?\\breturn\\s+new\\s+${PROD_INTERLOCKING}\\b`, "g");
|
|
334
|
+
for (const m of text.matchAll(factories)) out.add(`${m[1]}()`);
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// [{ resolution, execution }] means an actual resolution value feeds actual train execution.
|
|
339
|
+
// `execution` is null for an unassigned execute result; route/runner proof may assert resolution,
|
|
340
|
+
// while trace and sequence proof require the named execution result.
|
|
341
|
+
export function hasResolvedProductionRunnerImports(text, file, croot) {
|
|
342
|
+
const imported = new Set();
|
|
343
|
+
const re = /^\s*import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/gm;
|
|
344
|
+
for (const match of text.matchAll(re)) {
|
|
345
|
+
if (!match[2].startsWith(".")) continue;
|
|
346
|
+
const names = match[1].split(",").map(name => name.trim().split(/\s+as\s+/)[0]);
|
|
347
|
+
const target = resolve(dirname(file), match[2]);
|
|
348
|
+
const candidates = [target, `${target}.ts`, `${target}.tsx`, `${target}.js`, `${target}.mjs`, join(target, "index.ts")];
|
|
349
|
+
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);
|
|
352
|
+
}
|
|
353
|
+
return imported.has(PROD_INTERLOCKING) && imported.has(PROD_TRAIN);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function productionExecutionProofs(text, file, croot) {
|
|
357
|
+
if (file && croot && !hasResolvedProductionRunnerImports(text, file, croot)) return [];
|
|
358
|
+
const receivers = productionReceivers(text);
|
|
359
|
+
const proofs = [];
|
|
360
|
+
const resolutions = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*([^\\n]{0,240}?)\\.resolveTrain\\s*\\(`, "g");
|
|
361
|
+
for (const m of text.matchAll(resolutions)) {
|
|
362
|
+
const [whole, resolution, receiver] = m;
|
|
363
|
+
const direct = new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(receiver);
|
|
364
|
+
const named = [...receivers].some((name) => receiver.trim().endsWith(name));
|
|
365
|
+
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*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 });
|
|
370
|
+
}
|
|
371
|
+
return proofs;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
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})`);
|
|
376
|
+
const m = pat.exec(text);
|
|
377
|
+
return Boolean(m && (!expected || tokenCovered(expected, m[0])));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function hasProductionExecutionProof(text, file, croot) {
|
|
381
|
+
return productionExecutionProofs(text, file, croot).some(({ resolution, execution }) => {
|
|
382
|
+
if (assertsExpression(text, `${escaped(resolution)}\\.(?:routeId|trainId|selectedTrainId)`) ||
|
|
383
|
+
(execution && assertsExpression(text, escaped(execution)))) return true;
|
|
384
|
+
if (!execution) return false;
|
|
385
|
+
const trace = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${escaped(execution)}\\.trace\\b`).exec(text)?.[1];
|
|
386
|
+
return Boolean(trace && REQUIRED_TRACE_FIELDS.some(([field]) => assertsExpression(text, `${escaped(trace)}\\.${field}`)));
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function routeHasProductionProof(route, text, interlockingId = null, file, croot) {
|
|
391
|
+
const slug = interlockingId?.replace(/^interlocking:/, "");
|
|
392
|
+
return productionExecutionProofs(text, file, croot).some(({ resolution, execution }) => {
|
|
393
|
+
const assertedRoute = assertsExpression(text, `${escaped(resolution)}\\.routeId`, route.routeId);
|
|
394
|
+
const assertedTrain = assertsExpression(text, `${escaped(resolution)}\\.(?:trainId|selectedTrainId)`, route.trainId);
|
|
395
|
+
const scoped = !interlockingId || tokenCovered(interlockingId, text) || tokenCovered(slug, text);
|
|
396
|
+
return (assertedRoute && scoped) || assertedTrain || Boolean(scoped && execution && assertsExpression(text, escaped(execution)) && (tokenCovered(route.routeId, text) || tokenCovered(route.trainId, text)));
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function traceHasProductionProvenance(text, file, croot) {
|
|
401
|
+
return productionExecutionProofs(text, file, croot).some(({ execution }) => {
|
|
402
|
+
if (!execution) return false;
|
|
403
|
+
const trace = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${escaped(execution)}\\.trace\\b`).exec(text)?.[1];
|
|
404
|
+
return Boolean(trace);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function assertedTraceFields(text, traceName = "trace") {
|
|
409
|
+
return REQUIRED_TRACE_FIELDS.filter(([field]) => !assertsExpression(text, `${escaped(traceName)}\\.${field}`)).map(([field]) => field);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// A smoke path has a real Station Master call and observes that call's returned value; separately
|
|
413
|
+
// requiring both production runner instances rules out imports used only as lexical decoration.
|
|
414
|
+
export function stationMasterExecutionProof(text, action) {
|
|
415
|
+
const call = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${ident}\\.(?:handleAction|dispatch|executeAction)\\s*\\(\\s*["']${escaped(action)}["']`).exec(text);
|
|
416
|
+
if (!call) return false;
|
|
417
|
+
const result = call[1];
|
|
418
|
+
const instanceProof = new RegExp(`\\bexpect\\s*\\(\\s*${ident}\\.(?:interlockingRunner|trainRunner)\\s*\\)\\s*\\.toBeInstanceOf\\s*\\(\\s*(?:${PROD_INTERLOCKING}|${PROD_TRAIN})\\s*\\)`, "g");
|
|
419
|
+
const seen = new Set([...text.matchAll(instanceProof)].map((m) => m[0].includes(PROD_INTERLOCKING) ? PROD_INTERLOCKING : PROD_TRAIN));
|
|
420
|
+
return seen.has(PROD_INTERLOCKING) && seen.has(PROD_TRAIN) && assertsExpression(text, `${escaped(result)}(?:\\.[A-Za-z_$][\\w$]*)?`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// HTTP/module entrypoints do not expose a StationMaster instance to the test. Their equivalent
|
|
424
|
+
// witness is an asserted dispatch result plus a module body that resolves and executes the selected
|
|
425
|
+
// train; this keeps an end-to-end smoke from having to pierce the public boundary for private fields.
|
|
426
|
+
export function stationModuleExecutionProof(text, stationText) {
|
|
427
|
+
if (!stationText || !new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(stationText) ||
|
|
428
|
+
!/\.resolveTrain\s*\(/.test(stationText) || !new RegExp(`new\\s+${PROD_TRAIN}\\b`).test(stationText) ||
|
|
429
|
+
!/\.execute\s*\(/.test(stationText)) return false;
|
|
430
|
+
const call = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*await\\s+(?:${ident}\\.)?(?:dispatch|handleAction|executeAction)\\s*\\(`).exec(text);
|
|
431
|
+
return Boolean(call && assertsExpression(text, escaped(call[1])));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Bun's native `expect` vocabulary provides a compact mutation witness: exact ordered equality
|
|
435
|
+
// kills reorder/removal mutants, objectContaining records a handoff edge, and at(-1) observes the
|
|
436
|
+
// final wagon. All three must be about the TrainRunner result, never a hand-built literal.
|
|
437
|
+
export function hasSequenceMutationProof(text, trainId, file, croot) {
|
|
438
|
+
return productionExecutionProofs(text, file, croot).some(({ resolution, execution }) => {
|
|
439
|
+
if (!execution || !assertsExpression(text, `${escaped(resolution)}\\.(?:trainId|selectedTrainId)`, trainId)) return false;
|
|
440
|
+
const result = escaped(execution);
|
|
441
|
+
const ordered = new RegExp(`expect\\s*\\(\\s*${result}\\.(?:steps|sequence|wagons)\\s*\\)\\s*\\.to(?:Equal|StrictEqual)\\s*\\(\\s*\\[`, "s").test(text);
|
|
442
|
+
const handoff = new RegExp(`expect\\s*\\(\\s*${result}\\.(?:steps|sequence|wagons)\\s*\\)\\s*\\.toContainEqual\\s*\\(\\s*expect\\.objectContaining\\s*\\(\\s*\\{[\\s\\S]{0,240}?(?:from|to)\\s*:`, "s").test(text);
|
|
443
|
+
const finalWagon = new RegExp(`expect\\s*\\(\\s*${result}\\.(?:steps|sequence|wagons)\\s*\\.at\\(\\s*-1\\s*\\)\\s*\\)\\s*\\.to(?:Be|Equal|StrictEqual|Match)`, "s").test(text);
|
|
444
|
+
return ordered && handoff && finalWagon;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
318
448
|
export function writeReport(violations) {
|
|
319
449
|
const rp = process.env.ATDD_VIOLATIONS_REPORT;
|
|
320
450
|
if (!rp) {
|
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
PROD_INTERLOCKING,
|
|
23
23
|
PROD_TRAIN,
|
|
24
24
|
FORBIDDEN_PATTERNS,
|
|
25
|
+
hasProductionExecutionProof,
|
|
26
|
+
stationMasterExecutionProof,
|
|
27
|
+
stationModuleExecutionProof,
|
|
25
28
|
rel,
|
|
26
29
|
mk,
|
|
27
30
|
writeReport,
|
|
@@ -67,16 +70,18 @@ for (const scanRoot of roots) {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const semantic = hasProductionExecutionProof(text, file, croot) ||
|
|
74
|
+
records.some((rec) => rec.actions.some((action) => stationMasterExecutionProof(text, action))) ||
|
|
75
|
+
(drivesStationMaster(text) && stationModuleExecutionProof(text, station));
|
|
76
|
+
if (!semantic) {
|
|
72
77
|
violations.push(
|
|
73
78
|
mk(
|
|
74
79
|
RULE,
|
|
75
80
|
r,
|
|
76
81
|
1,
|
|
77
82
|
0,
|
|
78
|
-
`interlocking test "${r}"
|
|
79
|
-
`
|
|
83
|
+
`interlocking test "${r}" has no asserted result flowing from production InterlockingRunner ` +
|
|
84
|
+
`resolution into TrainRunner execution; imports and route/train literals are discovery only`,
|
|
80
85
|
"",
|
|
81
86
|
),
|
|
82
87
|
);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
interlockingFiles,
|
|
14
14
|
e2eFiles,
|
|
15
15
|
parseInterlocking,
|
|
16
|
-
|
|
16
|
+
routeHasProductionProof,
|
|
17
17
|
maskComments,
|
|
18
18
|
rel,
|
|
19
19
|
mk,
|
|
@@ -30,11 +30,11 @@ for (const scanRoot of roots) {
|
|
|
30
30
|
const records = interlockingFiles(croot)
|
|
31
31
|
.map((f) => ({ file: f, rec: parseInterlocking(readText(f)) }))
|
|
32
32
|
.filter((x) => x.rec);
|
|
33
|
-
const e2eTexts = e2eFiles(croot).map((
|
|
33
|
+
const e2eTexts = e2eFiles(croot).map((file) => ({ file, text: maskComments(readText(file)) }));
|
|
34
34
|
|
|
35
35
|
for (const { file, rec } of records) {
|
|
36
36
|
for (const route of rec.routes) {
|
|
37
|
-
if (
|
|
37
|
+
if (e2eTexts.some(({ file, text }) => routeHasProductionProof(route, text, rec.interlockingId, file, croot))) continue;
|
|
38
38
|
if (unbuiltWagons(croot, route.trainId).length) continue; // pending: a wagon on its train has no source yet
|
|
39
39
|
const cat =
|
|
40
40
|
route.category !== null
|
|
@@ -48,7 +48,7 @@ for (const scanRoot of roots) {
|
|
|
48
48
|
0,
|
|
49
49
|
`admissible route "${route.routeId}" of interlocking "${rec.interlockingId}" (${cat}, ` +
|
|
50
50
|
`resolves to train "${route.trainId}") has no e2e test exercising it; add an e2e/**/*.ts ` +
|
|
51
|
-
`test
|
|
51
|
+
`test whose asserted resolution selects this route/train and flows through InterlockingRunner -> TrainRunner`,
|
|
52
52
|
route.sourceLine.trim(),
|
|
53
53
|
),
|
|
54
54
|
);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
STATION_MASTER,
|
|
21
21
|
PROD_INTERLOCKING,
|
|
22
22
|
PROD_TRAIN,
|
|
23
|
+
stationMasterExecutionProof,
|
|
23
24
|
rel,
|
|
24
25
|
mk,
|
|
25
26
|
writeReport,
|
|
@@ -36,7 +37,7 @@ const SMOKE_PHASE = /^\s*\/\/\s*Phase:\s*SMOKE\b/m;
|
|
|
36
37
|
function actionSmokeCovered(action, e2eFiles) {
|
|
37
38
|
return e2eFiles.some(
|
|
38
39
|
({ raw, text: t }) =>
|
|
39
|
-
SMOKE_PHASE.test(raw) && tokenCovered(action, t) && STATION_MASTER.test(t) && t.includes(PROD_INTERLOCKING) && t.includes(PROD_TRAIN),
|
|
40
|
+
SMOKE_PHASE.test(raw) && tokenCovered(action, t) && STATION_MASTER.test(t) && t.includes(PROD_INTERLOCKING) && t.includes(PROD_TRAIN) && stationMasterExecutionProof(t, action),
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
lineAt,
|
|
21
21
|
TRACE_OBJECT,
|
|
22
22
|
REQUIRED_TRACE_FIELDS,
|
|
23
|
+
assertedTraceFields,
|
|
24
|
+
traceHasProductionProvenance,
|
|
23
25
|
rel,
|
|
24
26
|
mk,
|
|
25
27
|
writeReport,
|
|
@@ -42,8 +44,9 @@ for (const scanRoot of roots) {
|
|
|
42
44
|
if (!isInterlockingTest(text, tokens)) continue;
|
|
43
45
|
const m = TRACE_OBJECT.exec(text);
|
|
44
46
|
if (!m) continue; // not a trace-binding test.
|
|
45
|
-
const missing =
|
|
46
|
-
|
|
47
|
+
const missing = assertedTraceFields(text);
|
|
48
|
+
const provenance = traceHasProductionProvenance(text, file, croot);
|
|
49
|
+
if (!missing.length && provenance) continue;
|
|
47
50
|
const line = lineOfIndex(text, m.index);
|
|
48
51
|
violations.push(
|
|
49
52
|
mk(
|
|
@@ -51,9 +54,8 @@ for (const scanRoot of roots) {
|
|
|
51
54
|
rel(file, croot),
|
|
52
55
|
line,
|
|
53
56
|
0,
|
|
54
|
-
`interlocking trace test "${rel(file, croot)}" does not bind
|
|
55
|
-
`trace field(s) ${missing.join(", ")}
|
|
56
|
-
`routeId/selectedTrainId/routeCategory/routeCategoryDigit/guardId/resolutionStrategy/resolutionReason)`,
|
|
57
|
+
`interlocking trace test "${rel(file, croot)}" does not bind a production TrainRunner result to the ` +
|
|
58
|
+
`declared route${missing.length ? `: missing asserted trace field(s) ${missing.join(", ")}` : ""}${!provenance ? "; trace is not derived from InterlockingRunner -> TrainRunner execution" : ""}`,
|
|
57
59
|
lineAt(raw, line),
|
|
58
60
|
),
|
|
59
61
|
);
|
|
@@ -16,22 +16,17 @@
|
|
|
16
16
|
// suite green. The runtime obeyed the new plan; no test looked.
|
|
17
17
|
import {
|
|
18
18
|
parseJsonEnv, readText, findConsumerRoots, interlockingFiles, e2eFiles,
|
|
19
|
-
parseInterlocking,
|
|
19
|
+
parseInterlocking, rel, mk, PLAN_ROOT, unbuiltWagons, hasSequenceMutationProof,
|
|
20
20
|
} from "../_shared/interlocking.mjs";
|
|
21
21
|
import { writeFileSync, readdirSync, statSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
23
|
|
|
24
24
|
export const RULE_SEQUENCE = "tester.bun.interlocking-train-sequence-is-exercised";
|
|
25
25
|
|
|
26
|
-
// A line that ASSERTS AN ORDER: names the executed sequence AND compares it.
|
|
27
|
-
// Merely touching the word is not asserting the order — `expect(trace.steps)` alone
|
|
28
|
-
// says nothing about what ran, and counting it would repeat the over-broad trigger
|
|
29
|
-
// that has now been fixed three times in this family.
|
|
30
|
-
const SEQUENCE_ASSERTION =
|
|
31
|
-
/^[^\n]*\b(?:expect|assert)\b[^\n]*\b(?:steps|sequence|wagons)\b[^\n]*(?:==|toEqual|toStrictEqual|deepEqual)/m;
|
|
32
|
-
|
|
33
26
|
export function assertsASequence(text) {
|
|
34
|
-
|
|
27
|
+
// Retained as a narrow API for consumers of this module. The train id is intentionally
|
|
28
|
+
// unavailable here, so it cannot certify coverage; `trainSequenceCovered` below does.
|
|
29
|
+
return /\bexpect\s*\(\s*[A-Za-z_$][\w$]*\.(?:steps|sequence|wagons)\s*\)\s*\.to(?:Equal|StrictEqual)\s*\(/.test(text);
|
|
35
30
|
}
|
|
36
31
|
|
|
37
32
|
export function trainsReachableFromRoutes(records) {
|
|
@@ -45,8 +40,8 @@ export function trainsReachableFromRoutes(records) {
|
|
|
45
40
|
return out;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
export function trainSequenceCovered(trainId,
|
|
49
|
-
return
|
|
43
|
+
export function trainSequenceCovered(trainId, files, croot) {
|
|
44
|
+
return files.some(({ file, text }) => hasSequenceMutationProof(text, trainId, file, croot));
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
// Does the plan actually DECLARE a wagon sequence for this train?
|
|
@@ -85,13 +80,13 @@ export function scanExecution(scanRoot) {
|
|
|
85
80
|
const records = interlockingFiles(croot).map((f) => parseInterlocking(readText(f))).filter(Boolean);
|
|
86
81
|
if (!records.length) continue;
|
|
87
82
|
const files = e2eFiles(croot).map((f) => ({ file: f, text: readText(f) }));
|
|
88
|
-
|
|
83
|
+
|
|
89
84
|
for (const trainId of [...trainsReachableFromRoutes(records)].sort()) {
|
|
90
85
|
// The finding points at the train's own document, where the sequence a test must assert is declared (FWS #fqUGvWHn25mq).
|
|
91
86
|
const declared = declaresASequence(croot, trainId);
|
|
92
87
|
if (!declared) continue; // nothing declared to exercise
|
|
93
88
|
if (unbuiltWagons(croot, trainId).length) continue; // pending: a wagon on it has no source yet
|
|
94
|
-
if (trainSequenceCovered(trainId,
|
|
89
|
+
if (trainSequenceCovered(trainId, files, croot)) continue;
|
|
95
90
|
violations.push(
|
|
96
91
|
mk(RULE_SEQUENCE, rel(declared, croot), 1, 0,
|
|
97
92
|
`declared train "${trainId}" is selected by a route but no test asserts the wagon ` +
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -19,6 +19,15 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const RULE = "coder.bun.station-master-interlocking-routing";
|
|
21
21
|
const roots = parseJsonEnv("ATDD_SCAN_ROOTS", []);
|
|
22
|
+
|
|
23
|
+
function dispatchDelegatesThroughInterlocking(text) {
|
|
24
|
+
const match = /\b(?:export\s+)?function\s+dispatch\s*\([^)]*\)\s*\{([\s\S]*)\}\s*$/.exec(text);
|
|
25
|
+
if (!match) return false;
|
|
26
|
+
const body = match[1];
|
|
27
|
+
if (/\breturn\s+new\s+InterlockingRunner\s*\([^)]*\)\s*\.\s*execute\s*\(/.test(body)) return true;
|
|
28
|
+
const runner = /\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+InterlockingRunner\s*\(/.exec(body)?.[1];
|
|
29
|
+
return Boolean(runner && new RegExp(`\\breturn\\s+(?:await\\s+)?${runner}\\s*\\.\\s*execute\\s*\\(`).test(body));
|
|
30
|
+
}
|
|
22
31
|
const violations = [];
|
|
23
32
|
|
|
24
33
|
for (const scanRoot of roots) {
|
|
@@ -56,6 +65,16 @@ for (const scanRoot of roots) {
|
|
|
56
65
|
),
|
|
57
66
|
);
|
|
58
67
|
}
|
|
68
|
+
if (!dispatchDelegatesThroughInterlocking(text)) {
|
|
69
|
+
violations.push(mk(
|
|
70
|
+
RULE,
|
|
71
|
+
rel(app, croot),
|
|
72
|
+
line,
|
|
73
|
+
0,
|
|
74
|
+
"station-master-dispatch-bypasses-runners: mapped interlocking dispatch must return InterlockingRunner execution; inert runner references or direct business data do not execute the selected train",
|
|
75
|
+
lineAt(text, line),
|
|
76
|
+
));
|
|
77
|
+
}
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
80
|
|
|
@@ -56,6 +56,25 @@ function loadsJourneyDeclaration(text) {
|
|
|
56
56
|
return readsJourneyPath && parsesDeclaration;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function consumesDeclaredTopology(text) {
|
|
60
|
+
const masked = maskComments(text);
|
|
61
|
+
const declaration = /\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*Bun\.YAML\.parse\s*\(/.exec(masked);
|
|
62
|
+
if (!declaration) return false;
|
|
63
|
+
const name = declaration[1].replace(/[$]/g, "\\$");
|
|
64
|
+
const reads = (field) => new RegExp("\\b" + name + "\\s*\\.\\s*" + field + "\\s*\\.\\s*find\\s*\\(").test(masked);
|
|
65
|
+
// A runner must retain the interlocking result long enough to decide whether the
|
|
66
|
+
// declared route continues or terminates. Returning the first execute() directly
|
|
67
|
+
// is the characteristic skipped-continuation mutation.
|
|
68
|
+
const retainsResolution = /\b(?:const|let)\s+[A-Za-z_$][\w$]*\s*=\s*await\s+(?:new\s+InterlockingRunner\s*\([^)]*\)|[A-Za-z_$][\w$]*)\s*\.\s*execute\s*\(/.test(masked);
|
|
69
|
+
const readsEntrypoint = new RegExp("\\b" + name + "\\s*\\.\\s*entrypoint\\b").test(masked);
|
|
70
|
+
// Reading both lists is only discovery. A continuation must alter control flow to
|
|
71
|
+
// another declared interlocking and a terminal must decide the returned outcome;
|
|
72
|
+
// otherwise an early `return resolution` leaves the journey topology inert.
|
|
73
|
+
const followsContinuation = /\bif\s*\(\s*continuation\s*\)\s*\{[\s\S]{0,280}?\bcurrent\s*=\s*continuation\s*\.\s*to\s*\.\s*(?:interlocking_id|interlockingId)\b[\s\S]{0,280}?\bcontinue\b/.test(masked);
|
|
74
|
+
const returnsTerminal = /\bif\s*\(\s*terminal\s*\)\s*(?:\{[\s\S]{0,280}?)?\breturn\b[\s\S]{0,240}?\bterminal\s*\.\s*outcome\b/.test(masked);
|
|
75
|
+
return readsEntrypoint && reads("continuations") && reads("terminals") && retainsResolution && followsContinuation && returnsTerminal;
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
function hiddenTopologyLiterals(text) {
|
|
60
79
|
const masked = maskComments(text);
|
|
61
80
|
const hits = [];
|
|
@@ -114,6 +133,14 @@ for (const scanRoot of roots) {
|
|
|
114
133
|
"journey-runtime-transcription: JourneyRunner does not read and parse a journey declaration; topology may be hardcoded",
|
|
115
134
|
lineAt(text, line),
|
|
116
135
|
));
|
|
136
|
+
if (loadsDeclaration && !consumesDeclaredTopology(text)) violations.push(mk(
|
|
137
|
+
RULE,
|
|
138
|
+
rel(file, croot),
|
|
139
|
+
line,
|
|
140
|
+
0,
|
|
141
|
+
"journey-runtime-continuation-ignored: JourneyRunner reads a continuation-bearing declaration but returns the entrypoint InterlockingRunner result without consuming continuations and terminals",
|
|
142
|
+
lineAt(text, line),
|
|
143
|
+
));
|
|
117
144
|
for (const hit of hardcodedInterlockingConstruction(text)) violations.push(mk(
|
|
118
145
|
RULE,
|
|
119
146
|
rel(file, croot),
|