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.
- package/CHANGELOG.md +18 -5
- package/UPGRADE-PROPOSAL.md +141 -0
- package/docs/diagrams/c19-resilience-scenario.md +61 -0
- package/docs/diagrams/c19-resilience-scenario.mmd +28 -0
- package/docs/diagrams/c19-resilience-scenario.svg +181 -0
- package/examples/README.md +206 -0
- package/examples/branching-edges.ts +192 -0
- package/examples/branching-graph.ts +333 -0
- package/examples/branching-router.ts +475 -0
- package/examples/json-graph.ts +231 -0
- package/examples/json-scripts.ts +152 -0
- package/examples/order-flow.functions.cjs +32 -0
- package/examples/order-flow.graph.json +27 -0
- package/examples/order-flow.scripted.graph.json +68 -0
- package/js/examples/branching-edges.js +133 -0
- package/js/examples/branching-edges.js.map +1 -0
- package/js/examples/branching-graph.js +232 -0
- package/js/examples/branching-graph.js.map +1 -0
- package/js/examples/branching-router.js +305 -0
- package/js/examples/branching-router.js.map +1 -0
- package/js/examples/json-graph.js +197 -0
- package/js/examples/json-graph.js.map +1 -0
- package/js/examples/json-scripts.js +131 -0
- package/js/examples/json-scripts.js.map +1 -0
- package/js/tests/async.test.js +592 -0
- package/js/tests/async.test.js.map +1 -0
- package/js/tests/complex.test.js +972 -0
- package/js/tests/complex.test.js.map +1 -0
- package/js/tests/conformance.test.js +365 -0
- package/js/tests/conformance.test.js.map +1 -0
- package/js/tests/edges.test.js +702 -0
- package/js/tests/edges.test.js.map +1 -0
- package/js/tests/graphjson.test.js +504 -0
- package/js/tests/graphjson.test.js.map +1 -0
- package/js/tests/harness.js +192 -0
- package/js/tests/harness.js.map +1 -0
- package/js/tests/helpers.js +136 -0
- package/js/tests/helpers.js.map +1 -0
- package/js/tests/lint.test.js +317 -0
- package/js/tests/lint.test.js.map +1 -0
- package/js/tests/registry.test.js +705 -0
- package/js/tests/registry.test.js.map +1 -0
- package/js/tests/report.js +422 -0
- package/js/tests/report.js.map +1 -0
- package/js/tests/runner.js +67 -0
- package/js/tests/runner.js.map +1 -0
- package/js/tests/scripts.test.js +552 -0
- package/js/tests/scripts.test.js.map +1 -0
- package/js/tests/unit.test.js +1239 -0
- package/js/tests/unit.test.js.map +1 -0
- package/js/tests/upgrade.test.js +618 -0
- package/js/tests/upgrade.test.js.map +1 -0
- package/package.json +2 -9
- package/report/index.html +3727 -0
- package/report/report.json +3053 -0
- package/tests/async.test.ts +668 -0
- package/tests/complex.test.ts +1123 -0
- package/tests/conformance.test.ts +442 -0
- package/tests/edges.test.ts +848 -0
- package/tests/fixtures/scripts/bad-registration.cjs +4 -0
- package/tests/fixtures/scripts/broken.cjs +2 -0
- package/tests/fixtures/scripts/cjs-default.js +4 -0
- package/tests/fixtures/scripts/collide-a.cjs +4 -0
- package/tests/fixtures/scripts/collide-b.cjs +4 -0
- package/tests/fixtures/scripts/empty.cjs +2 -0
- package/tests/fixtures/scripts/esm-functions.mjs +10 -0
- package/tests/fixtures/scripts/esm-syntax.js +3 -0
- package/tests/fixtures/scripts/global-channel.cjs +6 -0
- package/tests/fixtures/scripts/global-channel.mjs +7 -0
- package/tests/fixtures/scripts/mismatch.cjs +4 -0
- package/tests/fixtures/scripts/needs-sibling.cjs +7 -0
- package/tests/fixtures/scripts/order.cjs +14 -0
- package/tests/fixtures/scripts/sibling-helper.cjs +3 -0
- package/tests/fixtures/scripts/throwing-global.mjs +6 -0
- package/tests/graphjson.test.ts +737 -0
- package/tests/harness.ts +291 -0
- package/tests/helpers.ts +216 -0
- package/tests/lint.test.ts +381 -0
- package/tests/registry.test.ts +1134 -0
- package/tests/report.ts +452 -0
- package/tests/runner.ts +58 -0
- package/tests/scripts.test.ts +970 -0
- package/tests/unit.test.ts +1192 -0
- package/tests/upgrade.test.ts +725 -0
- package/tools/server.mjs +152 -0
- package/tsconfig.browser.json +19 -0
- package/tsconfig.json +18 -0
- 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
|