automategraph 0.0.0-stage → 1.5.0

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 ADDED
@@ -0,0 +1,292 @@
1
+ # Changelog
2
+
3
+ All notable changes to the AutomateGraph Execution Engine.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/); the project uses semver.
5
+
6
+ ## [1.5.0] — Script loading
7
+
8
+ A document can now say where its functions live and have them loaded: `scripts` entries point at
9
+ files, inline source or URLs, and the loader executes them in the same realm. Purely additive —
10
+ without a `scripts` field, nothing changes; the core stays isomorphic and the loader is a
11
+ separate Node-only module.
12
+
13
+ ### Added
14
+
15
+ - **`src/node/graph-scripts.ts`** (Node-only, excluded from the browser build):
16
+ `loadGraphScripts(document, options)` returns
17
+ `{ functions, namespaces, scripts, registry }` — ready to pass as
18
+ `createEngineFromJSON(document, { functions })`. Options: `baseDir` (default
19
+ `process.cwd()`), `collisions` (`"throw"` | `"override"`), `onLog`, and injectable
20
+ `readFile` / `fetchText`.
21
+ - **Script declarations** in the graph document: `scripts: [{ namespace, path | source | url,
22
+ format? }]`. Validated by the isomorphic parser with JSON paths (`scripts[0].path`…): exactly
23
+ one source, identifier-like namespace, unique namespaces per document, known keys only, and
24
+ `parseGraphJSONDocument()` now exposes them. **Validation runs before execution** — a document
25
+ with a broken graph never runs a script.
26
+ - **Two provision strategies**, mixable in one script:
27
+ - EXPORTS — `module.exports = { … }` (CJS), `export const …` / `export default { … }` (ESM);
28
+ non-function exports are ignored and reported in `ignoredExports`;
29
+ - SELF-REGISTRATION — `globalThis.GraphEngine.registerEngineNamespace(namespace, functions)`,
30
+ installed by the loader for the duration of the load only, realm restored afterwards
31
+ (verified by identity, including a pre-existing `GraphEngine` global).
32
+ - **The namespace freeze** — claims are PROCESS-global (`globalThis` + `Symbol.for`), so a
33
+ second claim of the same namespace throws `NAMESPACE_ALREADY_REGISTERED`; a self-registering
34
+ script gets that throw from inside its own body. Claims are made at registration time, so a
35
+ script that registers and then throws leaves the namespace claimed.
36
+ `registeredNamespaces()` inspects the claims; `resetGraphScriptNamespaces()` clears them
37
+ (test/dev hygiene). Namespace mismatch throws `NAMESPACE_MISMATCH`.
38
+ - **Names keep their spelling** — the namespace is a claim slot, never a name prefix.
39
+ Cross-script name collisions throw `FUNCTION_NAME_COLLISION` unless the load opts into
40
+ `{ collisions: "override" }` (which logs a warning).
41
+ - **Format handling** — `.cjs` → `require`, `.mjs` → dynamic `import` (kept real through a
42
+ tsc-CommonJS-safe indirection), `.js`/URL → source-hint detection then CJS, falling back to
43
+ ESM and vice versa; `format: "cjs" | "esm" | "auto"` pins it and disables the fallback.
44
+ - **Errors** — `GraphScriptError` with codes `SCRIPT_LOAD_FAILED`, `SCRIPT_NO_FUNCTIONS`,
45
+ `SCRIPT_EXPORT_INVALID`, `NAMESPACE_ALREADY_REGISTERED`, `NAMESPACE_MISMATCH`,
46
+ `FUNCTION_NAME_COLLISION`; a script-raised registration error surfaces unwrapped, never as a
47
+ generic load failure.
48
+ - **Tests** — `tests/scripts.test.ts` (26) + 15 fixtures under `tests/fixtures/scripts/`;
49
+ suite 256 → 282.
50
+ - **Example** — `examples/json-scripts.ts` + `examples/order-flow.scripted.graph.json` +
51
+ `examples/order-flow.functions.cjs` (`npm run example:scripts`): one document, a path script
52
+ and an inline self-registering script, run end-to-end, then the freeze and the reset.
53
+
54
+ ### Changed
55
+
56
+ - `tsconfig.browser.json` excludes `src/node/**` — the isomorphic guard still covers the whole
57
+ core (engine, storage contract, JSON parser), the Node loader is deliberately outside it.
58
+ - `parseGraphJSONDocument()` added to `src/graph-json.ts`; `parseGraphJSON()` is now its
59
+ `.definition` and keeps its exact v1.4 behaviour.
60
+
61
+ ### Fixed
62
+
63
+ - **The format fallback replayed a script that had already run.** A script that executes and
64
+ throws while loading used to be retried in the other module format — running its body a second
65
+ time (double side effects, and a duplicate namespace claim for self-registering scripts).
66
+ The fallback is now restricted to module-FORMAT failures (`ERR_REQUIRE_ESM`, ESM syntax under
67
+ `require`, module-scope references); a script's own error fails the load immediately, after
68
+ exactly one execution. Guarded by scripts case S16.
69
+
70
+ ## [1.4.0] — Function registry and graph JSON
71
+
72
+ Moves code out of the graph definition: `run` and edge `test` may name a function that lives in
73
+ a registry, and a graph may be a JSON document. Purely additive — every v1.0–v1.3 graph keeps
74
+ its behaviour, because inline functions are still accepted everywhere.
75
+
76
+ ### Added
77
+
78
+ - **Function registry** — `FunctionRegistry` / `createFunctionRegistry()` /
79
+ `FunctionRegistry.from()`: `register`, `extend`, `has`, `get`, `names()` (sorted), `size`,
80
+ `snapshot()`, `info()`, `fingerprint()`. Keys must match `FUNCTION_NAME_PATTERN`
81
+ (`vendor:retry`, `order.chargeCard` are legal); registering a non-function throws.
82
+ - **Engine-level table** — `new GraphExecutionEngine(graph, { functions })` plus
83
+ `engine.registerFunction(name, fn)` at any time, and `engine.registry` for
84
+ `{ fingerprint, names }`.
85
+ - **Graph-level table** — `GraphDefinition.functions`, merged OVER the engine's table
86
+ (a graph may deliberately override a shared function). Mixed graphs — some nodes inline,
87
+ some named — are normal.
88
+ - **Named references** — `NodeDefinition.run` and `NextEdge.test` accept a name. Resolution is
89
+ LAZY (per evaluation), so registering a function after construction, or while a run is in
90
+ flight, still takes effect.
91
+ - **`UNKNOWN_FUNCTION`** — a missing `run` name records an unhandled error, fails the
92
+ activation, schedules nothing, and is PERMANENT: not retried, never routed through
93
+ `onThrow`/`fallback` (those are error recovery; this is a wiring defect). A missing `test`
94
+ name records the error once per node+name, never matches, and lets the rest of the chain
95
+ evaluate. The run settles FAILED because the error stays unhandled.
96
+ - **Observability** — `fn` on `NodeResult` and on `node.started` / `node.succeeded` /
97
+ `node.failed`; `{ fingerprint, names }` on `graph.ready`, `ExecutionReport.registry` and
98
+ every `ExecutionSnapshot` (omitted when no registry is configured). The fingerprint
99
+ `fnr1:<count>:<hash>` covers sorted names + function source text.
100
+ - **`edgeHasMatcher()`** — shared predicate for conditional vs. else edges; it counts a named
101
+ `test`, which fixed a lint regression (below).
102
+ - **`src/graph-json.ts`** — `parseGraphJSON()`, `createEngineFromJSON()`, `isGraphJSON()`,
103
+ `GraphJSONError` (with `issues: [{ path, message }]`). Node `id` defaults to its key; unknown
104
+ properties are rejected at every level; dangling `next`/`onThrow`/`fallback`/entry/terminal
105
+ references are reported with their JSON paths; `functions` inside the document is rejected
106
+ with a pointer to the engine options. The loader is isomorphic (string or object in, no fs).
107
+ - **`nodeFunctions()` / `edgeTests()`** — typed table builders, because a single table holds
108
+ both signatures and a bare literal cannot infer a parameter type from that union.
109
+ - **Tests** — `tests/registry.test.ts` (28) and `tests/graphjson.test.ts` (19); suite 209 → 256.
110
+ - **Examples** — `examples/json-graph.ts` + `examples/order-flow.graph.json`
111
+ (`npm run example:graphjson`): one document, four registries, including the missing-name case.
112
+
113
+ ### Changed
114
+
115
+ - `NodeDefinition.run`, `NextEdgeObject.test`, `GraphDefinition.functions`,
116
+ `EngineOptions.functions`, `NodeResult.fn`, `ExecutionSnapshot.registry`,
117
+ `ExecutionReport.registry` — all additive; `NormalizedEdge` gained `testName`.
118
+ - Validation: `run` must be a function or an identifier-like name; an edge `test` may be a
119
+ function or a name; the graph table's keys and values are checked at construction.
120
+
121
+ ### Fixed
122
+
123
+ - **Lint treated a named `test` as an else edge.** After `test` was split into
124
+ `test`/`testName`, `lintGraph()` still asked `edge.test === undefined` — so every edge after a
125
+ named matcher was reported `UNREACHABLE_EDGE`, and `PARALLEL_EDGE_MATCHER_IGNORED` was missed
126
+ for parallel edges carrying a name. Both now use `edgeHasMatcher()`; regression-guarded by
127
+ registry cases R18/R19.
128
+
129
+ ## [1.3.0] — Typed edges and explicit routing
130
+
131
+ Turns `next` from a list of destinations into an ordered, conditional edge list, and lets a node
132
+ route itself. Purely additive: every v1.0–v1.2 graph keeps its behaviour, because a string entry
133
+ normalizes to an unconditional parallel edge — exactly its old meaning.
134
+
135
+ ### Added
136
+
137
+ - **`NextEdge`** — `next` accepts, in any mix:
138
+ | declared | normalized | meaning |
139
+ |---|---|---|
140
+ | `"audit"` | `{ nextNode, parallel: true }` | unconditional fan-out (v1.0 behaviour) |
141
+ | `{ nextNode: "b", result: 1 }` | `{ …, parallel: false, hasResult: true }` | exclusive, deep equality |
142
+ | `{ nextNode: "c", test: fn }` | `{ …, parallel: false }` | exclusive, predicate |
143
+ | `{ nextNode: "d" }` | `{ …, parallel: false }` | exclusive **else** edge |
144
+ | `{ nextNode: "x", parallel: true }` | unchanged | fires regardless of the exclusive chain |
145
+
146
+ Exclusive edges are evaluated **in declaration order, first match wins**; a matched edge
147
+ suppresses later exclusive edges, parallel edges are never suppressed. No match schedules
148
+ nothing and records no error (silent). `result` uses deep equality (`deepEquals`: `Object.is`
149
+ for primitives — so `NaN` matches `NaN` — element-wise for arrays, own keys for objects,
150
+ `getTime()` for Dates, cycle-safe); `result` and `test` together must both pass.
151
+ - **Explicit routing** — a node can choose its own successor:
152
+ - `return new Next(target, value)` — `value` becomes the node's output, `target` the
153
+ destination. `Next` with an empty target throws `INVALID_NEXT_TARGET`.
154
+ - `context.goto(target)` followed by the normal `return value` — for void/imperative nodes.
155
+ - An explicit route **replaces the whole edge list** for that activation (parallel edges
156
+ included), may target any node in the graph, and is validated at runtime: an unknown target
157
+ records `INVALID_GOTO_TARGET` and schedules nothing.
158
+ - `Next` wins over `goto` when both are used; each retry attempt starts with no route set.
159
+ - **`NodeContext.goto()`**, **`Next`**, **`isNext()`**, **`normalizeNextEdges()`**,
160
+ **`deepEquals()`** exported.
161
+ - **`node.succeeded` payload** gains an optional `route: { nextNode, via: "next-class" | "goto" }`
162
+ when the activation routed itself.
163
+ - **New lint codes**: `UNREACHABLE_EDGE` (error — an exclusive edge declared after an else edge
164
+ can never fire), `PARALLEL_EDGE_MATCHER_IGNORED` (warn — parallel edges fire unconditionally,
165
+ so their matcher is ignored), `TERMINAL_EDGE_IGNORED` (warn — a terminal node latches completion
166
+ before routing, so its edges never run).
167
+ - **Validation** for edge entries: non-empty `nextNode`, `test` must be a function, `parallel`
168
+ must be a boolean, malformed entries rejected, targets must exist.
169
+ - **`tests/edges.test.ts` — 26 cases** wiring the whole contract, including the `a → b/c/d/e/f`
170
+ picture as a single execution (5 seeds) and the finding that a runtime route is invisible to
171
+ static reachability, so the edge should be declared as a fallback.
172
+ - **`examples/branching-edges.ts`** + comparison of the three approaches in `examples/README.md`.
173
+
174
+ ### Changed
175
+
176
+ - `NodeDefinition.next` is now `readonly NextEdge[]` (was `readonly string[]`). Source-compatible:
177
+ string arrays are still valid and behave identically.
178
+ - Terminal nodes: settlement order is unchanged (terminal check → routing), now documented and
179
+ warned about by lint.
180
+ - `lintGraph()` reachability follows declared edges only; runtime `Next`/`goto` targets cannot be
181
+ seen statically.
182
+
183
+ ## [1.2.0] — Success dead-end linting
184
+
185
+ Closes a linting gap found while explaining the C-19 resilience fixture: a non-terminal node
186
+ could declare `onThrow` (or a `fallback`) and never be flagged, even though its **success path
187
+ led nowhere** — the node could only "continue" by failing.
188
+
189
+ ### Added
190
+
191
+ - **`SUCCESS_DEAD_END` lint code (error tier)** — fires when a node is not a terminal node and
192
+ has no `next` target, but does have at least one live error transition (`onThrow`, or `fallback`
193
+ when the effective policy is `FALLBACK`). Message: *"…cannot continue after success (no next
194
+ target); only its error transitions are declared. Add next targets or declare it as a terminal
195
+ node"*. The rule is intentional about the difference: `DEAD_END_NODE` stays for the total case
196
+ (no `next`, no `onThrow`, no live `fallback`), and the two codes are **mutually exclusive** — a
197
+ node with no `next` always produces exactly one of them.
198
+ - **`tests/lint.test.ts` — 13 cases** covering the rule, its boundaries (terminal nodes, nodes
199
+ with `next`, inert fallbacks, live fallbacks, unreachable nodes) and its engine integration in
200
+ both `lint: "warn"` and `lint: "error"` modes. The C-19 fixture and the C-01 order-pipeline
201
+ fixture are asserted directly, so the two graphs discussed in review are now pinned by CI.
202
+ - New report kind `LINT` in the dashboard (badge, filter, inventory table).
203
+
204
+ ### Changed
205
+
206
+ - `lintGraph()` message for `DEAD_END_NODE` now spells out why it fired:
207
+ *"(no next, onThrow or fallback)"*.
208
+ - Lint lines are logged at the **issue** severity (`error` for error-tier codes) even when the
209
+ mode is `"warn"`; the mode only decides whether error-tier issues are enforced. This was already
210
+ the behaviour — it is now covered by a test so it cannot drift.
211
+
212
+ ### Upgrade notes
213
+
214
+ `lint: "error"` users may see graphs rejected that previously passed. The fix is one of:
215
+ add a `next` target, handle the failure inside the node so it always succeeds, or declare the node
216
+ in `terminalNodes` (pair it with `completionPolicy: "ALL_TERMINALS"` / `"DRAIN"` if other branches
217
+ must still complete). Linting is off by default, so default behaviour is unchanged.
218
+
219
+ ## [1.1.0] — Wave 1: correctness, policy and contract
220
+
221
+ Wave 1 turns the 18 audited behaviours into explicit guarantees. **Defaults preserve
222
+ v1.0 semantics** except for the two behaviour changes marked BREAKING below. Everything
223
+ else is additive or opt-in.
224
+
225
+ ### Added
226
+
227
+ - **`CompletionPolicy`** (`GraphDefinition.completionPolicy`): `"FIRST_TERMINAL"` (default,
228
+ v1.0 behaviour) | `"ALL_TERMINALS"` | `"DRAIN"`.
229
+ - **`activation.dropped` event** — every time the scheduler clears pending work it now
230
+ reports the dropped activations and the reason (`logical-completion`, `all-terminals-reached`,
231
+ `fail-fast`, `graph-fail-fast`, `checkpoint-failure`, `step-budget-exceeded`, `cancellation`).
232
+ - **Cancellation drain deadline** (A2): setting `EngineOptions.cancellationTimeoutMs` now also
233
+ bounds the drain. Work still running when the deadline elapses is marked
234
+ `NodeStatus.ABANDONED`, the run settles, and `ExecutionReport.drainTimedOut` /
235
+ `abandonedActivations` report what happened. Unset ⇒ v1.0 behaviour (wait for cooperative drain).
236
+ - **`NodeStatus.ABANDONED`** — a cancellation-deadline outcome distinct from `CANCELLED`.
237
+ - **Real FALLBACK routing** (A5): `NodeDefinition.fallback` is used as the error transition when
238
+ the effective error policy is `FALLBACK` and no `onThrow` is declared. The error is recorded
239
+ as handled; unknown targets are rejected at construction.
240
+ - **Bounded concurrency and step budget** (A8): `EngineOptions.maxConcurrency` (default:
241
+ unbounded) and `EngineOptions.maxActivations` — the latter records `STEP_BUDGET_EXCEEDED`,
242
+ stops scheduling and settles the run, which makes runaway cycles terminate.
243
+ - **Graph linting** (A9): exported `lintGraph(graph)` plus `EngineOptions.lint`
244
+ (`"off"` default | `"warn"` | `"error"`). Issues: `DUPLICATE_ENTRY`, `DUPLICATE_TERMINAL`,
245
+ `UNREACHABLE_NODE`, `UNREACHABLE_TERMINAL`, `DEAD_END_NODE`, `FALLBACK_TARGET_IGNORED`.
246
+ Findings are surfaced on `ExecutionReport.lintIssues`.
247
+ - **`node.cancelled` event** — emitted when an activation settles as cancelled.
248
+ - **Introspection**: `getInFlightActivationIds()`, `getLintIssues()`.
249
+ - **Storage adapter contract** (`src/storage-adapter.ts`): `StorageAdapter`, `StorageCapabilities`,
250
+ shape validation (`storageAdapterViolations`, `assertStorageAdapter`, `defineStorageAdapter`) and
251
+ a canonical JSON wire format (`serializeSnapshot`, `deserializeSnapshot`, `stableStringify`,
252
+ `compareSnapshots`, `SNAPSHOT_FORMAT`/`SNAPSHOT_FORMAT_VERSION`).
253
+ - **Storage conformance suite** (`src/storage-conformance.ts`): `runStorageConformance(fixture)`
254
+ verifies round-trip, missing-key behavior, execution isolation, no aliasing on save/load,
255
+ overwrite, deterministic serialization, JSON stability, failed-save atomicity (when the fixture
256
+ can inject faults), listing, delete idempotency and close.
257
+ - **`InMemoryStorage`** is now a full reference adapter: `capabilities`, `listExecutionIds()`,
258
+ `delete()`, `close()`.
259
+ - **Isomorphic build proof**: `tsconfig.browser.json` (DOM libs, `types: []`, `noEmit`) plus a test
260
+ that asserts the core compiles with no Node APIs. Timers are `setTimeout`-based only.
261
+ - **New test packs**: `tests/upgrade.test.ts` (19 cases) and `tests/conformance.test.ts` (11 cases),
262
+ wired into a kind-driven harness/report. Suite total: **170 tests**.
263
+
264
+ ### Changed (BREAKING)
265
+
266
+ - **A1 — cancellation symmetry.** A node callback that *rejects because of* an abort now settles
267
+ `NodeStatus.CANCELLED` with a **handled** `GRAPH_CANCELLED` error record. Previously it settled
268
+ `FAILED` with an unhandled error, which made cooperative cancellation asymmetric.
269
+ - **A4 — `FAIL_FAST` is sticky.** A node-level `FAIL_FAST` now installs the same persistent stop as
270
+ a graph-level one (it previously flushed the pending queue once, which was a no-op under unbounded
271
+ concurrency). A node-level `CONTINUE` still cannot shield other branches from a graph-level stop.
272
+
273
+ ### Fixed
274
+
275
+ - **A6 — `NodeResult.error`** is now populated for `FAILED` and `TIMED_OUT` activations (the field
276
+ existed but was never assigned).
277
+ - **A10 — dead code and hygiene**: removed the unused `resolveRun` field; in-flight activations are
278
+ tracked and released deterministically (`getInFlightActivationIds()` is empty after a settle);
279
+ `NodeJS.ErrnoException` replaced with a structural cast so the core compiles without Node types.
280
+
281
+ ### Notes
282
+
283
+ - `cancel()` still returns before the graph settles; in-flight work drains cooperatively. Outputs
284
+ produced by abandoned activations are never adopted.
285
+ - `InMemoryStorage.version` moved to `1.1.0` to reflect the adapter surface.
286
+
287
+ ## [1.0.0]
288
+
289
+ - Initial single-file engine: graph validation, event bus, in-memory storage, scheduler with
290
+ unbounded concurrency, retry/error policies, cooperative cancellation, atomic checkpoints.
291
+ - Audit suite: 140 tests (100 unit, 20 complex integration, 20 async-callback) documenting
292
+ 18 engine behaviours, with a self-contained HTML/JSON report and a live preview server.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 ammisaidkaidi
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.