@afokapu/atdd-bun 0.10.53 → 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 +43 -20
- package/package.json +1 -1
- package/src/hooks.ts +4 -3
|
@@ -56,6 +56,28 @@ function actionMapping(text, action) {
|
|
|
56
56
|
return { body: match[1], line: lineOfIndex(masked, match.index) };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function matchingBrace(text, open) {
|
|
60
|
+
let depth = 0;
|
|
61
|
+
for (let i = open; i < text.length; i++) {
|
|
62
|
+
if (text[i] === "{") depth++;
|
|
63
|
+
else if (text[i] === "}" && --depth === 0) return i;
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dispatchInvokesMappedJourneyRunner(text) {
|
|
69
|
+
const masked = maskComments(text);
|
|
70
|
+
const dispatch = /\b(?:async\s+)?function\s+dispatch\s*\([^)]*\)\s*\{/.exec(masked);
|
|
71
|
+
if (!dispatch) return false;
|
|
72
|
+
const open = dispatch.index + dispatch[0].length - 1;
|
|
73
|
+
const close = matchingBrace(masked, open);
|
|
74
|
+
const body = masked.slice(open + 1, close < 0 ? masked.length : close);
|
|
75
|
+
const direct = /\breturn\s+(?:await\s+)?new\s+JourneyRunner\s*\(\s*[A-Za-z_$][\w$]*\s*\.\s*path\s*\)\s*\.\s*execute\s*\(/.test(body);
|
|
76
|
+
const constructed = /\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+JourneyRunner\s*\(\s*[A-Za-z_$][\w$]*\s*\.\s*path\s*\)/.exec(body);
|
|
77
|
+
const delegated = constructed && new RegExp("\\breturn\\s+(?:await\\s+)?" + constructed[1] + "\\s*\\.\\s*execute\\s*\\(").test(body);
|
|
78
|
+
return direct || delegated;
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
for (const scanRoot of roots) {
|
|
60
82
|
for (const croot of findConsumerRoots(scanRoot)) {
|
|
61
83
|
const journeys = journeyDocuments(croot).filter(item => item.data?.entrypoint?.exposed === true);
|
|
@@ -76,6 +98,14 @@ for (const scanRoot of roots) {
|
|
|
76
98
|
"journey-station-unlinked: exposed journeys exist but Station Master never references JourneyRunner",
|
|
77
99
|
lineAt(text, 1),
|
|
78
100
|
));
|
|
101
|
+
if (referencesToken(text, "JourneyRunner") && !dispatchInvokesMappedJourneyRunner(text)) violations.push(mk(
|
|
102
|
+
RULE,
|
|
103
|
+
appPath,
|
|
104
|
+
1,
|
|
105
|
+
0,
|
|
106
|
+
"journey-station-dispatch-bypasses-runner: Station Master dispatch must return a JourneyRunner execution for the mapped exposed journey",
|
|
107
|
+
lineAt(text, 1),
|
|
108
|
+
));
|
|
79
109
|
|
|
80
110
|
for (const journey of journeys) {
|
|
81
111
|
const id = String(journey.data.journey_id ?? "");
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// CONTRACT (v1.1): reads ATDD_SCAN_ROOTS / ATDD_SCAN_EXCLUDES, writes RAW violations to
|
|
25
25
|
// ATDD_VIOLATIONS_REPORT, exits 0 regardless of count.
|
|
26
26
|
import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
27
|
-
import { join, sep } from "node:path";
|
|
27
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
28
28
|
|
|
29
29
|
const RULE = "coder.bun.wagon-honours-its-contract";
|
|
30
30
|
const EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
|
|
@@ -101,6 +101,95 @@ export function maskComments(text) {
|
|
|
101
101
|
.replace(/(^|[^:\\])\/\/[^\n]*/g, (m, p1) => p1 + " ".repeat(m.length - p1.length));
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
105
|
+
|
|
106
|
+
function closingBrace(text, open) {
|
|
107
|
+
let depth = 0;
|
|
108
|
+
for (let i = open; i < text.length; i++) {
|
|
109
|
+
if (text[i] === "{") depth++;
|
|
110
|
+
else if (text[i] === "}" && --depth === 0) return i;
|
|
111
|
+
}
|
|
112
|
+
return -1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function handlerBody(text, name) {
|
|
116
|
+
const escaped = escape(name);
|
|
117
|
+
const forms = [
|
|
118
|
+
new RegExp(`\\b(?:export\\s+)?(?:async\\s+)?function\\s+${escaped}\\s*\\([^)]*\\)\\s*(?::[^={]+)?\\{`),
|
|
119
|
+
new RegExp(`\\b(?:export\\s+)?(?:const|let)\\s+${escaped}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{`),
|
|
120
|
+
];
|
|
121
|
+
for (const form of forms) {
|
|
122
|
+
const match = form.exec(text);
|
|
123
|
+
if (!match) continue;
|
|
124
|
+
const open = match.index + match[0].lastIndexOf("{");
|
|
125
|
+
const close = closingBrace(text, open);
|
|
126
|
+
if (close >= 0) return text.slice(open + 1, close);
|
|
127
|
+
}
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function localConstants(text) {
|
|
132
|
+
const values = new Map();
|
|
133
|
+
const re = /\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*["']([^"']+)["']/g;
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = re.exec(text)) !== null) values.set(match[1], match[2]);
|
|
136
|
+
return values;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function artifactArgument(argument, constants) {
|
|
140
|
+
const literal = /^\s*["']([^"']+)["']\s*$/.exec(argument);
|
|
141
|
+
if (literal) return literal[1];
|
|
142
|
+
const identifier = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(argument);
|
|
143
|
+
return identifier ? constants.get(identifier[1]) ?? null : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cargoMoves(handler, module) {
|
|
147
|
+
const constants = localConstants(module), reads = new Set(), writes = new Set();
|
|
148
|
+
for (const [method, target] of [["get", reads], ["put", writes]]) {
|
|
149
|
+
const re = new RegExp(`\\bcargo\\s*\\.\\s*${method}\\s*\\(\\s*([^,)]*)`, "g");
|
|
150
|
+
let match;
|
|
151
|
+
while ((match = re.exec(handler)) !== null) {
|
|
152
|
+
const artifact = artifactArgument(match[1], constants);
|
|
153
|
+
if (artifact) target.add(artifact);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { reads, writes };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function importedHandlers(runner) {
|
|
160
|
+
const handlers = [];
|
|
161
|
+
const re = /^\s*import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/gm;
|
|
162
|
+
let match;
|
|
163
|
+
while ((match = re.exec(runner.text)) !== null) {
|
|
164
|
+
if (!match[2].startsWith(".")) continue;
|
|
165
|
+
const target = resolve(dirname(runner.file), match[2]);
|
|
166
|
+
const file = [target, `${target}.ts`, `${target}.tsx`, join(target, "index.ts")]
|
|
167
|
+
.find((candidate) => runner.sources.has(candidate));
|
|
168
|
+
if (!file) continue;
|
|
169
|
+
for (const entry of match[1].split(",")) {
|
|
170
|
+
const [imported, local = imported] = entry.trim().split(/\s+as\s+/);
|
|
171
|
+
if (imported && local && imported !== "Cargo") handlers.push({ name: local.trim(), file });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return handlers;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function executedHandlers(wagon, runners, sources) {
|
|
178
|
+
const handlers = [];
|
|
179
|
+
const wagonLiteral = escape(wagon);
|
|
180
|
+
for (const runner of runners) {
|
|
181
|
+
const imports = importedHandlers({ ...runner, sources });
|
|
182
|
+
for (const handler of imports) {
|
|
183
|
+
const branch = new RegExp(
|
|
184
|
+
`\\b(?:if\\s*\\(\\s*step\\s*={2,3}\\s*["']${wagonLiteral}["']\\s*\\)|case\\s*["']${wagonLiteral}["']\\s*:)` +
|
|
185
|
+
`[\\s\\S]{0,240}?\\b${escape(handler.name)}\\s*\\(\\s*cargo\\b`,
|
|
186
|
+
);
|
|
187
|
+
if (branch.test(runner.text)) handlers.push(handler);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return handlers;
|
|
191
|
+
}
|
|
192
|
+
|
|
104
193
|
const reportPath = process.env.ATDD_VIOLATIONS_REPORT;
|
|
105
194
|
if (!reportPath) {
|
|
106
195
|
process.stderr.write("bun-infra: ATDD_VIOLATIONS_REPORT not set\n");
|
|
@@ -126,26 +215,37 @@ for (const root of parseJsonEnv("ATDD_SCAN_ROOTS", [])) {
|
|
|
126
215
|
if (!contracts.length) continue; // no declared contract
|
|
127
216
|
|
|
128
217
|
const sources = walk(croot, (p) => TS.test(p) && !TEST.test(p), excludes)
|
|
129
|
-
.map((
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
218
|
+
.map((file) => ({ file, text: maskComments(read(file)) }));
|
|
219
|
+
// Do not turn a plan-only repository into a contract failure. Once this consumer
|
|
220
|
+
// does have Cargo-shaped source, however, each carried wagon must bind its own
|
|
221
|
+
// handler into the train path; a global source blob is not evidence of that.
|
|
222
|
+
if (!sources.some((s) => /\bCargo\b|\bcargo\s*\.\s*(?:put|get)\s*\(/.test(s.text))) continue;
|
|
223
|
+
const sourceByFile = new Map(sources.map((source) => [source.file, source.text]));
|
|
224
|
+
const trainRoot = join(croot, "src", "trains") + sep;
|
|
225
|
+
const runners = sources.filter((source) => source.file.startsWith(trainRoot));
|
|
137
226
|
for (const c of contracts) {
|
|
138
227
|
if (c.wagon && !carried.has(c.wagon)) continue; // no train carries this wagon yet
|
|
228
|
+
const handlers = c.wagon ? executedHandlers(c.wagon, runners, sourceByFile) : [];
|
|
229
|
+
const reads = new Set(), writes = new Set();
|
|
230
|
+
for (const handler of handlers) {
|
|
231
|
+
const module = sourceByFile.get(handler.file) ?? "";
|
|
232
|
+
const body = handlerBody(module, handler.name);
|
|
233
|
+
const moves = cargoMoves(body, module);
|
|
234
|
+
for (const artifact of moves.reads) reads.add(artifact);
|
|
235
|
+
for (const artifact of moves.writes) writes.add(artifact);
|
|
236
|
+
}
|
|
139
237
|
for (const [kind, names] of [["produce", c.produce], ["consume", c.consume]]) {
|
|
140
238
|
for (const name of names) {
|
|
141
|
-
|
|
239
|
+
const moved = kind === "produce" ? writes : reads;
|
|
240
|
+
if (moved.has(name)) continue;
|
|
142
241
|
const rel = c.file.startsWith(croot + sep) ? c.file.slice(croot.length + 1) : c.file;
|
|
143
242
|
violations.push({
|
|
144
243
|
rule_id: RULE, file: rel, line: 1, col: 0,
|
|
145
244
|
evidence:
|
|
146
245
|
`the wagon declares it ${kind === "produce" ? "produces" : "consumes"} ` +
|
|
147
|
-
`"${name}",
|
|
148
|
-
|
|
246
|
+
`"${name}", but no handler reachable from that wagon's train step ` +
|
|
247
|
+
`${kind === "produce" ? "writes" : "reads"} it through Cargo; artifact constants ` +
|
|
248
|
+
`or unrelated Cargo code do not honour the wagon contract`,
|
|
149
249
|
source_line: "",
|
|
150
250
|
});
|
|
151
251
|
}
|
|
@@ -6,10 +6,23 @@ export class JourneyRunner {
|
|
|
6
6
|
async execute(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
7
7
|
const declaration = Bun.YAML.parse(await Bun.file(this.journeyYamlPath).text()) as {
|
|
8
8
|
entrypoint: { interlocking_id: string };
|
|
9
|
+
continuations: Array<{ from: { interlocking_id: string; route_id: string }; to: { interlocking_id: string } }>;
|
|
10
|
+
terminals: Array<{ from: { interlocking_id: string; route_id: string }; outcome: string }>;
|
|
9
11
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
let current = declaration.entrypoint.interlocking_id;
|
|
13
|
+
for (;;) {
|
|
14
|
+
const id = current.replace("interlocking:", "");
|
|
15
|
+
const path = `plan/_trains/_interlockings/${id}.yaml`;
|
|
16
|
+
const runner = new InterlockingRunner(path);
|
|
17
|
+
const resolution = await runner.execute(action, inputs, state);
|
|
18
|
+
const continuation = declaration.continuations.find(edge => edge.from.interlocking_id === current && edge.from.route_id === resolution.routeId);
|
|
19
|
+
if (continuation) {
|
|
20
|
+
current = continuation.to.interlocking_id;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const terminal = declaration.terminals.find(edge => edge.from.interlocking_id === current && edge.from.route_id === resolution.routeId);
|
|
24
|
+
if (terminal) return { resolution, outcome: terminal.outcome };
|
|
25
|
+
throw new Error(`Journey topology has no outcome for ${current}`);
|
|
26
|
+
}
|
|
14
27
|
}
|
|
15
28
|
}
|
|
@@ -6,9 +6,22 @@ export class JourneyRunner {
|
|
|
6
6
|
async execute(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
7
7
|
const declaration = Bun.YAML.parse(await Bun.file(this.journeyYamlPath).text()) as {
|
|
8
8
|
entrypoint: { interlocking_id: string };
|
|
9
|
+
continuations: Array<{ from: { interlocking_id: string; route_id: string }; to: { interlocking_id: string } }>;
|
|
10
|
+
terminals: Array<{ from: { interlocking_id: string; route_id: string }; outcome: string }>;
|
|
9
11
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
let current = declaration.entrypoint.interlocking_id;
|
|
13
|
+
for (;;) {
|
|
14
|
+
const id = current.replace("interlocking:", "");
|
|
15
|
+
const path = `plan/_trains/_interlockings/${id}.yaml`;
|
|
16
|
+
const resolution = await new InterlockingRunner(path).execute(action, inputs, state);
|
|
17
|
+
const continuation = declaration.continuations.find(edge => edge.from.interlocking_id === current && edge.from.route_id === resolution.routeId);
|
|
18
|
+
if (continuation) {
|
|
19
|
+
current = continuation.to.interlocking_id;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const terminal = declaration.terminals.find(edge => edge.from.interlocking_id === current && edge.from.route_id === resolution.routeId);
|
|
23
|
+
if (terminal) return { resolution, outcome: terminal.outcome };
|
|
24
|
+
throw new Error(`Journey topology has no outcome for ${current}`);
|
|
25
|
+
}
|
|
13
26
|
}
|
|
14
27
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:checkout
|
|
3
|
+
title: Checkout
|
|
4
|
+
status: checked
|
|
5
|
+
entrypoint:
|
|
6
|
+
interlocking_id: interlocking:cart
|
|
7
|
+
exposed: true
|
|
8
|
+
actions: [checkout]
|
|
9
|
+
reason: null
|
|
10
|
+
surfaces: [backend]
|
|
11
|
+
continuations:
|
|
12
|
+
- from: { interlocking_id: interlocking:cart, route_id: ready }
|
|
13
|
+
artifact: order:prepared
|
|
14
|
+
to: { interlocking_id: interlocking:payment }
|
|
15
|
+
terminals:
|
|
16
|
+
- from: { interlocking_id: interlocking:cart, route_id: empty }
|
|
17
|
+
outcome: empty-cart
|
|
18
|
+
- from: { interlocking_id: interlocking:payment, route_id: paid }
|
|
19
|
+
outcome: completed
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { JourneyRunner } from "./src/trains/journey";
|
|
2
|
+
|
|
3
|
+
export const JOURNEY_MAP = {
|
|
4
|
+
checkout: {
|
|
5
|
+
journeyId: "journey:checkout",
|
|
6
|
+
path: "plan/_journeys/checkout.yaml",
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function dispatch(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
11
|
+
const mapping = JOURNEY_MAP[action as keyof typeof JOURNEY_MAP];
|
|
12
|
+
return new JourneyRunner(mapping.path).execute(action, inputs, state);
|
|
13
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { InterlockingRunner } from "@app/interlocking-runtime";
|
|
2
|
+
|
|
3
|
+
type Topology = {
|
|
4
|
+
entrypoint: { interlocking_id: string };
|
|
5
|
+
continuations: Array<{
|
|
6
|
+
from: { interlocking_id: string; route_id: string };
|
|
7
|
+
artifact: string;
|
|
8
|
+
to: { interlocking_id: string };
|
|
9
|
+
}>;
|
|
10
|
+
terminals: Array<{
|
|
11
|
+
from: { interlocking_id: string; route_id: string };
|
|
12
|
+
outcome: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class JourneyRunner {
|
|
17
|
+
constructor(private readonly journeyYamlPath: string) {}
|
|
18
|
+
|
|
19
|
+
async execute(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
20
|
+
const declaration = Bun.YAML.parse(await Bun.file(this.journeyYamlPath).text()) as Topology;
|
|
21
|
+
let current = declaration.entrypoint.interlocking_id;
|
|
22
|
+
for (;;) {
|
|
23
|
+
const id = current.replace("interlocking:", "");
|
|
24
|
+
const path = `plan/_trains/_interlockings/${id}.yaml`;
|
|
25
|
+
const resolution = await new InterlockingRunner(path).execute(action, inputs, state);
|
|
26
|
+
const continuation = declaration.continuations.find(edge => edge.from.interlocking_id === current && edge.from.route_id === resolution.routeId);
|
|
27
|
+
if (continuation) {
|
|
28
|
+
current = continuation.to.interlocking_id;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const terminal = declaration.terminals.find(edge => edge.from.interlocking_id === current && edge.from.route_id === resolution.routeId);
|
|
32
|
+
if (terminal) return { resolution, outcome: terminal.outcome };
|
|
33
|
+
throw new Error(`Journey topology has no outcome for ${current}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:checkout
|
|
3
|
+
entrypoint:
|
|
4
|
+
interlocking_id: interlocking:cart
|
|
5
|
+
exposed: false
|
|
6
|
+
actions: []
|
|
7
|
+
surfaces: [backend]
|
|
8
|
+
continuations:
|
|
9
|
+
- from: { interlocking_id: interlocking:cart, route_id: ready }
|
|
10
|
+
artifact: order:prepared
|
|
11
|
+
to: { interlocking_id: interlocking:payment }
|
|
12
|
+
terminals:
|
|
13
|
+
- from: { interlocking_id: interlocking:payment, route_id: paid }
|
|
14
|
+
outcome: completed
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { InterlockingRunner } from "@app/interlocking-runtime";
|
|
2
|
+
|
|
3
|
+
export class JourneyRunner {
|
|
4
|
+
constructor(private readonly journeyYamlPath: string) {}
|
|
5
|
+
|
|
6
|
+
async execute(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
7
|
+
const declaration = Bun.YAML.parse(await Bun.file(this.journeyYamlPath).text()) as any;
|
|
8
|
+
const id = declaration.entrypoint.interlocking_id.replace("interlocking:", "");
|
|
9
|
+
const path = `plan/_trains/_interlockings/${id}.yaml`;
|
|
10
|
+
const resolution = await new InterlockingRunner(path).execute(action, inputs, state);
|
|
11
|
+
declaration.continuations.find((edge: any) => edge.from.route_id === resolution.routeId);
|
|
12
|
+
declaration.terminals.find((edge: any) => edge.from.route_id === resolution.routeId);
|
|
13
|
+
return resolution;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:checkout
|
|
3
|
+
title: Checkout
|
|
4
|
+
status: checked
|
|
5
|
+
entrypoint:
|
|
6
|
+
interlocking_id: interlocking:cart
|
|
7
|
+
exposed: false
|
|
8
|
+
actions: []
|
|
9
|
+
reason: internal-transition-only
|
|
10
|
+
surfaces: [backend]
|
|
11
|
+
continuations:
|
|
12
|
+
- from: { interlocking_id: interlocking:cart, route_id: ready }
|
|
13
|
+
artifact: order:prepared
|
|
14
|
+
to: { interlocking_id: interlocking:payment }
|
|
15
|
+
terminals:
|
|
16
|
+
- from: { interlocking_id: interlocking:cart, route_id: empty }
|
|
17
|
+
outcome: empty-cart
|
|
18
|
+
- from: { interlocking_id: interlocking:payment, route_id: paid }
|
|
19
|
+
outcome: completed
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { InterlockingRunner } from "@app/interlocking-runtime";
|
|
2
|
+
|
|
3
|
+
export class JourneyRunner {
|
|
4
|
+
constructor(private readonly journeyYamlPath: string) {}
|
|
5
|
+
|
|
6
|
+
async execute(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
7
|
+
const declaration = Bun.YAML.parse(await Bun.file(this.journeyYamlPath).text()) as {
|
|
8
|
+
entrypoint: { interlocking_id: string };
|
|
9
|
+
continuations: unknown[];
|
|
10
|
+
terminals: unknown[];
|
|
11
|
+
};
|
|
12
|
+
const id = declaration.entrypoint.interlocking_id.replace("interlocking:", "");
|
|
13
|
+
const path = `plan/_trains/_interlockings/${id}.yaml`;
|
|
14
|
+
return new InterlockingRunner(path).execute(action, inputs, state);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:checkout
|
|
3
|
+
entrypoint:
|
|
4
|
+
interlocking_id: interlocking:cart
|
|
5
|
+
exposed: false
|
|
6
|
+
actions: []
|
|
7
|
+
surfaces: [backend]
|
|
8
|
+
continuations:
|
|
9
|
+
- from: { interlocking_id: interlocking:cart, route_id: ready }
|
|
10
|
+
artifact: order:prepared
|
|
11
|
+
to: { interlocking_id: interlocking:payment }
|
|
12
|
+
terminals:
|
|
13
|
+
- from: { interlocking_id: interlocking:payment, route_id: paid }
|
|
14
|
+
outcome: completed
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { InterlockingRunner } from "@app/interlocking-runtime";
|
|
2
|
+
|
|
3
|
+
export class JourneyRunner {
|
|
4
|
+
constructor(private readonly journeyYamlPath: string) {}
|
|
5
|
+
|
|
6
|
+
async execute(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
7
|
+
const declaration = Bun.YAML.parse(await Bun.file(this.journeyYamlPath).text()) as any;
|
|
8
|
+
let current = declaration.entrypoint.interlocking_id;
|
|
9
|
+
for (;;) {
|
|
10
|
+
const id = current.replace("interlocking:", "");
|
|
11
|
+
const resolution = await new InterlockingRunner(`plan/_trains/_interlockings/${id}.yaml`).execute(action, inputs, state);
|
|
12
|
+
const continuation = declaration.continuations.find((edge: any) => edge.from.route_id === resolution.routeId);
|
|
13
|
+
if (continuation) { current = current; continue; }
|
|
14
|
+
const terminal = declaration.terminals.find((edge: any) => edge.from.route_id === resolution.routeId);
|
|
15
|
+
if (terminal) return resolution;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
journey_id: journey:public
|
|
3
|
+
title: Public journey
|
|
4
|
+
status: checked
|
|
5
|
+
entrypoint:
|
|
6
|
+
interlocking_id: interlocking:first
|
|
7
|
+
exposed: true
|
|
8
|
+
actions: [start_journey]
|
|
9
|
+
reason: null
|
|
10
|
+
surfaces: [backend]
|
|
11
|
+
continuations: []
|
|
12
|
+
terminals:
|
|
13
|
+
- from: { interlocking_id: interlocking:first, route_id: done }
|
|
14
|
+
outcome: completed
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JourneyRunner } from "./src/trains/journey";
|
|
2
|
+
import { createOrder } from "./src/orders/create";
|
|
3
|
+
|
|
4
|
+
export const JOURNEY_MAP = {
|
|
5
|
+
start_journey: {
|
|
6
|
+
journeyId: "journey:public",
|
|
7
|
+
path: "plan/_journeys/public.yaml",
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function dispatch(action: string, inputs: Record<string, unknown>) {
|
|
12
|
+
const mapping = JOURNEY_MAP[action as keyof typeof JOURNEY_MAP];
|
|
13
|
+
void JourneyRunner;
|
|
14
|
+
void mapping;
|
|
15
|
+
return createOrder(inputs);
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InterlockingRunner } from "@app/interlocking-runtime";
|
|
2
|
+
|
|
3
|
+
export class JourneyRunner {
|
|
4
|
+
constructor(private readonly journeyYamlPath: string) {}
|
|
5
|
+
|
|
6
|
+
execute(action: string, inputs: Record<string, unknown>, state?: unknown) {
|
|
7
|
+
return new InterlockingRunner(this.journeyYamlPath).execute(action, inputs, state);
|
|
8
|
+
}
|
|
9
|
+
}
|