@afokapu/atdd-bun 0.10.57 → 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.
- package/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +258 -36
- package/detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs +6 -2
- package/detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs +10 -1
- package/detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs +11 -10
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/e2e/smoke/resolve-match.smoke.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/plan/_trains/_interlockings/match.yaml +9 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/server.ts +10 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/interlocking.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/journey.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/runner.ts +6 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/scenario-compilation/compile.routes.test.ts +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/session-replay/replay.routes.test.ts +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/source-corpus-admission/refuse.routes.test.ts +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/smoke/runtime-station-master.test.ts +44 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/support/production-train.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/create-world-pack.yaml +18 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/play-world-scenario.yaml +24 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/replay-world-session.yaml +18 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/scenario-compilation.yaml +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/session-replay.yaml +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/source-corpus-admission.yaml +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/server.ts +85 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/interlocking.ts +64 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/journey.ts +71 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/runner.ts +28 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/e2e/smoke/resolve-match.smoke.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/plan/_trains/_interlockings/match.yaml +9 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/server.ts +21 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/interlocking.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/journey.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/runner.ts +6 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/e2e/smoke/resolve-match.smoke.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/plan/_trains/_interlockings/match.yaml +9 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/server.ts +11 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/interlocking.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/journey.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/runner.ts +6 -0
- package/detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs +5 -0
- package/detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs +5 -1
- package/integrity.json +41 -7
- package/lib/journey-chain.mjs +677 -0
- package/package.json +3 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:create-world-pack
|
|
3
|
+
title: Create World Pack
|
|
4
|
+
status: draft
|
|
5
|
+
entrypoint:
|
|
6
|
+
interlocking_id: interlocking:source-corpus-admission
|
|
7
|
+
exposed: true
|
|
8
|
+
actions:
|
|
9
|
+
- create_world
|
|
10
|
+
reason: null
|
|
11
|
+
surfaces:
|
|
12
|
+
- backend
|
|
13
|
+
continuations: []
|
|
14
|
+
terminals:
|
|
15
|
+
- from:
|
|
16
|
+
interlocking_id: interlocking:source-corpus-admission
|
|
17
|
+
route_id: refuse
|
|
18
|
+
outcome: unsupported source refused
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:play-world-scenario
|
|
3
|
+
title: Play World Scenario
|
|
4
|
+
status: draft
|
|
5
|
+
entrypoint:
|
|
6
|
+
interlocking_id: interlocking:scenario-compilation
|
|
7
|
+
exposed: true
|
|
8
|
+
actions:
|
|
9
|
+
- create_scenario
|
|
10
|
+
reason: null
|
|
11
|
+
surfaces:
|
|
12
|
+
- backend
|
|
13
|
+
continuations:
|
|
14
|
+
- from:
|
|
15
|
+
interlocking_id: interlocking:scenario-compilation
|
|
16
|
+
route_id: compile
|
|
17
|
+
artifact: world:scenario:package
|
|
18
|
+
to:
|
|
19
|
+
interlocking_id: interlocking:play-turn-routing
|
|
20
|
+
terminals:
|
|
21
|
+
- from:
|
|
22
|
+
interlocking_id: interlocking:scenario-compilation
|
|
23
|
+
route_id: refuse
|
|
24
|
+
outcome: invalid Scenario refused
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:replay-world-session
|
|
3
|
+
title: Replay World Session
|
|
4
|
+
status: draft
|
|
5
|
+
entrypoint:
|
|
6
|
+
interlocking_id: interlocking:session-replay
|
|
7
|
+
exposed: true
|
|
8
|
+
actions:
|
|
9
|
+
- replay_session
|
|
10
|
+
reason: null
|
|
11
|
+
surfaces:
|
|
12
|
+
- backend
|
|
13
|
+
continuations: []
|
|
14
|
+
terminals:
|
|
15
|
+
- from:
|
|
16
|
+
interlocking_id: interlocking:session-replay
|
|
17
|
+
route_id: replay
|
|
18
|
+
outcome: recorded session replayed
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interlocking_id: interlocking:scenario-compilation
|
|
2
|
+
entrypoint:
|
|
3
|
+
exposed: true
|
|
4
|
+
actions:
|
|
5
|
+
- create_scenario
|
|
6
|
+
route_resolution:
|
|
7
|
+
strategy: fail_on_multiple_match
|
|
8
|
+
routes:
|
|
9
|
+
- route_id: compile
|
|
10
|
+
train_id: train:scenario:compile
|
|
11
|
+
train_path: plan/_trains/train:scenario:compile.yaml
|
|
12
|
+
guard:
|
|
13
|
+
id: guard:compile
|
|
14
|
+
input: scenario
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interlocking_id: interlocking:session-replay
|
|
2
|
+
entrypoint:
|
|
3
|
+
exposed: true
|
|
4
|
+
actions:
|
|
5
|
+
- replay_session
|
|
6
|
+
route_resolution:
|
|
7
|
+
strategy: fail_on_multiple_match
|
|
8
|
+
routes:
|
|
9
|
+
- route_id: replay
|
|
10
|
+
train_id: train:session:replay
|
|
11
|
+
train_path: plan/_trains/train:session:replay.yaml
|
|
12
|
+
guard:
|
|
13
|
+
id: guard:replay
|
|
14
|
+
input: replay
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interlocking_id: interlocking:source-corpus-admission
|
|
2
|
+
entrypoint:
|
|
3
|
+
exposed: true
|
|
4
|
+
actions:
|
|
5
|
+
- create_world
|
|
6
|
+
route_resolution:
|
|
7
|
+
strategy: fail_on_multiple_match
|
|
8
|
+
routes:
|
|
9
|
+
- route_id: refuse
|
|
10
|
+
train_id: train:source:refuse
|
|
11
|
+
train_path: plan/_trains/train:source:refuse.yaml
|
|
12
|
+
guard:
|
|
13
|
+
id: guard:refuse
|
|
14
|
+
input: source
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { JourneyRunner } from "./trains/journey.ts";
|
|
2
|
+
import { UnavailableWagonError, type WagonHandlerRegistry } from "./trains/runner.ts";
|
|
3
|
+
|
|
4
|
+
type StationRoute = Readonly<{ journeyId: string; path: string; interlockingId: string }>;
|
|
5
|
+
|
|
6
|
+
export const JOURNEY_MAP: Readonly<Record<string, StationRoute>> = {
|
|
7
|
+
create_world: {
|
|
8
|
+
journeyId: "journey:create-world-pack",
|
|
9
|
+
path: "plan/_journeys/create-world-pack.yaml",
|
|
10
|
+
interlockingId: "interlocking:source-corpus-admission",
|
|
11
|
+
},
|
|
12
|
+
create_scenario: {
|
|
13
|
+
journeyId: "journey:play-world-scenario",
|
|
14
|
+
path: "plan/_journeys/play-world-scenario.yaml",
|
|
15
|
+
interlockingId: "interlocking:scenario-compilation",
|
|
16
|
+
},
|
|
17
|
+
replay_session: {
|
|
18
|
+
journeyId: "journey:replay-world-session",
|
|
19
|
+
path: "plan/_journeys/replay-world-session.yaml",
|
|
20
|
+
interlockingId: "interlocking:session-replay",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const unavailableWagonHandlers: WagonHandlerRegistry = {};
|
|
25
|
+
|
|
26
|
+
// Source admission is the one composition with production handlers; it reports what it refused.
|
|
27
|
+
function sourceWagonHandlers(supported: boolean) {
|
|
28
|
+
let refusal: string | undefined;
|
|
29
|
+
return {
|
|
30
|
+
handlers: {
|
|
31
|
+
"train:source:refuse": () => {
|
|
32
|
+
if (!supported) refusal = "UNSUPPORTED_SOURCE_FORMAT";
|
|
33
|
+
return { refused: !supported };
|
|
34
|
+
},
|
|
35
|
+
} as WagonHandlerRegistry,
|
|
36
|
+
result: () => (refusal ? { refusal } : { admitted: true }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Production Station Master entrypoint for declared runtime journey actions. */
|
|
41
|
+
export async function executeAction(action: keyof typeof JOURNEY_MAP, inputs: Record<string, unknown>) {
|
|
42
|
+
const mapping = JOURNEY_MAP[action];
|
|
43
|
+
if (!mapping) throw new Error(`Station Master action is not declared: ${action}`);
|
|
44
|
+
const sourceRun = action === "create_world"
|
|
45
|
+
? sourceWagonHandlers(Boolean((inputs.source as { required_supported?: boolean } | undefined)?.required_supported))
|
|
46
|
+
: undefined;
|
|
47
|
+
try {
|
|
48
|
+
const execution = await new JourneyRunner(mapping.path).execute(action, inputs, {
|
|
49
|
+
initialArtifacts: action === "create_world" ? { "world:source:submission": inputs.source_submission } : {},
|
|
50
|
+
handlers: sourceRun?.handlers ?? unavailableWagonHandlers,
|
|
51
|
+
// Semantic continuations stay explicit deployment work; source remains bounded.
|
|
52
|
+
continueOnContinuation: false,
|
|
53
|
+
});
|
|
54
|
+
return sourceRun ? { ...execution, source: sourceRun.result() } : execution;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof UnavailableWagonError)
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
disposition: "INCOMPLETE_CONTINUATION" as const,
|
|
59
|
+
status: error.status,
|
|
60
|
+
trainId: error.trainId,
|
|
61
|
+
wagon: error.wagon,
|
|
62
|
+
});
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** HTTP-shaped Station Master for source admission. */
|
|
68
|
+
export async function stationMaster(command: { submission: { id: string }; supported: boolean }): Promise<Response> {
|
|
69
|
+
const execution = await executeAction("create_world", {
|
|
70
|
+
source: { required_supported: command.supported },
|
|
71
|
+
source_submission: command.submission,
|
|
72
|
+
});
|
|
73
|
+
if ("disposition" in execution)
|
|
74
|
+
return Response.json(execution, { status: execution.status });
|
|
75
|
+
const sourceResult = "source" in execution ? execution.source : undefined;
|
|
76
|
+
const refused = Boolean(sourceResult && typeof sourceResult === "object" && "refusal" in sourceResult);
|
|
77
|
+
return Response.json(sourceResult, {
|
|
78
|
+
status: refused ? 422 : 201,
|
|
79
|
+
headers: {
|
|
80
|
+
"x-journey-disposition": "TERMINAL",
|
|
81
|
+
"x-train-output-artifact": execution.terminal.artifact,
|
|
82
|
+
"x-train-wagons": execution.wagons.join(","),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { TrainRunner, type ExecutedTrain, type WagonHandlerRegistry } from "./runner.ts";
|
|
3
|
+
|
|
4
|
+
export type InterlockingResolution = Readonly<{
|
|
5
|
+
interlockingId: string;
|
|
6
|
+
routeId: string;
|
|
7
|
+
trainId: string;
|
|
8
|
+
trainPath: string;
|
|
9
|
+
category: string;
|
|
10
|
+
guardId: string;
|
|
11
|
+
resolutionStrategy: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type InterlockedExecution = Readonly<{ resolution: InterlockingResolution; executed: ExecutedTrain }>;
|
|
15
|
+
|
|
16
|
+
type Route = Readonly<{ route_id: string; train_id: string; train_path: string; guard: Readonly<{ id: string; input: string }> }>;
|
|
17
|
+
|
|
18
|
+
/** Resolves one route from the parsed declaration without transcribing route or train values. */
|
|
19
|
+
export class InterlockingRunner {
|
|
20
|
+
constructor(private readonly interlockingYamlPath: string) {}
|
|
21
|
+
|
|
22
|
+
async resolveTrain(action: string, inputs: Record<string, unknown>): Promise<InterlockingResolution> {
|
|
23
|
+
const declared = Bun.YAML.parse(readFileSync(this.interlockingYamlPath, "utf8")) as {
|
|
24
|
+
interlocking_id: string;
|
|
25
|
+
entrypoint: { actions: readonly string[] };
|
|
26
|
+
route_resolution: { strategy: string };
|
|
27
|
+
routes: readonly Route[];
|
|
28
|
+
};
|
|
29
|
+
if (!declared.entrypoint.actions.includes(action))
|
|
30
|
+
throw new Error(`action ${action} is not declared by ${declared.interlocking_id}`);
|
|
31
|
+
const matching = declared.routes.filter((route) => Boolean(inputs[route.guard.input]));
|
|
32
|
+
if (matching.length !== 1)
|
|
33
|
+
throw new Error(`${declared.interlocking_id} ${declared.route_resolution.strategy} selected ${matching.length} routes`);
|
|
34
|
+
const selected = matching[0]!;
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
interlockingId: declared.interlocking_id,
|
|
37
|
+
routeId: selected.route_id,
|
|
38
|
+
trainId: selected.train_id,
|
|
39
|
+
trainPath: selected.train_path,
|
|
40
|
+
category: "declared",
|
|
41
|
+
resolutionStrategy: declared.route_resolution.strategy,
|
|
42
|
+
guardId: selected.guard.id,
|
|
43
|
+
reason: `guard ${selected.guard.id} held`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Selects exactly one declared train, then delegates its wagon execution. */
|
|
48
|
+
async execute(
|
|
49
|
+
action: string,
|
|
50
|
+
inputs: Record<string, unknown>,
|
|
51
|
+
initialArtifacts: Record<string, unknown>,
|
|
52
|
+
handlers: WagonHandlerRegistry,
|
|
53
|
+
): Promise<InterlockedExecution> {
|
|
54
|
+
const selected = await this.resolveTrain(action, inputs);
|
|
55
|
+
// Selection occurs once and the selected YAML path determines execution.
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
resolution: selected,
|
|
58
|
+
executed: await new TrainRunner(selected.trainPath, handlers).execute(
|
|
59
|
+
selected,
|
|
60
|
+
initialArtifacts,
|
|
61
|
+
),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { InterlockingRunner } from "./interlocking.ts";
|
|
3
|
+
import type { ExecutedTrain, WagonHandlerRegistry } from "./runner.ts";
|
|
4
|
+
|
|
5
|
+
type Transition = Readonly<{
|
|
6
|
+
from: Readonly<{ interlocking_id: string; route_id: string }>;
|
|
7
|
+
to?: Readonly<{ interlocking_id: string }>;
|
|
8
|
+
artifact?: string;
|
|
9
|
+
outcome?: string;
|
|
10
|
+
}>;
|
|
11
|
+
type JourneyDeclaration = Readonly<{
|
|
12
|
+
entrypoint: Readonly<{ interlocking_id: string; actions: readonly string[] }>;
|
|
13
|
+
continuations: readonly Transition[];
|
|
14
|
+
terminals: readonly Transition[];
|
|
15
|
+
}>;
|
|
16
|
+
type ExecutionContext = Readonly<{
|
|
17
|
+
initialArtifacts: Record<string, unknown>;
|
|
18
|
+
handlers: WagonHandlerRegistry;
|
|
19
|
+
continueOnContinuation?: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
export type JourneyExecution = ExecutedTrain & Readonly<{ journey: Readonly<Record<string, unknown>> }>;
|
|
22
|
+
|
|
23
|
+
const pathFor = (interlockingId: string) =>
|
|
24
|
+
`plan/_trains/_interlockings/${interlockingId.replace("interlocking:", "")}.yaml`;
|
|
25
|
+
|
|
26
|
+
/** Delegates each declared journey boundary to InterlockingRunner. */
|
|
27
|
+
export class JourneyRunner {
|
|
28
|
+
constructor(private readonly journeyYamlPath: string) {}
|
|
29
|
+
|
|
30
|
+
/** Executes each declared route until the journey reaches one declared terminal. */
|
|
31
|
+
async execute(action: string, inputs: Record<string, unknown>, context: ExecutionContext): Promise<JourneyExecution> {
|
|
32
|
+
const declared = Bun.YAML.parse(readFileSync(this.journeyYamlPath, "utf8")) as JourneyDeclaration;
|
|
33
|
+
if (!declared?.entrypoint?.interlocking_id || !declared.entrypoint.actions?.includes(action))
|
|
34
|
+
throw new Error(`invalid journey entrypoint: ${this.journeyYamlPath}`);
|
|
35
|
+
let current = declared.entrypoint.interlocking_id;
|
|
36
|
+
let seed = context.initialArtifacts;
|
|
37
|
+
const traversed = [] as string[];
|
|
38
|
+
for (let hops = 0; hops < 32; hops++) {
|
|
39
|
+
const path = pathFor(current);
|
|
40
|
+
const run = await new InterlockingRunner(path).execute(action, inputs, seed, context.handlers);
|
|
41
|
+
const key = `${run.resolution.interlockingId}/${run.resolution.routeId}`;
|
|
42
|
+
traversed.push(run.resolution.interlockingId);
|
|
43
|
+
const continuation = declared.continuations.find(
|
|
44
|
+
(item) => item.from.interlocking_id === run.resolution.interlockingId && item.from.route_id === run.resolution.routeId,
|
|
45
|
+
);
|
|
46
|
+
const terminal = declared.terminals.find(
|
|
47
|
+
(item) => item.from.interlocking_id === run.resolution.interlockingId && item.from.route_id === run.resolution.routeId,
|
|
48
|
+
);
|
|
49
|
+
if (terminal) return Object.freeze({
|
|
50
|
+
...run.executed,
|
|
51
|
+
journey: Object.freeze({ terminal, outcome: terminal.outcome, interlockings: Object.freeze(traversed) }),
|
|
52
|
+
});
|
|
53
|
+
if (continuation && !context.continueOnContinuation)
|
|
54
|
+
// Deployment composition can stop at a declared boundary without fabricating its successor.
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
...run.executed,
|
|
57
|
+
journey: Object.freeze({ continuation, interlockings: Object.freeze(traversed) }),
|
|
58
|
+
});
|
|
59
|
+
if (continuation) {
|
|
60
|
+
if (!continuation.to?.interlocking_id || !continuation.artifact)
|
|
61
|
+
throw new Error(`invalid journey continuation at ${key}`);
|
|
62
|
+
current = continuation.to.interlocking_id;
|
|
63
|
+
// The next train receives only the planner-declared handoff from the prior execution.
|
|
64
|
+
seed = { [continuation.artifact]: run.executed.terminal.value };
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`journey route is not declared as a continuation or terminal: ${key}`);
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`journey continuation limit exceeded: ${this.journeyYamlPath}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ExecutedTrain = Readonly<{
|
|
2
|
+
selectedTrainId: string;
|
|
3
|
+
terminal: Readonly<{ artifact: string; value: unknown }>;
|
|
4
|
+
wagons: readonly string[];
|
|
5
|
+
}>;
|
|
6
|
+
export type WagonHandlerRegistry = Readonly<Record<string, (seed: Record<string, unknown>) => unknown>>;
|
|
7
|
+
|
|
8
|
+
/** A declared wagon whose handler this deployment has not composed. */
|
|
9
|
+
export class UnavailableWagonError extends Error {
|
|
10
|
+
readonly status = 202;
|
|
11
|
+
constructor(readonly trainId: string, readonly wagon: string) {
|
|
12
|
+
super(`wagon ${wagon} of ${trainId} is unavailable`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class TrainRunner {
|
|
17
|
+
constructor(private readonly trainPath: string, private readonly handlers: WagonHandlerRegistry) {}
|
|
18
|
+
async execute(resolution: Readonly<{ trainId: string }>, seed: Record<string, unknown>): Promise<ExecutedTrain> {
|
|
19
|
+
const wagon = this.trainPath.replace(/^plan\/_trains\/|\.yaml$/g, "");
|
|
20
|
+
const handler = this.handlers[wagon];
|
|
21
|
+
if (!handler) throw new UnavailableWagonError(resolution.trainId, wagon);
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
selectedTrainId: resolution.trainId,
|
|
24
|
+
terminal: Object.freeze({ artifact: `${wagon}:result`, value: handler(seed) }),
|
|
25
|
+
wagons: Object.freeze([wagon]),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -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,21 @@
|
|
|
1
|
+
import { JourneyRunner } from "./src/trains/journey";
|
|
2
|
+
import { InterlockingRunner } from "./src/trains/interlocking";
|
|
3
|
+
import { TrainRunner } from "./src/trains/runner";
|
|
4
|
+
|
|
5
|
+
export const JOURNEY_MAP = {
|
|
6
|
+
resolve_match: { journeyId: "journey:match", path: "plan/_journeys/match.yaml" },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// The production chain is importable and even exercised by a module-local helper, but the
|
|
10
|
+
// exported action returns business data directly instead of the JourneyRunner execution.
|
|
11
|
+
const runner = new InterlockingRunner("plan/_trains/_interlockings/match.yaml");
|
|
12
|
+
function inertProductionHelper(action: string, inputs: object, context: { handlers: object; seed: object }) {
|
|
13
|
+
const resolution = runner.resolveTrain(action, inputs);
|
|
14
|
+
return new TrainRunner(resolution.trainPath, context.handlers).execute(resolution, context.seed);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function dispatch(_action: keyof typeof JOURNEY_MAP, _inputs: object, _context: { handlers: object; seed: object }) {
|
|
18
|
+
void JourneyRunner;
|
|
19
|
+
void inertProductionHelper;
|
|
20
|
+
return { selectedTrainId: "train:match:nominal" };
|
|
21
|
+
}
|
|
@@ -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,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
|
+
});
|
package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/server.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { JourneyRunner } from "./src/trains/journey.test";
|
|
2
|
+
|
|
3
|
+
export const JOURNEY_MAP = {
|
|
4
|
+
resolve_match: { journeyId: "journey:match", path: "plan/_journeys/match.yaml" },
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Identical to the clean dispatch except that JourneyRunner resolves to a test-local module.
|
|
8
|
+
export function dispatch(action: keyof typeof JOURNEY_MAP, inputs: object, context: { handlers: object; seed: object }) {
|
|
9
|
+
const mapping = JOURNEY_MAP[action];
|
|
10
|
+
return new JourneyRunner(mapping.path).execute(action, inputs, context);
|
|
11
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
mk,
|
|
17
17
|
writeReport,
|
|
18
18
|
} from "../_shared/interlocking.mjs";
|
|
19
|
+
import { provenJourneyActions } from "../../../lib/journey-chain.mjs";
|
|
19
20
|
|
|
20
21
|
const RULE = "coder.bun.station-master-interlocking-routing";
|
|
21
22
|
const roots = parseJsonEnv("ATDD_SCAN_ROOTS", []);
|
|
@@ -37,6 +38,10 @@ for (const scanRoot of roots) {
|
|
|
37
38
|
const text = readText(app);
|
|
38
39
|
const jm = journeyMap(text);
|
|
39
40
|
if (!jm.hasInterlocking) continue; // pure direct-train Station Master carries no obligation.
|
|
41
|
+
// A journey-mediated Station Master delegates to the runners through JourneyRunner; when an exported
|
|
42
|
+
// action provably returns JourneyRunner -> InterlockingRunner -> TrainRunner execution (bound
|
|
43
|
+
// imports, live returns, no overrides), it need not reference the lower runners itself.
|
|
44
|
+
if (provenJourneyActions(text, app, croot).length > 0) continue;
|
|
40
45
|
|
|
41
46
|
const line = jm.interlockingLine || 1;
|
|
42
47
|
if (!referencesToken(text, "InterlockingRunner")) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
maskComments,
|
|
16
16
|
PLAN_ROOT,
|
|
17
17
|
} from "../_shared/interlocking.mjs";
|
|
18
|
+
import { provenJourneyActions } from "../../../lib/journey-chain.mjs";
|
|
18
19
|
|
|
19
20
|
const RULE = "coder.bun.station-master-journey-routing";
|
|
20
21
|
const roots = parseJsonEnv("ATDD_SCAN_ROOTS", []);
|
|
@@ -98,7 +99,10 @@ for (const scanRoot of roots) {
|
|
|
98
99
|
"journey-station-unlinked: exposed journeys exist but Station Master never references JourneyRunner",
|
|
99
100
|
lineAt(text, 1),
|
|
100
101
|
));
|
|
101
|
-
|
|
102
|
+
// Any exported action (not only `dispatch`) may carry the mapped JourneyRunner execution, provided
|
|
103
|
+
// the whole chain is proven on the AST (lib/journey-chain.mjs).
|
|
104
|
+
if (referencesToken(text, "JourneyRunner") && !dispatchInvokesMappedJourneyRunner(text) &&
|
|
105
|
+
provenJourneyActions(text, app, croot).length === 0) violations.push(mk(
|
|
102
106
|
RULE,
|
|
103
107
|
appPath,
|
|
104
108
|
1,
|