@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.
Files changed (44) hide show
  1. package/detectors/bun_interlocking_binding/checks/interlocking_runtime_executes.mjs +46 -1
  2. package/detectors/bun_interlocking_binding/detect.mjs +2 -2
  3. package/detectors/bun_interlocking_binding/fixtures/clean/server.ts +2 -2
  4. package/detectors/bun_interlocking_binding/fixtures/clean/src/trains/runner.ts +21 -3
  5. package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/server.ts +2 -2
  6. package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/src/trains/runner.ts +21 -3
  7. package/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +132 -2
  8. package/detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs +9 -4
  9. package/detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs +4 -4
  10. package/detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs +2 -1
  11. package/detectors/bun_interlocking_coverage/checks/interlocking_trace_binds.mjs +7 -5
  12. package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +8 -13
  13. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/convex/trains/interlocking.ts +1 -0
  14. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/convex/trains/runner.ts +1 -0
  15. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_route_coverage/convex/trains/interlocking.ts +1 -0
  16. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_route_coverage/convex/trains/runner.ts +1 -0
  17. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage/convex/trains/interlocking.ts +1 -0
  18. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage/convex/trains/runner.ts +1 -0
  19. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage_module/convex/trains/interlocking.ts +1 -0
  20. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage_module/convex/trains/runner.ts +1 -0
  21. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_trace_binds/convex/trains/interlocking.ts +1 -0
  22. package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_trace_binds/convex/trains/runner.ts +1 -0
  23. package/detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs +19 -0
  24. package/detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs +27 -0
  25. package/detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs +30 -0
  26. package/detectors/bun_interlocking_infrastructure/checks/wagon_contract_honoured.mjs +112 -12
  27. package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_runner_boundary/src/trains/journey.ts +17 -4
  28. package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_station_routing/src/trains/journey.ts +16 -3
  29. package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/plan/_journeys/checkout.yaml +19 -0
  30. package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/server.ts +13 -0
  31. package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/src/trains/journey.ts +36 -0
  32. package/detectors/bun_interlocking_infrastructure/fixtures/clean/wagon_contract_honoured/src/trains/runner.ts +5 -0
  33. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_dead_read/plan/_journeys/checkout.yaml +14 -0
  34. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_dead_read/src/trains/journey.ts +15 -0
  35. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_returns_early/plan/_journeys/checkout.yaml +19 -0
  36. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_returns_early/src/trains/journey.ts +16 -0
  37. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_self_loop/plan/_journeys/checkout.yaml +14 -0
  38. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_self_loop/src/trains/journey.ts +18 -0
  39. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/plan/_journeys/public.yaml +14 -0
  40. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/server.ts +16 -0
  41. package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/src/trains/journey.ts +9 -0
  42. package/integrity.json +43 -20
  43. package/package.json +1 -1
  44. 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((f) => ({ file: f, text: read(f) }));
130
- // A wagon implementation is one that moves artifacts through Cargo. Detected by
131
- // that behaviour rather than by filename — a `wagon.ts` naming heuristic is the
132
- // kind of fitted check this hub has had to unpick more than once.
133
- const wagonCode = sources.filter((s) => /\bCargo\b|\bcargo\s*\.\s*(?:put|get)\s*\(/.test(s.text));
134
- if (!wagonCode.length) continue; // nothing implemented to judge
135
-
136
- const blob = wagonCode.map((s) => maskComments(s.text)).join("\n");
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
- if (blob.includes(name)) continue;
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}", and no wagon implementation names it; the declared contract and ` +
148
- `the Cargo it actually moves are free to disagree`,
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
- const id = declaration.entrypoint.interlocking_id.replace("interlocking:", "");
11
- const path = `plan/_trains/_interlockings/${id}.yaml`;
12
- const runner = new InterlockingRunner(path);
13
- return runner.execute(action, inputs, state);
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
- const id = declaration.entrypoint.interlocking_id.replace("interlocking:", "");
11
- const path = `plan/_trains/_interlockings/${id}.yaml`;
12
- return new InterlockingRunner(path).execute(action, inputs, state);
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,5 @@
1
+ import { Cargo, runWagon } from "../orders/wagon";
2
+
3
+ export function runTrain(step: string, cargo: Cargo): void {
4
+ if (step === "confirm-order") runWagon(cargo);
5
+ }
@@ -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
+ }