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,452 @@
1
+ /* ============================================================
2
+ * Static web report generator — writes:
3
+ * report/index.html (self-contained dashboard, no network deps)
4
+ * report/report.json (machine-readable full results)
5
+ *
6
+ * v1.1: fully kind-driven. Adding a new test pack (see TEST_KINDS
7
+ * in harness.ts) requires no changes here.
8
+ * ============================================================ */
9
+
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+
13
+ import { KindTotals, RunSummary, TestKind, TestResultRecord } from "./harness";
14
+
15
+ const KIND_META: Record<TestKind, { badge: string; blurb: string; color: string }> = {
16
+ unit: {
17
+ badge: "UNIT",
18
+ blurb: "constructor validation, error model, event bus, storage, freezing, lifecycle, scheduler, retry, cancellation",
19
+ color: "#7c9cff"
20
+ },
21
+ integration: {
22
+ badge: "COMPLEX",
23
+ blurb: "end-to-end pipelines, failure semantics, concurrency & scale, listeners, plugin lifecycle, cancellation",
24
+ color: "#c084fc"
25
+ },
26
+ async: {
27
+ badge: "ASYNC",
28
+ blurb: "async node callbacks across every path: success, retry, ONTHROW, timeout, abort, fan-out, checkpoints",
29
+ color: "#4fd1c5"
30
+ },
31
+ upgrade: {
32
+ badge: "UPGRADE",
33
+ blurb: "Wave 1 (v1.1) guarantees: drain deadline, completion policies, sticky FAIL_FAST, FALLBACK routing, budgets, linting, isomorphic build",
34
+ color: "#f0b429"
35
+ },
36
+ conformance: {
37
+ badge: "CONFORMANCE",
38
+ blurb: "the storage adapter contract is enforced by a checker with teeth, plus the snapshot wire format",
39
+ color: "#31c48d"
40
+ },
41
+ lint: {
42
+ badge: "LINT",
43
+ blurb: "the SUCCESS_DEAD_END rule: non-terminal nodes whose success path has nowhere to go",
44
+ color: "#fb923c"
45
+ },
46
+ edges: {
47
+ badge: "EDGES",
48
+ blurb: "typed edges: deep-equality/t predicate routing, else edges, parallel edges and explicit Next/goto routing",
49
+ color: "#38bdf8"
50
+ },
51
+ registry: {
52
+ badge: "REGISTRY",
53
+ blurb: "the function registry: run/test by name, lazy resolution, UNKNOWN_FUNCTION, fingerprints",
54
+ color: "#a78bfa"
55
+ },
56
+ graphjson: {
57
+ badge: "JSON",
58
+ blurb: "graph data files: parsing a document into a definition, total validation, engine from JSON",
59
+ color: "#34d399"
60
+ },
61
+ scripts: {
62
+ badge: "SCRIPTS",
63
+ blurb: "script loading: exported and self-registered functions, format detection, process-global namespace freeze",
64
+ color: "#f472b6"
65
+ }
66
+ };
67
+
68
+ const UNIT_CATEGORIES: Array<[string, string]> = [
69
+ ["Graph validation", "Constructor-time rejection of malformed graph definitions"],
70
+ ["Error classes & utilities", "Error hierarchy, codes, id/time generation, throw coercion"],
71
+ ["EventBus", "Required vs non-required listeners, ordering, isolation, teardown"],
72
+ ["InMemoryStorage", "Atomic snapshot semantics and copy isolation"],
73
+ ["Graph freezing", "Immutability of definitions after construction"],
74
+ ["Engine lifecycle", "State machine, single-run guard, plugin init/shutdown"],
75
+ ["Execution & scheduler", "Scheduling, fan-out, contexts, events, checkpoints, settlement"],
76
+ ["Retry", "RETRY policy looping, delays, attempt accounting"],
77
+ ["Cancellation", "Cooperative abort, cancellation events, terminal no-ops"]
78
+ ];
79
+
80
+ const escapeHtml = (value: string): string =>
81
+ value
82
+ .replace(/&/g, "&amp;")
83
+ .replace(/</g, "&lt;")
84
+ .replace(/>/g, "&gt;")
85
+ .replace(/"/g, "&quot;")
86
+ .replace(/'/g, "&#39;");
87
+
88
+ export function findingsOf(record: TestResultRecord): string[] {
89
+ const derived = record.logs
90
+ .filter(line => line.startsWith("finding:"))
91
+ .map(line => line.replace(/^finding:\s*/, ""));
92
+ if (record.finding) derived.unshift(record.finding);
93
+ return [...new Set(derived)];
94
+ }
95
+
96
+ function renderTestRow(record: TestResultRecord): string {
97
+ const findings = findingsOf(record);
98
+ const isFinding = findings.length > 0;
99
+ const classes = [
100
+ "row",
101
+ record.status === "passed" ? "ok" : "bad",
102
+ record.kind
103
+ ].join(" ");
104
+
105
+ const details: string[] = [];
106
+
107
+ if (isFinding) {
108
+ details.push(
109
+ `<div class="detail finding"><span class="tag">findings</span><ul>` +
110
+ findings.map(line => `<li>${escapeHtml(line)}</li>`).join("") +
111
+ "</ul></div>"
112
+ );
113
+ }
114
+
115
+ if (record.logs.length > 0) {
116
+ details.push(
117
+ `<div class="detail"><span class="tag">observations</span><ul>` +
118
+ record.logs.map(line => `<li>${escapeHtml(line)}</li>`).join("") +
119
+ "</ul></div>"
120
+ );
121
+ }
122
+
123
+ if (record.error) {
124
+ details.push(
125
+ `<div class="detail error"><span class="tag">error</span>` +
126
+ `<div class="errline">${escapeHtml(record.error.name)}: ${escapeHtml(record.error.message)}</div>` +
127
+ (record.error.stack
128
+ ? `<pre>${escapeHtml(record.error.stack.split("\n").slice(0, 8).join("\n"))}</pre>`
129
+ : "") +
130
+ "</div>"
131
+ );
132
+ }
133
+
134
+ return `
135
+ <details class="${classes}" data-kind="${record.kind}" data-status="${record.status}" data-finding="${isFinding}" data-search="${escapeHtml(
136
+ (record.id + " " + record.category + " " + record.name + " " + findings.join(" ")).toLowerCase()
137
+ )}">
138
+ <summary>
139
+ <span class="pill ${record.status}">${record.status === "passed" ? "PASS" : "FAIL"}</span>
140
+ <span class="id">${escapeHtml(record.id)}</span>
141
+ <span class="cat">${escapeHtml(record.category)}</span>
142
+ <span class="name">${escapeHtml(record.name)}</span>
143
+ ${isFinding ? '<span class="star" title="documents an engine semantic">finding</span>' : ""}
144
+ <span class="ms">${record.durationMs} ms</span>
145
+ </summary>
146
+ ${details.join("") || '<div class="detail muted">No additional observations.</div>'}
147
+ </details>`;
148
+ }
149
+
150
+ function bar(label: string, kind: TestKind, totals: KindTotals, durationMs: number): string {
151
+ const pct = totals.total === 0 ? 0 : Math.round((totals.passed / totals.total) * 100);
152
+ return `
153
+ <div class="barrow">
154
+ <div class="barlabel"><span class="badge ${kind}">${KIND_META[kind]?.badge ?? kind.toUpperCase()}</span> ${escapeHtml(label)}</div>
155
+ <div class="track"><div class="fill ${totals.failed > 0 ? "warn" : ""}" style="width:${pct}%">${pct}%</div></div>
156
+ <div class="barmeta">${totals.passed}/${totals.total} · ${durationMs} ms</div>
157
+ </div>`;
158
+ }
159
+
160
+ export function writeReport(summary: RunSummary, workspaceRoot: string): { htmlPath: string; jsonPath: string } {
161
+ const reportDir = path.join(workspaceRoot, "report");
162
+ fs.mkdirSync(reportDir, { recursive: true });
163
+
164
+ const jsonPath = path.join(reportDir, "report.json");
165
+ fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), "utf8");
166
+
167
+ const { totals } = summary;
168
+ const kinds = Object.keys(KIND_META) as TestKind[];
169
+ const findings = summary.tests.flatMap(test =>
170
+ findingsOf(test).map(text => ({ test, text }))
171
+ );
172
+ const failedTests = summary.tests.filter(test => test.status === "failed");
173
+
174
+ const testsOfKind = (kind: TestKind): TestResultRecord[] =>
175
+ summary.tests.filter(test => test.kind === kind);
176
+
177
+ const categoryDurations = new Map(
178
+ summary.byCategory.map(entry => [`${entry.kind}::${entry.category}`, entry.durationMs])
179
+ );
180
+
181
+ const kindSummary = kinds
182
+ .map(kind => `${totals.byKind[kind].total} ${KIND_META[kind].badge.toLowerCase()}`)
183
+ .join(" + ");
184
+
185
+ const processHygiene =
186
+ summary.unhandledRejections.length === 0
187
+ ? `<span class="pill passed">clean</span> no unhandled rejections observed during the run`
188
+ : `<span class="pill failed">noisy</span> ${summary.unhandledRejections.length} unhandled rejection(s) observed`;
189
+
190
+ const inventoryTable = (kind: TestKind): string => {
191
+ const tests = testsOfKind(kind);
192
+ if (tests.length === 0) return "";
193
+ return `
194
+ <h3>${tests.length} ${KIND_META[kind].badge} cases — ${escapeHtml(KIND_META[kind].blurb)}</h3>
195
+ <table>
196
+ <tr><th>#</th><th>Case</th><th>Area</th><th>Time</th></tr>
197
+ ${tests
198
+ .map(
199
+ test =>
200
+ `<tr><td>${escapeHtml(test.id)}</td><td>${escapeHtml(test.name)}</td>` +
201
+ `<td class="muted">${escapeHtml(test.category)}</td><td>${test.durationMs} ms</td></tr>`
202
+ )
203
+ .join("")}
204
+ </table>`;
205
+ };
206
+
207
+ const html = `<!doctype html>
208
+ <html lang="en">
209
+ <head>
210
+ <meta charset="utf-8"/>
211
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
212
+ <title>AutomateGraph Execution Engine — Test Report (${totals.total} tests)</title>
213
+ <style>
214
+ :root{
215
+ --bg:#0b1020; --panel:#121a33; --panel2:#0f1730; --line:#24304f;
216
+ --txt:#e7ecf7; --muted:#94a3c4; --accent:#6ea8fe; --ok:#31c48d; --bad:#f4645f;
217
+ --warn:#f0b429; --unit:#7c9cff; --complex:#c084fc; --async:#4fd1c5;
218
+ --upgrade:#f0b429; --conformance:#31c48d;
219
+ }
220
+ *{box-sizing:border-box}
221
+ body{margin:0;background:radial-gradient(1200px 600px at 15% -10%,#16224a 0%,var(--bg) 55%);color:var(--txt);
222
+ font:15px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
223
+ .wrap{max-width:1180px;margin:0 auto;padding:32px 20px 80px}
224
+ h1{font-size:30px;margin:0 0 6px;letter-spacing:-.4px}
225
+ h2{font-size:19px;margin:34px 0 12px;letter-spacing:-.2px}
226
+ h3{font-size:14px;margin:20px 0 8px;color:var(--muted);text-transform:uppercase;letter-spacing:.07em}
227
+ .sub{color:var(--muted);margin:0 0 22px}
228
+ .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:22px 0 8px}
229
+ .card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
230
+ border-radius:14px;padding:14px 16px}
231
+ .card .k{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}
232
+ .card .v{font-size:25px;font-weight:700;margin-top:4px}
233
+ .card.good .v{color:var(--ok)} .card.bad .v{color:var(--bad)}
234
+ .barrow{display:grid;grid-template-columns:340px 1fr 130px;gap:12px;align-items:center;padding:6px 0;border-bottom:1px dashed #1c2540}
235
+ .barlabel{font-size:13.5px}
236
+ .track{background:#0a1024;border:1px solid var(--line);border-radius:999px;height:16px;overflow:hidden}
237
+ .fill{height:100%;background:linear-gradient(90deg,#2f7cf6,#31c48d);color:#04122b;font-size:10.5px;
238
+ font-weight:700;text-align:right;padding-right:6px;line-height:16px}
239
+ .fill.warn{background:linear-gradient(90deg,#f0b429,#f4645f)}
240
+ .barmeta{color:var(--muted);font-size:12.5px;text-align:right}
241
+ .badge{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.06em;padding:2px 7px;border-radius:6px;vertical-align:middle}
242
+ .badge.unit{background:#1b2a55;color:var(--unit)}
243
+ .badge.integration{background:#331b55;color:var(--complex)}
244
+ .badge.async{background:#123a3a;color:var(--async)}
245
+ .badge.upgrade{background:#3a2b0d;color:var(--upgrade)}
246
+ .badge.conformance{background:#0f3627;color:var(--conformance)}
247
+ .badge.lint{background:#3a2410;color:#fb923c}
248
+ .badge.edges{background:#0d2a3a;color:#38bdf8}
249
+ .badge.registry{background:#2a1f45;color:#a78bfa}
250
+ .badge.graphjson{background:#0e3229;color:#34d399}
251
+ .badge.scripts{background:#3d1f2f;color:#f472b6}
252
+ .pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px}
253
+ .pill.passed{background:#10322a;color:var(--ok);border:1px solid #1c5b46}
254
+ .pill.failed{background:#3a1620;color:var(--bad);border:1px solid #7a2b32}
255
+ .findings{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px}
256
+ .fcard{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--warn);border-radius:12px;padding:12px 14px}
257
+ .fcard .fid{color:var(--warn);font-weight:700;font-size:12px}
258
+ .fcard .ftitle{font-weight:600;margin:2px 0 6px}
259
+ .fcard .fbody{color:#c8d3ec;font-size:13.5px}
260
+ .toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:14px 0}
261
+ .btn{background:#16204a;border:1px solid var(--line);color:var(--txt);border-radius:999px;padding:6px 13px;
262
+ font-size:13px;cursor:pointer}
263
+ .btn.active{background:#233b7a;border-color:#3b57a6}
264
+ input[type=search]{background:#0d142c;border:1px solid var(--line);color:var(--txt);border-radius:999px;
265
+ padding:7px 14px;min-width:260px;font-size:13.5px}
266
+ .row{border:1px solid var(--line);border-radius:10px;margin:6px 0;background:var(--panel2)}
267
+ .row summary{display:grid;grid-template-columns:64px 62px 210px 1fr 74px 78px;gap:10px;align-items:center;
268
+ padding:9px 12px;cursor:pointer;list-style:none}
269
+ .row summary::-webkit-details-marker{display:none}
270
+ .row.ok{border-left:3px solid var(--ok)} .row.bad{border-left:3px solid var(--bad)}
271
+ .row .id{color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums}
272
+ .row .cat{color:#b9c6e6;font-size:13px}
273
+ .row .name{font-size:13.5px}
274
+ .row .ms{color:var(--muted);font-size:12px;text-align:right}
275
+ .row .star{color:var(--warn);font-size:11px;border:1px solid #5f4a12;background:#2a2208;border-radius:6px;
276
+ padding:1px 6px;justify-self:start}
277
+ .detail{padding:10px 14px 12px;border-top:1px solid #1a2340;font-size:13.5px;color:#c8d3ec}
278
+ .detail .tag{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.06em;color:#8fa6d8;
279
+ text-transform:uppercase;margin-right:8px}
280
+ .detail ul{margin:6px 0 0 18px;padding:0}
281
+ .detail.error .errline{color:#ffb3b0;font-weight:600}
282
+ pre{background:#080d1c;border:1px solid #1c2540;border-radius:8px;padding:10px;overflow:auto;font-size:12.5px;color:#b9c6e6}
283
+ .muted{color:var(--muted)}
284
+ table{width:100%;border-collapse:collapse;font-size:13.5px}
285
+ th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #1c2540;vertical-align:top}
286
+ th{color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.06em}
287
+ code{background:#0d142c;border:1px solid #1c2540;border-radius:6px;padding:1px 6px;font-size:12.5px}
288
+ .note{background:#101a38;border:1px solid var(--line);border-radius:12px;padding:12px 16px;color:#c8d3ec;font-size:13.5px}
289
+ .note.warn{border-color:#5f4a12;background:#241d09}
290
+ footer{color:var(--muted);font-size:12.5px;margin-top:28px;border-top:1px solid #1c2540;padding-top:14px}
291
+ </style>
292
+ </head>
293
+ <body>
294
+ <div class="wrap">
295
+ <h1>AutomateGraph Execution Engine v1.5 — Test Report</h1>
296
+ <p class="sub">${totals.total} tests (${kindSummary}) executed against the single-file isomorphic core.
297
+ Generated ${escapeHtml(summary.generatedAt)} · Node ${escapeHtml(summary.nodeVersion)} · wall clock ${summary.durationMs} ms.</p>
298
+
299
+ <div class="cards">
300
+ <div class="card"><div class="k">Total</div><div class="v">${totals.total}</div></div>
301
+ <div class="card good"><div class="k">Passed</div><div class="v">${totals.passed}</div></div>
302
+ <div class="card ${totals.failed ? "bad" : "good"}"><div class="k">Failed</div><div class="v">${totals.failed}</div></div>
303
+ ${kinds
304
+ .map(
305
+ kind =>
306
+ `<div class="card"><div class="k">${KIND_META[kind].badge}</div><div class="v">${totals.byKind[kind].passed}/${totals.byKind[kind].total}</div></div>`
307
+ )
308
+ .join("")}
309
+ <div class="card"><div class="k">Findings</div><div class="v">${findings.length}</div></div>
310
+ <div class="card"><div class="k">Duration</div><div class="v">${(summary.durationMs / 1000).toFixed(2)}s</div></div>
311
+ </div>
312
+
313
+ <p class="note">Process hygiene on shutdown: ${processHygiene}. Each test runs in sequence with a per-test
314
+ timeout, so a hang or a leaked promise surfaces in this report instead of hanging the suite.</p>
315
+
316
+ <p class="note warn"><strong>v1.1 changed two observable behaviours:</strong> a cooperative callback that
317
+ <em>rejects because of</em> an abort now settles <code>CANCELLED</code> (was <code>FAILED</code> + unhandled error), and
318
+ <code>FAIL_FAST</code> is sticky at node level (was a one-off queue flush). <strong>v1.2 added the
319
+ <code>SUCCESS_DEAD_END</code> lint error.</strong> <strong>v1.3 adds typed edges</strong> — <code>next</code> now accepts
320
+ <code>{ nextNode, result, test, parallel }</code> objects (exclusive, ordered, first-match-wins, deep
321
+ equality) alongside the original strings (parallel, unchanged), plus explicit routing via
322
+ <code>return new Next(target, value)</code> or <code>context.goto(target)</code>. <strong>v1.4 adds the
323
+ function registry</strong> — <code>run</code> and edge <code>test</code> may name a registered function,
324
+ resolved lazily at evaluation time, with <code>{ fingerprint, names }</code> reported on
325
+ every run — plus a JSON loader (<code>src/graph-json.ts</code>) that turns a graph
326
+ document into a definition with total, path-carrying validation. <strong>v1.5 loads scripts</strong> —
327
+ a document may declare where its functions live (<code>path</code>, inline
328
+ <code>source</code> or <code>url</code>), the script either exports them or registers itself through
329
+ <code>globalThis.GraphEngine.registerEngineNamespace()</code>, and a namespace is frozen once per
330
+ process. Purely additive: every v1.0–v1.4 graph keeps its behaviour. See
331
+ <code>CHANGELOG.md</code>.</p>
332
+
333
+ <h2>Results by category</h2>
334
+ ${summary.byCategory
335
+ .map(entry =>
336
+ bar(
337
+ entry.category,
338
+ entry.kind,
339
+ { total: entry.total, passed: entry.passed, failed: entry.failed },
340
+ entry.durationMs
341
+ )
342
+ )
343
+ .join("")}
344
+
345
+ <h2>Engine semantics documented by the findings (${findings.length})</h2>
346
+ <div class="findings">
347
+ ${findings
348
+ .map(
349
+ ({ test, text }) => `<div class="fcard">
350
+ <div class="fid">${escapeHtml(test.id)} · ${escapeHtml(KIND_META[test.kind]?.badge ?? test.kind)}</div>
351
+ <div class="ftitle">${escapeHtml(test.name)}</div>
352
+ <div class="fbody">${escapeHtml(text)}</div>
353
+ </div>`
354
+ )
355
+ .join("")}
356
+ </div>
357
+
358
+ <h2>Test inventory</h2>
359
+ <h3>${UNIT_CATEGORIES.length} unit categories (${totals.byKind.unit.total} tests)</h3>
360
+ <table>
361
+ <tr><th>Category</th><th>Focus</th><th>Count</th><th>Time</th></tr>
362
+ ${UNIT_CATEGORIES.map(([category, focus]) => {
363
+ const count = summary.tests.filter(
364
+ test => test.kind === "unit" && test.category === category
365
+ ).length;
366
+ const duration = categoryDurations.get(`unit::${category}`) ?? 0;
367
+ return `<tr><td>${escapeHtml(category)}</td><td class="muted">${escapeHtml(focus)}</td><td>${count}</td><td>${duration} ms</td></tr>`;
368
+ }).join("")}
369
+ </table>
370
+ ${(["integration", "async", "upgrade", "conformance", "lint", "edges", "registry", "graphjson", "scripts"] as TestKind[]).map(inventoryTable).join("")}
371
+
372
+ <h2>All test results</h2>
373
+ <div class="toolbar">
374
+ <button class="btn active" data-filter="all">All (${totals.total})</button>
375
+ ${kinds
376
+ .map(
377
+ kind =>
378
+ `<button class="btn" data-filter="${kind}">${KIND_META[kind].badge} (${totals.byKind[kind].total})</button>`
379
+ )
380
+ .join("")}
381
+ <button class="btn" data-filter="failed">Failed (${totals.failed})</button>
382
+ <button class="btn" data-filter="finding">Findings (${findings.length})</button>
383
+ <input type="search" id="q" placeholder="Search id, category, test name…"/>
384
+ </div>
385
+ <div id="rows">
386
+ ${kinds
387
+ .map(kind => testsOfKind(kind).map(renderTestRow).join(""))
388
+ .join("")}
389
+ </div>
390
+
391
+ <h2>How the suite was run</h2>
392
+ <div class="note">
393
+ <p style="margin-top:0"><code>npx tsc -p tsconfig.json</code> compiles <code>src/</code> and the tests to <code>js/</code>,
394
+ <code>npx tsc -p tsconfig.browser.json</code> proves the core is isomorphic (DOM libs, no Node types — asserted by the
395
+ UPGRADE pack), then <code>node js/tests/runner.js</code> executes all ${totals.total} cases (${kinds
396
+ .map(kind => `${totals.byKind[kind].total} ${KIND_META[kind].badge.toLowerCase()}`)
397
+ .join(" + ")}) and writes
398
+ <code>report/report.json</code> plus this page. No third-party test framework: <code>tests/harness.ts</code> provides
399
+ exception-based checks, per-test timeouts and log capture.</p>
400
+ <p>Artifacts: <code>src/automategraph.ts</code> (engine), <code>src/storage-adapter.ts</code> (adapter contract +
401
+ wire format), <code>src/storage-conformance.ts</code> (adapter checker), <code>tests/unit.test.ts</code>,
402
+ <code>tests/complex.test.ts</code>, <code>tests/async.test.ts</code>, <code>tests/upgrade.test.ts</code>,
403
+ <code>tests/conformance.test.ts</code>, <code>tests/lint.test.ts</code>, <code>tests/edges.test.ts</code>, <code>tests/registry.test.ts</code>, <code>tests/graphjson.test.ts</code>, <code>tests/scripts.test.ts</code>, <code>docs/diagrams/</code>,
404
+ <code>CHANGELOG.md</code>, <code>UPGRADE-PROPOSAL.md</code>.</p>
405
+ </div>
406
+
407
+ <footer>
408
+ ${failedTests.length === 0
409
+ ? `All ${totals.total} tests passed. Findings above are deliberate documentation of observed engine behaviour, not failures.`
410
+ : `${failedTests.length} failing test(s): ${failedTests.map(test => escapeHtml(test.id)).join(", ")}`}
411
+ </footer>
412
+ </div>
413
+ <script>
414
+ (function () {
415
+ var active = "all";
416
+ var buttons = Array.prototype.slice.call(document.querySelectorAll(".btn[data-filter]"));
417
+ var rows = Array.prototype.slice.call(document.querySelectorAll("#rows > details"));
418
+ var query = document.getElementById("q");
419
+
420
+ function apply() {
421
+ var needle = (query.value || "").trim().toLowerCase();
422
+ rows.forEach(function (row) {
423
+ var kindOk =
424
+ active === "all" ||
425
+ (active === "failed" && row.dataset.status === "failed") ||
426
+ (active === "finding" && row.dataset.finding === "true") ||
427
+ row.dataset.kind === active;
428
+ var textOk = needle === "" || row.dataset.search.indexOf(needle) !== -1;
429
+ row.style.display = kindOk && textOk ? "" : "none";
430
+ });
431
+ }
432
+
433
+ buttons.forEach(function (button) {
434
+ button.addEventListener("click", function () {
435
+ active = button.dataset.filter;
436
+ buttons.forEach(function (other) { other.classList.toggle("active", other === button); });
437
+ apply();
438
+ });
439
+ });
440
+
441
+ query.addEventListener("input", apply);
442
+ apply();
443
+ })();
444
+ </script>
445
+ </body>
446
+ </html>`;
447
+
448
+ const htmlPath = path.join(reportDir, "index.html");
449
+ fs.writeFileSync(htmlPath, html, "utf8");
450
+
451
+ return { htmlPath, jsonPath };
452
+ }
@@ -0,0 +1,58 @@
1
+ /* ============================================================
2
+ * Test entry point — runs every registered case, prints a
3
+ * summary and writes the web report (report/index.html + JSON).
4
+ * ============================================================ */
5
+
6
+ import * as path from "path";
7
+
8
+ import { TEST_KINDS, runAll } from "./harness";
9
+ import { findingsOf, writeReport } from "./report";
10
+
11
+ import "./unit.test";
12
+ import "./complex.test";
13
+ import "./async.test";
14
+ import "./upgrade.test";
15
+ import "./conformance.test";
16
+ import "./lint.test";
17
+ import "./edges.test";
18
+ import "./registry.test";
19
+ import "./graphjson.test";
20
+ import "./scripts.test";
21
+
22
+ async function main(): Promise<void> {
23
+ const summary = await runAll();
24
+
25
+ const workspaceRoot = path.resolve(__dirname, "..", "..");
26
+ const { htmlPath, jsonPath } = writeReport(summary, workspaceRoot);
27
+
28
+ process.stdout.write("\n────────────────────────────────────────────\n");
29
+
30
+ for (const kind of TEST_KINDS) {
31
+ const totals = summary.totals.byKind[kind];
32
+ if (totals.total === 0) continue;
33
+ const label = kind.toUpperCase().padEnd(12, " ");
34
+ process.stdout.write(
35
+ `${label}${totals.passed}/${totals.total} passed\n`
36
+ );
37
+ }
38
+
39
+ const findingCount = summary.tests.reduce(
40
+ (total, test) => total + findingsOf(test).length,
41
+ 0
42
+ );
43
+
44
+ process.stdout.write(
45
+ `TOTAL ${summary.totals.passed}/${summary.totals.total} passed in ${summary.durationMs}ms\n`
46
+ );
47
+ process.stdout.write(`FINDINGS ${findingCount}\n`);
48
+ process.stdout.write(`REPORT ${htmlPath}\n`);
49
+ process.stdout.write(`JSON ${jsonPath}\n`);
50
+ if (summary.unhandledRejections.length > 0) {
51
+ process.stdout.write(`WARN ${summary.unhandledRejections.length} unhandled rejection(s)\n`);
52
+ }
53
+ process.stdout.write("────────────────────────────────────────────\n");
54
+
55
+ process.exit(summary.totals.failed > 0 ? 1 : 0);
56
+ }
57
+
58
+ void main();