automategraph 1.5.1 → 1.5.3

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 (88) hide show
  1. package/CHANGELOG.md +18 -5
  2. package/UPGRADE-PROPOSAL.md +141 -0
  3. package/docs/diagrams/c19-resilience-scenario.md +61 -0
  4. package/docs/diagrams/c19-resilience-scenario.mmd +28 -0
  5. package/docs/diagrams/c19-resilience-scenario.svg +181 -0
  6. package/examples/README.md +206 -0
  7. package/examples/branching-edges.ts +192 -0
  8. package/examples/branching-graph.ts +333 -0
  9. package/examples/branching-router.ts +475 -0
  10. package/examples/json-graph.ts +231 -0
  11. package/examples/json-scripts.ts +152 -0
  12. package/examples/order-flow.functions.cjs +32 -0
  13. package/examples/order-flow.graph.json +27 -0
  14. package/examples/order-flow.scripted.graph.json +68 -0
  15. package/js/examples/branching-edges.js +133 -0
  16. package/js/examples/branching-edges.js.map +1 -0
  17. package/js/examples/branching-graph.js +232 -0
  18. package/js/examples/branching-graph.js.map +1 -0
  19. package/js/examples/branching-router.js +305 -0
  20. package/js/examples/branching-router.js.map +1 -0
  21. package/js/examples/json-graph.js +197 -0
  22. package/js/examples/json-graph.js.map +1 -0
  23. package/js/examples/json-scripts.js +131 -0
  24. package/js/examples/json-scripts.js.map +1 -0
  25. package/js/tests/async.test.js +592 -0
  26. package/js/tests/async.test.js.map +1 -0
  27. package/js/tests/complex.test.js +972 -0
  28. package/js/tests/complex.test.js.map +1 -0
  29. package/js/tests/conformance.test.js +365 -0
  30. package/js/tests/conformance.test.js.map +1 -0
  31. package/js/tests/edges.test.js +702 -0
  32. package/js/tests/edges.test.js.map +1 -0
  33. package/js/tests/graphjson.test.js +504 -0
  34. package/js/tests/graphjson.test.js.map +1 -0
  35. package/js/tests/harness.js +192 -0
  36. package/js/tests/harness.js.map +1 -0
  37. package/js/tests/helpers.js +136 -0
  38. package/js/tests/helpers.js.map +1 -0
  39. package/js/tests/lint.test.js +317 -0
  40. package/js/tests/lint.test.js.map +1 -0
  41. package/js/tests/registry.test.js +705 -0
  42. package/js/tests/registry.test.js.map +1 -0
  43. package/js/tests/report.js +422 -0
  44. package/js/tests/report.js.map +1 -0
  45. package/js/tests/runner.js +67 -0
  46. package/js/tests/runner.js.map +1 -0
  47. package/js/tests/scripts.test.js +552 -0
  48. package/js/tests/scripts.test.js.map +1 -0
  49. package/js/tests/unit.test.js +1239 -0
  50. package/js/tests/unit.test.js.map +1 -0
  51. package/js/tests/upgrade.test.js +618 -0
  52. package/js/tests/upgrade.test.js.map +1 -0
  53. package/package.json +2 -9
  54. package/report/index.html +3727 -0
  55. package/report/report.json +3053 -0
  56. package/tests/async.test.ts +668 -0
  57. package/tests/complex.test.ts +1123 -0
  58. package/tests/conformance.test.ts +442 -0
  59. package/tests/edges.test.ts +848 -0
  60. package/tests/fixtures/scripts/bad-registration.cjs +4 -0
  61. package/tests/fixtures/scripts/broken.cjs +2 -0
  62. package/tests/fixtures/scripts/cjs-default.js +4 -0
  63. package/tests/fixtures/scripts/collide-a.cjs +4 -0
  64. package/tests/fixtures/scripts/collide-b.cjs +4 -0
  65. package/tests/fixtures/scripts/empty.cjs +2 -0
  66. package/tests/fixtures/scripts/esm-functions.mjs +10 -0
  67. package/tests/fixtures/scripts/esm-syntax.js +3 -0
  68. package/tests/fixtures/scripts/global-channel.cjs +6 -0
  69. package/tests/fixtures/scripts/global-channel.mjs +7 -0
  70. package/tests/fixtures/scripts/mismatch.cjs +4 -0
  71. package/tests/fixtures/scripts/needs-sibling.cjs +7 -0
  72. package/tests/fixtures/scripts/order.cjs +14 -0
  73. package/tests/fixtures/scripts/sibling-helper.cjs +3 -0
  74. package/tests/fixtures/scripts/throwing-global.mjs +6 -0
  75. package/tests/graphjson.test.ts +737 -0
  76. package/tests/harness.ts +291 -0
  77. package/tests/helpers.ts +216 -0
  78. package/tests/lint.test.ts +381 -0
  79. package/tests/registry.test.ts +1134 -0
  80. package/tests/report.ts +452 -0
  81. package/tests/runner.ts +58 -0
  82. package/tests/scripts.test.ts +970 -0
  83. package/tests/unit.test.ts +1192 -0
  84. package/tests/upgrade.test.ts +725 -0
  85. package/tools/server.mjs +152 -0
  86. package/tsconfig.browser.json +19 -0
  87. package/tsconfig.json +18 -0
  88. package/tsconfig.publish.json +13 -0
@@ -0,0 +1,152 @@
1
+ /* ============================================================
2
+ * A document that brings its own code (v1.5)
3
+ * ------------------------------------------------------------
4
+ * order-flow.scripted.graph.json declares WHICH functions it needs and
5
+ * WHERE they come from:
6
+ *
7
+ * scripts: [
8
+ * { "namespace": "order", "path": "./order-flow.functions.cjs" },
9
+ * { "namespace": "audit", "source": "…registerEngineNamespace('audit', …)" }
10
+ * ]
11
+ *
12
+ * Two strategies in one document:
13
+ * • `order` — a CommonJS FILE whose export table is the contribution
14
+ * • `audit` — INLINE source that registers itself from inside the
15
+ * script through globalThis.GraphEngine (the loader
16
+ * installs that global for the duration of the load only)
17
+ *
18
+ * The pipeline stays two-step:
19
+ *
20
+ * const { functions } = await loadGraphScripts(document);
21
+ * const engine = createEngineFromJSON(document, { functions });
22
+ *
23
+ * Run: npm run example:scripts (after npm run build)
24
+ * ============================================================ */
25
+
26
+ import * as fs from "fs";
27
+ import * as path from "path";
28
+
29
+ import {
30
+ GraphScriptError,
31
+ loadGraphScripts,
32
+ registeredNamespaces,
33
+ resetGraphScriptNamespaces
34
+ } from "../src/node/graph-scripts";
35
+ import { createEngineFromJSON } from "../src/graph-json";
36
+
37
+ const EXAMPLES = path.join(__dirname, "..", "..", "examples");
38
+ const DOCUMENT_PATH = path.join(
39
+ EXAMPLES,
40
+ "order-flow.scripted.graph.json"
41
+ );
42
+
43
+ async function main(): Promise<void> {
44
+ const document = fs.readFileSync(DOCUMENT_PATH, "utf8");
45
+
46
+ console.log("=".repeat(96));
47
+ console.log(
48
+ `graph with scripts — ${path.basename(DOCUMENT_PATH)} ` +
49
+ "(functions live in the document's own scripts)"
50
+ );
51
+ console.log("=".repeat(96));
52
+
53
+ /* ---------- 1. load ---------- */
54
+
55
+ const loaded = await loadGraphScripts(document, {
56
+ baseDir: EXAMPLES,
57
+ onLog: (level, message) => {
58
+ if (level === "warn" || level === "error") console.log(` log[${level}] ${message}`);
59
+ }
60
+ });
61
+
62
+ for (const script of loaded.scripts) {
63
+ console.log(`\n▶ namespace "${script.namespace}" (${script.source}, ${script.format})`);
64
+ console.log(` provided: ${script.names.join(", ")}`);
65
+ if (script.ignoredExports.length > 0) {
66
+ console.log(` ignored (not functions): ${script.ignoredExports.join(", ")}`);
67
+ }
68
+ }
69
+
70
+ console.log(
71
+ `\nmerged registry: ${loaded.registry.names.length} functions, ` +
72
+ `fingerprint ${loaded.registry.fingerprint}`
73
+ );
74
+
75
+ /* ---------- 2. run ---------- */
76
+
77
+ const engine = createEngineFromJSON(document, {
78
+ functions: loaded.functions,
79
+ lint: "error"
80
+ });
81
+
82
+ const report = await engine.run();
83
+
84
+ const path2 = report.results
85
+ .filter(result => result.status === "SUCCEEDED")
86
+ .map(result => result.nodeId)
87
+ .join(" → ");
88
+
89
+ console.log("\n" + "-".repeat(96));
90
+ console.log(
91
+ `run: ${report.status} path: ${path2}` +
92
+ ` ${path2 === "classify → audit → charge → ship" ? "✓" : "✗ expected classify → audit → charge → ship"}`
93
+ );
94
+
95
+ for (const result of report.results) {
96
+ console.log(
97
+ ` ${result.nodeId.padEnd(9)} fn=${(result.fn ?? "-").padEnd(15)} ` +
98
+ `attempt=${result.attempt} ${result.status}` +
99
+ (result.output === undefined ? "" : ` output=${JSON.stringify(result.output)}`)
100
+ );
101
+ }
102
+
103
+ console.log(
104
+ ` report.registry: ${report.registry?.fingerprint} ` +
105
+ `(${report.registry?.fingerprint === loaded.registry.fingerprint ? "matches the loaded table" : "MISMATCH"})`
106
+ );
107
+ console.log(` lint issues: ${report.lintIssues.length}`);
108
+ if (report.errors.length > 0) {
109
+ for (const error of report.errors) {
110
+ console.log(` error ${error.code} @${error.nodeId} handled=${error.handled}`);
111
+ }
112
+ }
113
+
114
+ /* ---------- 3. the freeze ---------- */
115
+
116
+ console.log("\n" + "-".repeat(96));
117
+ console.log("the namespace freeze: one claim per process\n");
118
+
119
+ console.log(
120
+ ` claims now: ${registeredNamespaces()
121
+ .map(claim => `${claim.namespace} (${claim.names}, from ${claim.source})`)
122
+ .join("; ")}`
123
+ );
124
+
125
+ try {
126
+ await loadGraphScripts(document, { baseDir: EXAMPLES });
127
+ console.log(" ✗ a second load succeeded — the freeze is broken");
128
+ } catch (error) {
129
+ const scripted = error as GraphScriptError;
130
+ console.log(` second load → ${scripted.code}`);
131
+ console.log(` ${scripted.message.split(". ")[0]}.`);
132
+ }
133
+
134
+ console.log(
135
+ "\n (resetGraphScriptNamespaces() clears the claims — test/CI hygiene;\n" +
136
+ " production code should not need it: a document is loaded once.)"
137
+ );
138
+
139
+ resetGraphScriptNamespaces();
140
+
141
+ const reloaded = await loadGraphScripts(document, { baseDir: EXAMPLES });
142
+ console.log(
143
+ ` after reset → loaded again: ${reloaded.registry.names.length} functions, ` +
144
+ `fingerprint ${reloaded.registry.fingerprint}`
145
+ );
146
+
147
+ console.log("\n" + "=".repeat(96));
148
+ }
149
+
150
+ if (require.main === module) {
151
+ void main();
152
+ }
@@ -0,0 +1,32 @@
1
+ /* ============================================================
2
+ * The functions referenced by examples/order-flow.scripted.graph.json
3
+ * ------------------------------------------------------------
4
+ * A plain CommonJS module: the export table IS the contribution. Names
5
+ * keep their exported spelling — the declared namespace ("order") is a
6
+ * claim slot, not a prefix.
7
+ *
8
+ * `ENGINE_VERSION` is deliberately not a function: the loader skips it
9
+ * and reports it under `ignoredExports`.
10
+ * ============================================================ */
11
+
12
+ const ENGINE_VERSION = "1.5.0";
13
+
14
+ module.exports = {
15
+ classifyOrder: () => "priority",
16
+
17
+ isPriority: value => value === "priority",
18
+
19
+ chargeCard: context => {
20
+ if (context.attempt === 1) {
21
+ throw new Error("chargeCard: gateway timeout (transient)");
22
+ }
23
+
24
+ return "charged";
25
+ },
26
+
27
+ shipOrder: () => "shipped",
28
+ manualReview: () => "queued for a human",
29
+ rejectOrder: () => "rejected",
30
+
31
+ ENGINE_VERSION
32
+ };
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "order-flow",
3
+ "version": "1.0.0",
4
+ "entryNodes": ["classify"],
5
+ "terminalNodes": ["ship", "manual", "reject"],
6
+ "completionPolicy": "FIRST_TERMINAL",
7
+ "nodes": {
8
+ "classify": {
9
+ "run": "classifyOrder",
10
+ "next": [
11
+ { "test": "isPriority", "nextNode": "charge" },
12
+ { "result": "manual", "nextNode": "manual" },
13
+ { "nextNode": "reject" }
14
+ ]
15
+ },
16
+ "charge": {
17
+ "run": "chargeCard",
18
+ "next": ["ship"],
19
+ "onThrow": "manual",
20
+ "errorPolicy": "RETRY",
21
+ "retry": { "maxAttempts": 2, "delayMs": 5 }
22
+ },
23
+ "ship": { "run": "shipOrder" },
24
+ "manual": { "run": "manualReview" },
25
+ "reject": { "run": "rejectOrder" }
26
+ }
27
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "id": "order-flow-scripted",
3
+ "version": "1.5.0",
4
+ "entryNodes": [
5
+ "classify"
6
+ ],
7
+ "terminalNodes": [
8
+ "ship",
9
+ "manual",
10
+ "reject"
11
+ ],
12
+ "completionPolicy": "FIRST_TERMINAL",
13
+ "scripts": [
14
+ {
15
+ "namespace": "order",
16
+ "path": "./order-flow.functions.cjs"
17
+ },
18
+ {
19
+ "namespace": "audit",
20
+ "source": "globalThis.GraphEngine.registerEngineNamespace('audit', { auditDecision: context => `audited ${JSON.stringify(context.outputs.classify)} on attempt ${context.attempt}` });"
21
+ }
22
+ ],
23
+ "nodes": {
24
+ "classify": {
25
+ "run": "classifyOrder",
26
+ "next": [
27
+ {
28
+ "test": "isPriority",
29
+ "nextNode": "audit"
30
+ },
31
+ {
32
+ "result": "manual",
33
+ "nextNode": "manual"
34
+ },
35
+ {
36
+ "nextNode": "reject"
37
+ }
38
+ ]
39
+ },
40
+ "audit": {
41
+ "run": "auditDecision",
42
+ "next": [
43
+ "charge"
44
+ ]
45
+ },
46
+ "charge": {
47
+ "run": "chargeCard",
48
+ "next": [
49
+ "ship"
50
+ ],
51
+ "onThrow": "manual",
52
+ "errorPolicy": "RETRY",
53
+ "retry": {
54
+ "maxAttempts": 2,
55
+ "delayMs": 5
56
+ }
57
+ },
58
+ "ship": {
59
+ "run": "shipOrder"
60
+ },
61
+ "manual": {
62
+ "run": "manualReview"
63
+ },
64
+ "reject": {
65
+ "run": "rejectOrder"
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /* ============================================================
3
+ * Branching graph with TYPED EDGES (v1.3) — one execution
4
+ * ------------------------------------------------------------
5
+ * The same picture as examples/branching-graph.ts, but the arrows
6
+ * are now declared where they belong — on the node — and the whole
7
+ * thing runs as ONE GraphExecutionEngine execution:
8
+ *
9
+ * a ── result === 1 ────────────▶ b ──(forces)──▶ f
10
+ * a ── test(result) === true ───▶ c
11
+ * a ── else ────────────────────▶ d
12
+ * a ── on throw ────────────────▶ e
13
+ * a ── on retry ────────────────▶ f
14
+ *
15
+ * What replaced what (see CHANGELOG 1.3.0):
16
+ * guards + skip markers → { result } / { test } / { nextNode } edges
17
+ * the b → f gate → context.goto("f") inside b
18
+ * "on retry → f" route → return new Next("f", result) when attempt > 1
19
+ * completionPolicy DRAIN → not needed: exclusive edges schedule one branch
20
+ *
21
+ * Run: node js/examples/branching-edges.js (after npm run build)
22
+ * ============================================================ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.test = void 0;
25
+ exports.buildGraph = buildGraph;
26
+ const automategraph_1 = require("../src/automategraph");
27
+ /** `test(result)` from the picture. */
28
+ const test = (result) => result === 2;
29
+ exports.test = test;
30
+ const node = (id, run, extra = {}) => ({ id, run, ...extra });
31
+ function buildGraph(seed) {
32
+ return {
33
+ id: "branching-edges",
34
+ version: "1.3.0",
35
+ nodes: {
36
+ /* ---------------------------- a ---------------------------- */
37
+ a: node("a", (context) => {
38
+ // on throw → e (a real engine transition, declared below)
39
+ if (seed === "throw")
40
+ throw new Error("a: upstream exploded");
41
+ // retry: attempt 1 fails, attempt 2 succeeds
42
+ if (seed === "retry" && context.attempt === 1) {
43
+ throw new Error(`a: transient failure on attempt ${context.attempt}`);
44
+ }
45
+ const result = seed === "retry" ? 2 : seed;
46
+ // on retry → f (only the node knows the attempt number)
47
+ if (context.attempt > 1)
48
+ return new automategraph_1.Next("f", result);
49
+ return result;
50
+ }, {
51
+ next: [
52
+ { result: 1, nextNode: "b" },
53
+ { test: value => typeof value === "number" && (0, exports.test)(value), nextNode: "c" },
54
+ { nextNode: "d" } // else
55
+ ],
56
+ onThrow: "e",
57
+ retry: { maxAttempts: 2, delayMs: 5 },
58
+ errorPolicy: "RETRY"
59
+ }),
60
+ /* ---------------------------- b ---------------------------- */
61
+ // returns void, then forces f from inside the execution
62
+ b: node("b", (context) => {
63
+ context.goto("f");
64
+ return undefined; // void
65
+ }, {
66
+ // declared so static reachability sees f; at runtime the explicit
67
+ // route wins and this edge stays a fallback if b ever returns a value
68
+ next: ["f"]
69
+ }),
70
+ /* ---------------------------- c / d / e / f ---------------------------- */
71
+ c: node("c", () => "c: handled result 2"),
72
+ d: node("d", () => "d: handled anything else"),
73
+ e: node("e", () => "e: failure route"),
74
+ f: node("f", () => "f: reached by retry or by b's forced jump")
75
+ },
76
+ entryNodes: ["a"],
77
+ terminalNodes: ["c", "d", "e", "f"],
78
+ completionPolicy: "FIRST_TERMINAL" // exclusive edges ⇒ one branch only
79
+ };
80
+ }
81
+ /* ============================= demo ============================= */
82
+ const SCENARIOS = [
83
+ { label: "result === 1 → b, and b forces f", seed: 1, path: ["a", "b", "f"] },
84
+ { label: "test(result) → c", seed: 2, path: ["a", "c"] },
85
+ { label: "else → d", seed: 3, path: ["a", "d"] },
86
+ { label: "on throw → e", seed: "throw", path: ["a", "e"] },
87
+ { label: "on retry → f", seed: "retry", path: ["a", "f"] }
88
+ ];
89
+ function show(value) {
90
+ return value === undefined ? "undefined (void)" : JSON.stringify(value);
91
+ }
92
+ /** Collects what each node produced / routed to, in execution order. */
93
+ function instrument(engine) {
94
+ const visited = [];
95
+ engine.events.on("node.succeeded", payload => {
96
+ const info = payload;
97
+ visited.push({ nodeId: info.nodeId, attempt: 1, output: info.output, route: info.route });
98
+ });
99
+ return visited;
100
+ }
101
+ async function main() {
102
+ console.log("=".repeat(100));
103
+ console.log("typed edges (v1.3) — the whole picture as ONE execution, lint: \"error\"");
104
+ console.log("=".repeat(100));
105
+ let failures = 0;
106
+ for (const scenario of SCENARIOS) {
107
+ const engine = new automategraph_1.GraphExecutionEngine(buildGraph(scenario.seed), { lint: "error" });
108
+ const visited = instrument(engine);
109
+ const report = await engine.run();
110
+ const path = report.results.map(result => result.nodeId);
111
+ const ok = path.join(",") === scenario.path.join(",") && report.status === "COMPLETED";
112
+ if (!ok)
113
+ failures += 1;
114
+ console.log(`\n▶ seed = ${JSON.stringify(scenario.seed)} ${scenario.label}`);
115
+ console.log(` ${report.status} path: ${path.join(" → ")} ` +
116
+ `${ok ? "✓" : `✗ expected ${scenario.path.join(" → ")}`}`);
117
+ for (const step of visited) {
118
+ const route = step.route ? ` ⤳ routed to ${step.route.nextNode} (via ${step.route.via})` : "";
119
+ console.log(` ${step.nodeId.padEnd(2)} output=${show(step.output).padEnd(40)}${route}`);
120
+ }
121
+ for (const error of report.errors) {
122
+ console.log(` error: ${error.code}@${error.nodeId} handled=${error.handled}`);
123
+ }
124
+ }
125
+ console.log(`\n${"-".repeat(100)}`);
126
+ console.log(`scenarios: ${SCENARIOS.length - failures}/${SCENARIOS.length} matched`);
127
+ if (failures > 0)
128
+ process.exitCode = 1;
129
+ }
130
+ if (require.main === module) {
131
+ void main();
132
+ }
133
+ //# sourceMappingURL=branching-edges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branching-edges.js","sourceRoot":"","sources":["../../examples/branching-edges.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ /* ============================================================
3
+ * Branching graph — a → b / c / d / e / f
4
+ * ------------------------------------------------------------
5
+ * ONE GraphDefinition. No router, no walk loop, no engine change.
6
+ * Built with the same tiny builders the fixtures use:
7
+ *
8
+ * node(id, run, extra) → { id, run, ...extra }
9
+ * constNode(id, output, x) → node(id, () => output, x)
10
+ * g(spec) → GraphDefinition
11
+ *
12
+ * The picture:
13
+ *
14
+ * a ── result === 1 ────────────▶ b ──(forces)──▶ f
15
+ * a ── test(result) === true ───▶ c
16
+ * a ── else ────────────────────▶ d
17
+ * a ── on throw ────────────────▶ e
18
+ * a ── on retry ────────────────▶ f
19
+ *
20
+ * HOW EACH ARROW IS EXPRESSED (engine-native only)
21
+ * ---------------------------------------------------------------
22
+ * fan-out + guards `next` is an UNCONDITIONAL fan-out, so a lists
23
+ * every candidate and each one starts with a guard
24
+ * that reads a's output: the acting branch does its
25
+ * work, the others return a SKIP marker.
26
+ * result === 1 → b guard: a.route === "b"
27
+ * test(result) → c guard: a.route === "c" (test(r) = r === 2)
28
+ * else → d guard: a.route === "d" (a computed "else")
29
+ * on throw → e real transition: a.onThrow = "e"
30
+ * on retry → f a returns route "f" when context.attempt > 1
31
+ * (the retry loop is internal, so the attempt
32
+ * number is the only per-attempt signal a node has)
33
+ * b forces f b returns void, then releases a gate that f is
34
+ * waiting on — f cannot act before b finished.
35
+ *
36
+ * WHY THE GATE FOR "b FORCES f"
37
+ * ---------------------------------------------------------------
38
+ * A node cannot schedule work. The only in-node state transition the
39
+ * engine offers is `throw` + `onThrow`, and here it cannot be used:
40
+ * • f must stay in a's fan-out, otherwise the retry arrow has no way
41
+ * to reach it (a recovered attempt succeeds, it cannot also throw);
42
+ * • if b threw to f, f would be activated a SECOND time, and f
43
+ * cannot tell that arrival apart from the fan-out one — both see
44
+ * the same outputs (b threw, so it has no output to check);
45
+ * • and b would be recorded FAILED, which is wrong for a jump.
46
+ * So b forces f by ORDER, not by scheduling: f is already activated by
47
+ * the fan-out, but it blocks on a gate that only b releases. The
48
+ * observable result is exactly "b runs, then f runs — because of b".
49
+ *
50
+ * COMPLETION
51
+ * ---------------------------------------------------------------
52
+ * b, c, d, e and f are declared terminal and the graph uses
53
+ * completionPolicy: "DRAIN", because skipping nodes also succeed and
54
+ * FIRST_TERMINAL could otherwise latch on a SKIP and drop real work.
55
+ *
56
+ * Run: node js/examples/branching-graph.js (after npm run build)
57
+ * ============================================================ */
58
+ Object.defineProperty(exports, "__esModule", { value: true });
59
+ exports.FORCE_EVENT = exports.test = void 0;
60
+ exports.buildGraph = buildGraph;
61
+ exports.summarise = summarise;
62
+ const automategraph_1 = require("../src/automategraph");
63
+ const node = (id, run, extra = {}) => ({ id, run, ...extra });
64
+ const constNode = (id, output, extra = {}) => node(id, () => output, extra);
65
+ const g = (spec) => ({
66
+ id: spec.id ?? "branching-graph",
67
+ version: spec.version ?? "1.0.0",
68
+ entryNodes: spec.entryNodes,
69
+ terminalNodes: spec.terminalNodes,
70
+ errorPolicy: spec.errorPolicy,
71
+ completionPolicy: spec.completionPolicy,
72
+ nodes: Object.fromEntries(spec.nodes.map(definition => [definition.id, definition]))
73
+ });
74
+ /** `test(result)` from the picture. */
75
+ const test = (result) => result === 2;
76
+ exports.test = test;
77
+ const SKIPPED = (nodeId) => ({ skipped: true, nodeId });
78
+ const routeOf = (context) => context.outputs.a;
79
+ /** Observation only — nobody acts on this event; it just records the intent. */
80
+ exports.FORCE_EVENT = "graph.force";
81
+ /* =========================
82
+ * 3. The graph
83
+ * ========================= */
84
+ /**
85
+ * Builds the graph for one run. A fresh gate per run is what makes
86
+ * "b forces f" deterministic without a router.
87
+ */
88
+ function buildGraph(seed) {
89
+ /* --- the b → f gate (closure state, not engine state) --- */
90
+ let released = false;
91
+ let release = () => { };
92
+ const gate = new Promise(resolve => { release = () => { released = true; resolve(); }; });
93
+ return g({
94
+ id: "branching-graph",
95
+ version: "1.0.0",
96
+ nodes: [
97
+ /* ---------------- a: produces the value, owns every arrow ---------------- */
98
+ node("a", (context) => {
99
+ // on throw → e (engine transition, declared as a.onThrow below)
100
+ if (seed === "throw") {
101
+ throw new Error("a: upstream exploded");
102
+ }
103
+ // on retry → f: the retry loop is internal, so a sees its own attempt
104
+ if (seed === "retry" && context.attempt === 1) {
105
+ throw new Error(`a: transient failure on attempt ${context.attempt}`);
106
+ }
107
+ const result = seed === "retry" ? 2 : seed;
108
+ if (context.attempt > 1) {
109
+ // recovered on a later attempt → the "on retry" arrow points at f
110
+ return { route: "f", result, attempts: context.attempt };
111
+ }
112
+ if (result === 1)
113
+ return { route: "b", result, attempts: context.attempt };
114
+ if ((0, exports.test)(result))
115
+ return { route: "c", result, attempts: context.attempt };
116
+ return { route: "d", result, attempts: context.attempt }; // else
117
+ }, {
118
+ // unconditional fan-out: every candidate is activated, guards pick one
119
+ next: ["b", "c", "d", "f"],
120
+ onThrow: "e",
121
+ retry: { maxAttempts: 2, delayMs: 5 },
122
+ errorPolicy: "RETRY"
123
+ }),
124
+ /* ---------------- b: returns void, then forces f ---------------- */
125
+ node("b", async (context) => {
126
+ if (routeOf(context)?.route !== "b")
127
+ return SKIPPED("b");
128
+ // b's work ... whatever it needs to do, it returns nothing
129
+ await context.emit(exports.FORCE_EVENT, { from: "b", to: "f", reason: "b short-circuits to f" });
130
+ release(); // ← this is the force: f is unblocked by b, after b
131
+ return undefined; // void
132
+ }),
133
+ /* ---------------- c: test(result) === true ---------------- */
134
+ node("c", (context) => routeOf(context)?.route === "c"
135
+ ? `c: handled result ${routeOf(context).result}`
136
+ : SKIPPED("c")),
137
+ /* ---------------- d: else ---------------- */
138
+ node("d", (context) => routeOf(context)?.route === "d"
139
+ ? `d: handled result ${routeOf(context).result}`
140
+ : SKIPPED("d")),
141
+ /* ---------------- e: on throw ---------------- */
142
+ constNode("e", "e: failure route"),
143
+ /* ---------------- f: on retry, or forced by b ---------------- */
144
+ node("f", async (context) => {
145
+ const route = routeOf(context)?.route;
146
+ if (route === "f")
147
+ return "f: reached after a retry (on retry arrow)";
148
+ if (route !== "b")
149
+ return SKIPPED("f");
150
+ // route === "b": f only acts once b has forced it
151
+ if (!released)
152
+ await gate;
153
+ return "f: forced by b (ran after b completed)";
154
+ })
155
+ ],
156
+ entryNodes: ["a"],
157
+ terminalNodes: ["b", "c", "d", "e", "f"],
158
+ // skipping nodes succeed too, so let the graph drain instead of
159
+ // latching on the first success (FIRST_TERMINAL would drop work)
160
+ completionPolicy: "DRAIN"
161
+ });
162
+ }
163
+ /* =========================
164
+ * 4. Demo — one engine run per seed, no router
165
+ * ========================= */
166
+ const SCENARIOS = [
167
+ { label: "result === 1 → b, then b forces f", seed: 1, expectedActed: ["b", "f"] },
168
+ { label: "test(result) → c", seed: 2, expectedActed: ["c"] },
169
+ { label: "else → d", seed: 3, expectedActed: ["d"] },
170
+ { label: "on throw → e", seed: "throw", expectedActed: ["e"] },
171
+ { label: "on retry → f", seed: "retry", expectedActed: ["f"] }
172
+ ];
173
+ const isSkipped = (output) => typeof output === "object" && output !== null && output.skipped === true;
174
+ const show = (output) => output === undefined ? "undefined (void)" : JSON.stringify(output);
175
+ const BRANCHES = ["b", "c", "d", "e", "f"];
176
+ /** `acted` = the branch nodes that did real work (a is the dispatcher, not a branch). */
177
+ function summarise(report) {
178
+ const dispatcher = report.results.find(result => result.nodeId === "a");
179
+ return {
180
+ acted: report.results
181
+ .filter(result => BRANCHES.includes(result.nodeId) &&
182
+ result.status === "SUCCEEDED" &&
183
+ !isSkipped(result.output))
184
+ .map(result => result.nodeId),
185
+ status: report.status,
186
+ attempts: dispatcher?.attempt ?? 0
187
+ };
188
+ }
189
+ async function main() {
190
+ console.log("=".repeat(104));
191
+ console.log("branching graph — one GraphDefinition, no router, engine untouched");
192
+ console.log("=".repeat(104));
193
+ let failures = 0;
194
+ for (const scenario of SCENARIOS) {
195
+ const engine = new automategraph_1.GraphExecutionEngine(buildGraph(scenario.seed), { lint: "error" });
196
+ const forces = [];
197
+ engine.events.on(exports.FORCE_EVENT, payload => { forces.push(payload); });
198
+ const report = await engine.run();
199
+ const summary = summarise(report);
200
+ const actedOk = summary.acted.join(",") === scenario.expectedActed.join(",");
201
+ const statusOk = summary.status === "COMPLETED";
202
+ if (!actedOk || !statusOk)
203
+ failures += 1;
204
+ console.log(`\n▶ seed = ${JSON.stringify(scenario.seed)} ${scenario.label}`);
205
+ console.log(` status=${summary.status} a.attempt=${summary.attempts} ` +
206
+ `acted: ${summary.acted.join(" → ")} ${actedOk && statusOk ? "✓" : "✗"}`);
207
+ for (const result of report.results) {
208
+ const tag = result.status === "SUCCEEDED" && !isSkipped(result.output)
209
+ ? "ACTS "
210
+ : result.status === "SUCCEEDED"
211
+ ? "skip "
212
+ : result.status.padEnd(5);
213
+ console.log(` ${tag} ${result.nodeId} attempt=${result.attempt} output=${show(result.output)}`);
214
+ }
215
+ if (report.errors.length > 0) {
216
+ console.log(` errors: ${report.errors
217
+ .map(error => `${error.code}@${error.nodeId ?? "-"}(handled=${error.handled})`)
218
+ .join(", ")}`);
219
+ }
220
+ if (forces.length > 0) {
221
+ console.log(` ${exports.FORCE_EVENT}: ${JSON.stringify(forces[0])}`);
222
+ }
223
+ }
224
+ console.log(`\n${"-".repeat(104)}`);
225
+ console.log(`scenarios: ${SCENARIOS.length - failures}/${SCENARIOS.length} matched`);
226
+ if (failures > 0)
227
+ process.exitCode = 1;
228
+ }
229
+ if (require.main === module) {
230
+ void main();
231
+ }
232
+ //# sourceMappingURL=branching-graph.js.map