@cynodia/axiom 0.13.0-alpha.1 → 0.14.0-alpha.1

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/README.md CHANGED
@@ -33,7 +33,7 @@ Shorter forms of the same routing: [`AGENTS.md`](AGENTS.md) and [`llms.txt`](llm
33
33
  at this package's root.
34
34
 
35
35
  **Status: experimental / alpha.** The API may change between alpha releases. The
36
- documentation in `docs/` describes this exact version, `0.13.0-alpha.1`.
36
+ documentation in `docs/` describes this exact version, `0.14.0-alpha.1`.
37
37
 
38
38
  ## Installation
39
39
 
@@ -47,7 +47,7 @@ Every release of this project is a pre-release and npm's `latest` tag points at
47
47
  plain command above installs the current version. **There is no `alpha` dist-tag** — the tag
48
48
  was removed once it stopped tracking releases, and `npm install @cynodia/axiom@alpha` now
49
49
  fails with a 404. Pin the exact version instead when one is needed:
50
- `npm install @cynodia/axiom@0.13.0-alpha.1`.
50
+ `npm install @cynodia/axiom@0.14.0-alpha.1`.
51
51
 
52
52
  These are ES modules compiled to ES2022; import them with `import`, not `require`. There is
53
53
  no published Axiom CLI. `@cynodia/axiom-server`'s SQLite persistence adapter additionally
@@ -193,6 +193,7 @@ focused document when the reference is not specific enough for the question at h
193
193
  | Evolving a deployed schema: `MigrationDef`, fingerprint, the startup gate, `executeMigration`, providers | [`docs/MIGRATIONS.md`](docs/MIGRATIONS.md) |
194
194
  | Running N authority processes at once: ownership, leases, fencing, delivery guarantees, version skew | [`docs/DISTRIBUTED_AUTHORITY.md`](docs/DISTRIBUTED_AUTHORITY.md) |
195
195
  | Observing a `QueryDef` result over time: live deltas, reconnect, cursor, backpressure, transport | [`docs/LIVE_QUERIES.md`](docs/LIVE_QUERIES.md) |
196
+ | Durable workflows: steps, bindings, event waits, timers, retries, cancellation, crash recovery | [`docs/WORKFLOWS.md`](docs/WORKFLOWS.md) |
196
197
  | Machine queries, mutation impact and graph transformations | [`docs/AGENT_API.md`](docs/AGENT_API.md) |
197
198
 
198
199
  `docs/AGENT_REFERENCE.md` plus the `.d.ts` declarations are intended to be sufficient on
@@ -1,6 +1,6 @@
1
1
  # Actions and transactions
2
2
 
3
- Axiom 0.13.0-alpha.1. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.14.0-alpha.1. An action is behavior expressed as data, executed as a transaction.
4
4
 
5
5
  ```ts
6
6
  {
package/docs/AGENT_API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent API
2
2
 
3
- Axiom 0.13.0-alpha.1. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.14.0-alpha.1. The machine-facing interface. Agents query semantics and apply
4
4
  structural transformations; they never edit generated code.
5
5
 
6
6
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.13.0-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.14.0-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
4
4
  declarations before authoring or modifying an Axiom application.
5
5
 
6
6
  Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
@@ -730,7 +730,8 @@ Portable artifacts, for a runtime written in another language:
730
730
  @cynodia/axiom-server/schema/server-ir.v4.schema.json JSON Schema for axiom.server.v4
731
731
  @cynodia/axiom-server/schema/server-ir.v5.schema.json JSON Schema for axiom.server.v5
732
732
  @cynodia/axiom-server/schema/server-ir.v6.schema.json JSON Schema for axiom.server.v6
733
- @cynodia/axiom-server/schema/server-ir.v7.schema.json JSON Schema for axiom.server.v7 (latest)
733
+ @cynodia/axiom-server/schema/server-ir.v7.schema.json JSON Schema for axiom.server.v7
734
+ @cynodia/axiom-server/schema/server-ir.v8.schema.json JSON Schema for axiom.server.v8 (latest)
734
735
  @cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
735
736
  @cynodia/axiom-server/conformance/queries/<name>.json one query conformance fixture (axiom.conformance.v4)
736
737
  @cynodia/axiom-server/conformance/migrations/<name>.json one migration conformance fixture (axiom.conformance.v5)
@@ -1069,12 +1070,27 @@ insert/remove above. `insert`/`move` carry a target index into the final result.
1069
1070
  conformance oracle.
1070
1071
 
1071
1072
  Invalidation: conservative static dependency set (`queryDependencies`) — source entity, used
1072
- relationship endpoints, read-policy entity, `StateDef`s read by any clause or the policy
1073
- predicate; an unresolved `ref` → `broad` (re-evaluate on every commit). Local commits wake
1074
- the engine synchronously; a **remote** commit is seen via `persistence.revision()` re-observed
1075
- on an interval (`liveQueryPollMs`, default 250). No broadcast, no sticky routing. A
1076
- non-Axiom write to the provider store is not observed until the next Axiom commit
1077
- (documented limit).
1073
+ relationship endpoints, read-policy entity; an unresolved `ref` → `broad` (re-evaluate on
1074
+ every commit). Local commits wake the engine synchronously; a **remote** commit — a
1075
+ `StateDef` *or* a `provider-record` mutation — is seen through the **observable application
1076
+ revision**, projected from two durable sources re-observed on an interval (`liveQueryPollMs`,
1077
+ default 250): `stateRevision` (`persistence.revision()`, StateDef commits) and
1078
+ `dataGeneration` (Σ of each provider's `observedMutationGeneration()`, advanced **atomically
1079
+ inside `applyMutations`**). `server.revisionInspection()` → `{ applicationRevision,
1080
+ stateRevision, dataGeneration }` (kept distinct). No broadcast, no sticky routing, no Redis.
1081
+ Every `provider-record` mutation committed through an Axiom `ActionDef` (insert / update /
1082
+ delete) participates — a `StateDef` "sync pulse" is **not** required. A non-Axiom write
1083
+ straight to the provider store is not observed until the next Axiom commit (documented
1084
+ limit). `DataProvider.capabilities.mutationObservation` = `'durable'` (SQLite) /
1085
+ `'in-process'` (memory) / `'none'`; `openLiveQuery` on a writable `'none'` provider is
1086
+ refused `LIVE_QUERY_PROVIDER_NOT_OBSERVABLE`.
1087
+
1088
+ QueryDef scope: a query clause (and a `ReadPolicy` predicate) may reference `ref(rowScopeId)`
1089
+ / parameters / relationship binds / `PRINCIPAL` / nested iteration scopes — **not** a
1090
+ `StateDef`. A `StateDef` ref is `QUERY_STATE_REF_NOT_ALLOWED` at `validateGraph` /
1091
+ `compileToServerIR`; `analyzeLiveQuery` → `not-live-capable` +
1092
+ `dependencies.unsupportedStateRefs`; runtime guards a hand-built IR. Bind a runtime-varying
1093
+ value as a query **parameter**.
1078
1094
 
1079
1095
  Authorization: `ReadPolicy` + principal bound into every re-evaluation; a row that becomes
1080
1096
  invisible leaves as `remove` / `reset`; unauthorized data never retained past one
@@ -1101,7 +1117,90 @@ Portable tier: `axiom.conformance.v7` (`conformance/live/`), `runLiveQueryConfor
1101
1117
  / `runLiveQueryConformanceSuite`. Server IR stays `axiom.server.v7`.
1102
1118
 
1103
1119
  Diagnostics: `LIVE_QUERY_NOT_CAPABLE` `LIVE_QUERY_CURSOR_INVALID`
1104
- `LIVE_QUERY_CURSOR_INCOMPATIBLE` `LIVE_QUERY_EVALUATION_FAILED`.
1120
+ `LIVE_QUERY_CURSOR_INCOMPATIBLE` `LIVE_QUERY_EVALUATION_FAILED`
1121
+ `LIVE_QUERY_PROVIDER_NOT_OBSERVABLE` `QUERY_STATE_REF_NOT_ALLOWED`.
1122
+
1123
+ ## DURABLE WORKFLOWS
1124
+
1125
+ Full model: [`WORKFLOWS.md`](WORKFLOWS.md).
1126
+
1127
+ A `WorkflowDef` (graph node kind `workflow`) is a long-running semantic computation with a
1128
+ **durable control position** — not a background promise, a persisted callback, a job-queue
1129
+ entry, a cron task, a mutable JSON blob or a process-local listener. No application script
1130
+ body. Server IR `axiom.server.v8`; a graph with no workflow compiles to the byte-identical
1131
+ v1–v7 it always did and its `semanticFingerprint` is unchanged.
1132
+
1133
+ Steps (six, portable): `action` (invoke an `ActionDef` under the workflow principal;
1134
+ `onError` edge; `retry` policy), `wait-event` (wait for a matching canonical `EventDef`;
1135
+ `where` predicate, `bind` → single-assignment bindings, `timeout` + `onTimeout`), `timer`
1136
+ (`after: {seconds}` / `at: Expression`, target captured once), `branch` (deterministic
1137
+ `when` → `then`/`else`), `complete` (→ `completed`, `output`), `fail` (→ `failed`, `error`).
1138
+ Acyclic — `validateGraph` rejects a control-flow cycle.
1139
+
1140
+ Expression scope (closed): `ref(<input id>)`, `ref(<binding id>)`, `ref('EVENT')` (only in a
1141
+ `wait-event` `where`/`bind`), `ref('PRINCIPAL')`. **Not** a `StateDef`
1142
+ (`WORKFLOW_EXPRESSION_SCOPE`), **not** a `QueryDef`, **not** `now`/`uuid`/`random`
1143
+ (`WORKFLOW_NONDETERMINISTIC`). Model dynamic state as an `ActionDef` before a branch.
1144
+
1145
+ Start: `server.startWorkflow({ workflowId, arguments, credential, idempotencyKey })` →
1146
+ `{ instanceId, status }`. Idempotent on `(workflowId, principalFingerprint, idempotencyKey,
1147
+ compatibilityFingerprint)` — a retry after a lost response is one logical instance. The
1148
+ principal is bound; every action step runs as it and re-evaluates current authorization.
1149
+
1150
+ Identity: `instanceRevision` (monotone durable) + a coordination `fence` — every transition
1151
+ is a CAS `R + fence → R+1`, atomic, check *inside* the write transaction. `activationId` =
1152
+ `"<stepId>#0"` (a future loop feature revisits without changing instance identity). Action
1153
+ invocation identity = `"<instanceId>/<activationId>"`, used as the `ActionDef` request id so
1154
+ a crash between "action committed" and "transition recorded" reconciles rather than
1155
+ double-executing. **Exactly-once logical transition** per activation; **not** exactly-once
1156
+ physical effect execution (effect system governs that; logical effect identity is stable
1157
+ across workflow retries).
1158
+
1159
+ Event waits: the durable wait registration (`eventId`, correlation, `sinceEventSeq`) commits
1160
+ in the *same* transition — no "waiting then subscribe" gap. Driven by Axiom's single inbound
1161
+ event pipeline; startup/failover replays a match that landed in a crash window from
1162
+ `sinceEventSeq`; existing dedup means a wait transitions at most once; fanout (a match
1163
+ unblocks every independently-matching instance, never global consume). Event vs timeout:
1164
+ exactly one wins on `instanceRevision`.
1165
+
1166
+ Timers: target instant computed once on activation and stored — a restart does not
1167
+ recompute `now + after`. Physically at-least-once firing, logically exactly-once transition.
1168
+ The waiting row *is* the timer.
1169
+
1170
+ Retries: `retry: { maxAttempts, initialDelaySeconds, backoffMultiplier, maxDelaySeconds }`.
1171
+ Retryable vs terminal is structured, never message-string parsing. Attempt count +
1172
+ `nextEligibleAt` are durable (authority death does not reset). Lease/fencing → one current
1173
+ executor.
1174
+
1175
+ Cancellation: `server.cancelWorkflow(instanceId, credential)` — idempotent, a fenced durable
1176
+ transition to `cancelled`. **Not** rollback (committed actions / dispatched effects stand;
1177
+ no auto-compensation). A later timer/event does not transition a terminal instance. Terminal
1178
+ states are durable and irreversible; a stale authority cannot resurrect one.
1179
+
1180
+ Multi-authority: leaderless. Any compatible authority advances any eligible instance;
1181
+ per-instance lease+fence (reused 0.12 `CoordinationProvider`); stale owner refused. Startup
1182
+ discovers runnable / retry-due / timer-due / recoverably-waiting instances and advances them
1183
+ — the application does **not** scan stuck workflows, call `resumeWorkflow`, or re-register
1184
+ timers/waits. Same logical outcome at 1 or N authorities. Incompatible build refuses to
1185
+ advance an instance (fail-closed).
1186
+
1187
+ Inspection: `server.getWorkflow(instanceId)` / `inspectWorkflows(limit)` — `status`,
1188
+ `currentStepId`, `activationId`, `attempt`, `waitingReason`, `nextEligibleAt`,
1189
+ `instanceRevision`, `failure`, `output` (no secrets). `server.workflowHistory(instanceId)`
1190
+ — the durable transition log. `AgentAPI.analyzeWorkflow(workflowId)` — static: inputs, steps
1191
+ + edges, action/event dependencies, terminal outcomes, acyclicity, possible wait reasons.
1192
+
1193
+ `WorkflowStore`: `createMemoryWorkflowStore()` (single process), `createSqliteWorkflowStore({
1194
+ location })` (cross-process — `BEGIN IMMEDIATE`, check inside the transaction, `busy_timeout`,
1195
+ `CREATE TABLE IF NOT EXISTS` + `INSERT OR IGNORE` init). Portable tier
1196
+ `axiom.conformance.v8` (`conformance/workflow/`), `runWorkflowConformanceFixture` / `Suite`.
1197
+
1198
+ Diagnostics (validation): `WORKFLOW_ENTRY_NOT_FOUND` `WORKFLOW_STEP_NOT_FOUND`
1199
+ `WORKFLOW_DUPLICATE_STEP_ID` `WORKFLOW_INVALID_STEP` `WORKFLOW_CYCLE_NOT_ALLOWED`
1200
+ `WORKFLOW_ACTION_NOT_FOUND` `WORKFLOW_EVENT_NOT_FOUND` `WORKFLOW_BINDING_NOT_FOUND`
1201
+ `WORKFLOW_DUPLICATE_BINDING` `WORKFLOW_INVALID_RETRY_POLICY` `WORKFLOW_INVALID_TIMER`
1202
+ `WORKFLOW_UNREACHABLE_STEP` `WORKFLOW_NO_TERMINAL` `WORKFLOW_EXPRESSION_SCOPE`
1203
+ `WORKFLOW_NONDETERMINISTIC`.
1105
1204
 
1106
1205
  ## Metadata classes
1107
1206
 
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.13.0-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.14.0-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
4
4
  alternative.
5
5
 
6
6
  ## 1. Field names as entity runtime keys
@@ -908,3 +908,141 @@ A `QueryDef` whose filter / sort / projection reads a nondeterministic builtin i
908
908
  changed" has no meaning, and `openLiveQuery` returns `LIVE_QUERY_NOT_CAPABLE`. Express the
909
909
  window as a bound parameter (`ref(P_SINCE)`), or observe a status field that an action or a
910
910
  scheduled trigger maintains, and let *that* commit drive the live query.
911
+
912
+ ## 69. Assuming a provider-record commit only needs to wake the *local* live-query engine
913
+
914
+ ```ts
915
+ // WRONG (the shape of the defect the 0.13.1 hardening fixed) — mutate provider data and rely on the
916
+ // authority that did the write to notice.
917
+ { kind: 'set', target: providerRecordFieldLocation(E_ORDER, F_ID, ref(P_ID), F_STATUS), value: literal('shipped') }
918
+ // … a live query served from a *different* authority never re-evaluates.
919
+ ```
920
+
921
+ Why wrong: a `provider-record` mutation touches the `DataProvider`, not the
922
+ `PersistenceAdapter`, so `persistence.revision()` does not move and a remote authority's
923
+ poll sees nothing. A committed change stays permanently invisible where it is not observed,
924
+ and the live result then depends on *which* authority happens to serve the query — a
925
+ topology-dependent meaning.
926
+
927
+ The framework closes this: the SQLite provider advances a durable `mutationGeneration`
928
+ **inside** the same `applyMutations` transaction, and every authority's live-query poll
929
+ re-reads it alongside `persistence.revision()`. There is nothing for the application to do —
930
+ no revision bump, no `StateDef` "sync pulse", no broadcast. If your own custom `DataProvider`
931
+ supports writes, it MUST implement `observedMutationGeneration()` (advanced atomically with
932
+ `applyMutations`) or declare `mutationObservation: 'none'` and accept that
933
+ `openLiveQuery` is refused for it.
934
+
935
+ ## 70. Bumping a `StateDef` counter so a provider-record change "syncs"
936
+
937
+ ```ts
938
+ // WRONG — the audit counter is load-bearing for correctness, which it must never be.
939
+ operations: [
940
+ { kind: 'set', target: providerRecordFieldLocation(E_ORDER, F_ID, ref(P_ID), F_STATUS), value: ref(P_STATUS) },
941
+ { kind: 'set', target: stateLocation(S_OPS), value: binary('add', ref(S_OPS), literal(1)) }, // "so remote live queries wake"
942
+ ]
943
+ ```
944
+
945
+ Why wrong: it works by accident — the `StateDef` write moves `persistence.revision()`, which
946
+ the poll observes — and it fails the moment an action legitimately mutates only provider
947
+ records. Live-query invalidation observes the *provider's* durable generation directly; a
948
+ provider-record-only action is fully observable on its own. Write the `StateDef` only if the
949
+ application actually needs it.
950
+
951
+ ## 71. Referencing a `StateDef` from a `QueryDef` filter
952
+
953
+ ```ts
954
+ // WRONG — `QUERY_STATE_REF_NOT_ALLOWED` at validateGraph / compileToServerIR.
955
+ {
956
+ id: Q_LARGE_ORDERS, kind: 'query', source: E_ORDER, rowScopeId: ROW,
957
+ filter: binary('gte', field(ref(ROW), F_TOTAL), ref(S_THRESHOLD)), // S_THRESHOLD is a StateDef
958
+ }
959
+ ```
960
+
961
+ Why wrong: a `QueryDef` clause is evaluated **by the `DataProvider`**, whose scope has no
962
+ authority state — `ref(S_THRESHOLD)` would resolve to nothing and the query would silently
963
+ return an empty (or wrong) result. Before 0.13.1, `validateGraph` accepted this,
964
+ `queryDependencies` advertised the "dependency", and only execution disagreed. Now every
965
+ layer rejects it consistently. Bind the threshold as a **query parameter** and pass it from
966
+ the action, trigger or client that runs the query. (A `ReadPolicyDef` predicate shares the
967
+ same state-free scope and the same rule.)
968
+
969
+ ## 72. A hand-rolled workflow state machine on top of `StateDef` + triggers + counters
970
+
971
+ ```ts
972
+ // WRONG — semantic escape: the application reimplements durable orchestration.
973
+ { id: S_ORDER_STAGE, kind: 'state', authority: 'server', valueType: enumType(['reserving','awaiting_payment','shipping','done']) }
974
+ // + a scheduler job to time out payment
975
+ // + an event handler that flips the stage
976
+ // + a manual retry counter field
977
+ // + a "leader" flag so only one process advances it
978
+ ```
979
+
980
+ Why wrong: every one of those pieces — state field, scheduler, event handler, retry counter,
981
+ idempotency key, crash recovery, leader election — is orchestration infrastructure the
982
+ framework already owns. Model it as a `WorkflowDef`: an `action` step, a `wait-event` step
983
+ with a `timeout`, a `branch`, `complete` / `fail`. Exactly-once logical transition, no
984
+ registration gap, durable retries, fenced ownership and crash recovery come for free.
985
+
986
+ ## 73. Generating your own job / activation ids
987
+
988
+ ```ts
989
+ // WRONG — a random id per attempt makes a crash-recovered retry look like new work.
990
+ const jobId = crypto.randomUUID();
991
+ await chargeCard({ idempotencyKey: jobId });
992
+ ```
993
+
994
+ Why wrong: a workflow action step already executes with a stable logical invocation identity
995
+ (`<instanceId>/<activationId>`), reused across retries and across authority failover, and an
996
+ effect it dispatches keeps a stable logical effect identity. Rolling your own id breaks
997
+ double-execution reconciliation — the reclaiming authority cannot tell the action already
998
+ committed.
999
+
1000
+ ## 74. A process timer (`setTimeout` / cron) for a workflow wait
1001
+
1002
+ ```ts
1003
+ // WRONG — the wait is not durable; a crash loses it.
1004
+ setTimeout(() => advanceWorkflow(id), 7 * 24 * 3600 * 1000);
1005
+ ```
1006
+
1007
+ Why wrong: a `timer` step's target instant is captured once and stored in the durable
1008
+ transition record; the waiting row *is* the timer, rediscovered on startup. A `setTimeout`
1009
+ evaporates on restart, and a `now + delay` recomputed after a crash silently extends the
1010
+ wait.
1011
+
1012
+ ## 75. Subscribing to an event emitter *after* marking the workflow "waiting"
1013
+
1014
+ ```ts
1015
+ // WRONG — a crash between these two lines loses a matching event forever.
1016
+ await store.setStatus(id, 'waiting');
1017
+ emitter.on('payment_confirmed', () => resumeWorkflow(id));
1018
+ ```
1019
+
1020
+ Why wrong: the durable event-wait registration commits in the **same** transaction as the
1021
+ transition into the `wait-event` step, and a match that lands during a crash window is
1022
+ replayed from the durable `sinceEventSeq`. There is no in-memory-only registration window.
1023
+
1024
+ ## 76. Treating `cancelWorkflow` as an undo
1025
+
1026
+ ```ts
1027
+ // WRONG — cancellation does not reverse anything.
1028
+ await server.cancelWorkflow(id); // "and now the charge is refunded and inventory released"
1029
+ ```
1030
+
1031
+ Why wrong: cancellation means *do not execute future steps*. It does not undo committed
1032
+ actions or dispatched external effects, and 0.14 has no automatic compensation. If a
1033
+ workflow needs to release a reservation on a timeout, that is an explicit `onTimeout` edge to
1034
+ a `release_inventory` action step.
1035
+
1036
+ ## 77. Sticky routing / a workflow leader / polling the workflow table
1037
+
1038
+ ```ts
1039
+ // WRONG — none of this is needed and all of it is fragile.
1040
+ route(`/wf/${id}`, toAuthority(ownerOf(id))); // sticky routing
1041
+ if (isLeader) { for (const w of loadAllWorkflows()) advance(w); } // leader + full scan
1042
+ setInterval(() => sql`SELECT * FROM workflows WHERE stuck = 1`, 1000); // app polling
1043
+ ```
1044
+
1045
+ Why wrong: workflow execution is leaderless — any compatible authority advances any eligible
1046
+ instance under a fenced per-instance lease. Recovery discovery is bounded and indexed and
1047
+ runs on startup automatically. Reading status is safe through any authority. The application
1048
+ writes none of this.
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.13.0-alpha.1. How an application crosses the trust boundary.
3
+ Axiom 0.14.0-alpha.1. How an application crosses the trust boundary.
4
4
 
5
5
  Until 0.5.x an Axiom application executed locally. 0.6 adds an **authority**: a generic
6
6
  runtime that owns state, decides mutations and persists them. The same semantic graph
@@ -298,6 +298,7 @@ interface PersistenceAdapter {
298
298
  - A semantic transaction that writes several states MUST persist as **one unit**. An adapter must never apply a subset.
299
299
  - `commit` carries the revision each written state had when the transaction began. A mismatch MUST refuse the commit rather than overwrite.
300
300
  - Derived state is recomputed, never stored.
301
+ - `commit` may also carry an optional `idempotency: { key, response, window }` written in the **same** transaction as the state (spec14pt2 F1); an adapter that also implements the optional `loadIdempotentResponse` / `recordIdempotentResponse` pair gives cross-restart, cross-authority exactly-once for a workflow's ActionDef step. `createMemoryPersistence` and `createSqlitePersistence` both do; an adapter that does not leaves that reconciliation to the (memory-only) request window, which is documented rather than assumed away.
301
302
 
302
303
  | Adapter | For |
303
304
  | --- | --- |
@@ -497,6 +498,8 @@ does for a local failure.
497
498
  | `QUERY_PROVIDER_FAILURE` | The provider reported a failure executing the query or applying a provider-record mutation. Carries no state value. |
498
499
  | `QUERY_RESULT_TYPE_MISMATCH` | The provider returned rows that do not conform to the query's declared result shape. |
499
500
  | `QUERY_PROVIDER_MISSING` | No `DataProvider` is registered for this query's source entity (`AxiomServerOptions.dataProvider` / `dataProviders`). |
501
+ | `QUERY_STATE_REF_NOT_ALLOWED` | A `QueryDef` clause or a `ReadPolicyDef` predicate references a `StateDef`. A query executes on the `DataProvider`, which binds no authority state, so the reference would evaluate to nothing. `compileToServerIR` rejects such a graph; this is the runtime guard if a hand-built IR bypasses validation (spec13.1 F2, §81). Bind a runtime-varying value through a query parameter. |
502
+ | `LIVE_QUERY_PROVIDER_NOT_OBSERVABLE` | `openLiveQuery` for a query whose writable `DataProvider` reports `capabilities.mutationObservation === 'none'` — its committed mutations cannot be made observable to another authority, so a distributed live query is refused rather than served silently stale on a peer (spec13.1 F1, §33, §123). |
500
503
  | `LIVE_QUERY_NOT_CAPABLE` | `openLiveQuery` was called for a `QueryDef` that cannot be observed live — its filter / sort / projection reads a nondeterministic builtin (`now`, `uuid`). An aggregate or grouped query is *not* refused here: it is served live as whole-result `reset`s (spec13 §145, §146, §148). |
501
504
  | `LIVE_QUERY_CURSOR_INVALID` | An `axiom.live-query-cursor.v1` was tampered with, unsigned, or minted for a different query / principal / arguments / read policy. Fail-closed: continuing from it is refused and nothing is disclosed (spec13 §33-§35). |
502
505
  | `LIVE_QUERY_CURSOR_INCOMPATIBLE` | A live-query cursor was minted by an authority whose schema fingerprint, semantic fingerprint or server contract differs from this one's — the same fail-closed compatibility check the distributed work store applies. A presentation-only graph change does *not* trigger this (spec13 §79-§82). |
@@ -687,8 +690,9 @@ page plus the conformance fixtures.
687
690
  | `axiom.server.v5` | `SubscriptionDef` and `StorageDef`, and the `blob-metadata`/`blob-commit`/`blob-delete` operation kinds — the inbound external-I/O direction and binary object storage | 0.9.0 |
688
691
  | `axiom.server.v6` | `QueryDef`, `RelationshipDef` and `ReadPolicyDef`, the `query` operation kind, and the `provider-record` location — the semantic data-access & query layer over large authoritative datasets | 0.10.0 |
689
692
  | `axiom.server.v7` | `MigrationDef` and the closed migration-operation vocabulary, plus the top-level `schemaVersion` and `schemaFingerprint` fields — semantic schema evolution over persisted canonical data | 0.11.0 |
693
+ | `axiom.server.v8` | `WorkflowDef` and the closed workflow-step vocabulary (`action`, `wait-event`, `timer`, `branch`, `complete`, `fail`) — durable, portable long-running orchestration | 0.14.0 |
690
694
 
691
- `SERVER_IR_CONTRACTS` enumerates all seven, and is the single source of truth this table is
695
+ `SERVER_IR_CONTRACTS` enumerates all eight, and is the single source of truth this table is
692
696
  tested against — `packages/demo/test/documentation.test.ts` fails if a contract in
693
697
  `SERVER_IR_CONTRACTS` has no row here, or a row here names a contract the code does not
694
698
  declare (spec 8.2 §7-8). The rules:
@@ -1,6 +1,6 @@
1
1
  # Constraints
2
2
 
3
- Axiom 0.13.0-alpha.1. Two constructs, answering different questions. They are not
3
+ Axiom 0.14.0-alpha.1. Two constructs, answering different questions. They are not
4
4
  interchangeable.
5
5
 
6
6
  | | Question | Sees |
@@ -1,6 +1,6 @@
1
1
  # Distributed authority
2
2
 
3
- *This document describes Axiom `0.13.0-alpha.1`.*
3
+ *This document describes Axiom `0.14.0-alpha.1`.*
4
4
 
5
5
  The authoritative runtime (`docs/AUTHORITY.md`) may run as **more than one process at the
6
6
  same time**, over one shared persistence provider, without any change to the
package/docs/EFFECTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Effects
2
2
 
3
- Axiom 0.13.0-alpha.1. External effects are not rollback-capable state mutations. This file
3
+ Axiom 0.14.0-alpha.1. External effects are not rollback-capable state mutations. This file
4
4
  is the delivery model; [`AUTHORITY.md`](AUTHORITY.md#external-effects) is the load-bearing
5
5
  statement of why, and [`INTEGRATIONS.md`](INTEGRATIONS.md) is the operation vocabulary this
6
6
  builds on.
package/docs/EVENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Events
2
2
 
3
- Axiom 0.13.0-alpha.1. An event is a typed fact — something that happened — never work
3
+ Axiom 0.14.0-alpha.1. An event is a typed fact — something that happened — never work
4
4
  itself. [`AUTHORITY.md`](AUTHORITY.md#external-events) is the load-bearing statement;
5
5
  this file is the vocabulary and the webhook delivery mechanism. A **subscription** is the
6
6
  other way an external fact becomes an `EventDef` payload — see
@@ -1,6 +1,6 @@
1
1
  # Expressions
2
2
 
3
- Axiom 0.13.0-alpha.1. An expression describes **what value is computed**. It is a tree of
3
+ Axiom 0.14.0-alpha.1. An expression describes **what value is computed**. It is a tree of
4
4
  plain data, never source text and never a callback. Evaluation is pure: an expression MUST
5
5
  NOT change state.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Graph model
2
2
 
3
- Axiom 0.13.0-alpha.1. The `ApplicationGraph` is the authoritative representation of an
3
+ Axiom 0.14.0-alpha.1. The `ApplicationGraph` is the authoritative representation of an
4
4
  application. Everything else — the IR, the page, the DOM — is derived from it and is never
5
5
  edited.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Integrations
2
2
 
3
- Axiom 0.13.0-alpha.1. How an application declares and calls an external system, without
3
+ Axiom 0.14.0-alpha.1. How an application declares and calls an external system, without
4
4
  embedding a transport, an SDK or a secret in the graph. The authority boundary this
5
5
  depends on is [`AUTHORITY.md`](AUTHORITY.md#external-systems); this file is the vocabulary.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Realtime — live canonical queries
2
2
 
3
- Axiom 0.13.0-alpha.1. The operational contract for **observing a `QueryDef` result over
3
+ Axiom 0.14.0-alpha.1. The operational contract for **observing a `QueryDef` result over
4
4
  time**: subscribe once, receive an initial coherent result, then receive canonical changes
5
5
  as authoritative committed state moves — through any compatible authority, across
6
6
  reconnects. `axiom.server.v7` (0.13 adds no IR vocabulary).
@@ -111,20 +111,70 @@ the incremental changes is still correct, just chattier.
111
111
 
112
112
  A committed change wakes a live subscription when it may have moved that query's result. The
113
113
  dependency set is **conservative and static** (`queryDependencies`): the source entity,
114
- every entity a used relationship reaches, every entity a read policy governs, and every
115
- `StateDef` a query clause or the effective `ReadPolicy` predicate reads. A `ref` that cannot
116
- be resolved to a state, parameter, principal or iteration scope sets `broad` — that
117
- subscription re-evaluates on **every** commit. False negatives are forbidden: a missed
118
- invalidation is a release blocker; a redundant one only costs a re-evaluation that produces
119
- no client message.
120
-
121
- Local commits wake the engine synchronously. A commit made on **another authority** is
122
- observed through the shared durable revision: an authority serving live queries re-reads
123
- `persistence.revision()` on an interval (`AxiomServerOptions.liveQueryPollMs`, default 250;
124
- `0` disables it) and, on an advance it has not already processed, re-evaluates every live
125
- subscription. There is no broadcast, no pub/sub and no sticky routing. A change written
126
- directly to a provider's store **outside** an Axiom transaction does not advance the durable
127
- revision and is not observed until the next Axiom commit — this is a documented limitation.
114
+ every entity a used relationship reaches, and every entity a read policy governs. A `ref`
115
+ that cannot be resolved to a parameter, principal, relationship bind or iteration scope sets
116
+ `broad` — that subscription re-evaluates on **every** commit. False negatives are forbidden:
117
+ a missed invalidation is a release blocker; a redundant one only costs a re-evaluation that
118
+ produces no client message. (A `QueryDef` clause cannot reference a `StateDef` — see
119
+ [QueryDef expression scope](#querydef-expression-scope) — so there is no `StateDef`
120
+ dependency for a query.)
121
+
122
+ ### The observable application revision
123
+
124
+ Every committed **Axiom** mutation that can change canonical query meaning becomes
125
+ observable to every compatible authority, **regardless of which persistence surface it
126
+ touched** — a `StateDef` write, a `provider-record` write, or both. This is projected from
127
+ two durable sources:
128
+
129
+ | Source | Advanced by | Read with |
130
+ | --- | --- | --- |
131
+ | `stateRevision` | a committed `StateDef` write (`PersistenceAdapter`) | `server.revisionInspection().stateRevision` |
132
+ | `dataGeneration` | a committed `applyMutations` batch, advanced **atomically inside the provider's write transaction** | `server.revisionInspection().dataGeneration` |
133
+
134
+ `server.revisionInspection().applicationRevision` is this authority's local monotone count of
135
+ distinct observed application-meaning changes, projected from both. It is not comparable
136
+ across authorities, and nothing compares it across authorities.
137
+
138
+ Local commits wake the engine synchronously. A commit made on **another authority** — a
139
+ `StateDef` *or* a `provider-record` commit — is observed by the idle-authority poll: an
140
+ authority serving live queries re-reads *both* durable sources on an interval
141
+ (`AxiomServerOptions.liveQueryPollMs`, default 250; `0` disables it) and, on an advance it
142
+ has not already folded in, re-evaluates every live subscription. There is no broadcast, no
143
+ pub/sub and no sticky routing.
144
+
145
+ Because a provider commit and its `dataGeneration` advance are one atomic transaction, a
146
+ writer that crashes the instant after the commit returns leaves the change **and** its
147
+ observable evidence both durable — a newly started authority reads the current
148
+ `dataGeneration` and serves the correct result.
149
+
150
+ **In scope:** every `provider-record` mutation committed through a canonical Axiom
151
+ `ActionDef` (create, update, delete). **Out of scope:** a write made directly to a
152
+ provider's store *outside* an Axiom transaction — it does not advance `dataGeneration` and
153
+ is not observed until the next Axiom commit, unless the provider explicitly advertises
154
+ external-change observation.
155
+
156
+ ### Provider capability
157
+
158
+ `DataProvider.capabilities.mutationObservation` is `'durable'` (the SQLite reference — a
159
+ generation any authority sharing the store can read), `'in-process'` (the memory reference —
160
+ single process only), or `'none'`. `openLiveQuery` for a query whose **writable** provider
161
+ reports `'none'` is refused with `LIVE_QUERY_PROVIDER_NOT_OBSERVABLE` rather than served
162
+ silently stale on a peer. A custom provider implements `observedMutationGeneration(): Promise<number>`
163
+ — a monotone counter advanced atomically with `applyMutations` — to support distributed live
164
+ queries.
165
+
166
+ ## QueryDef expression scope
167
+
168
+ A `QueryDef` clause (`filter`, `sort`, `projection`, `groupBy`, `aggregate`) is evaluated
169
+ **by the `DataProvider`**, whose scope is: `ref(rowScopeId)`, `ref(<parameter id>)`,
170
+ `ref(<relationship bindAs>)`, `PRINCIPAL`, and any nested iteration-scope id introduced
171
+ inside a clause. It is **not** a `StateDef`. A `ref` to a `StateDef` in any query clause —
172
+ or in a `ReadPolicyDef` predicate, which shares this scope — is a validation error
173
+ (`QUERY_STATE_REF_NOT_ALLOWED`): `validateGraph` / `compileToServerIR` reject it,
174
+ `analyzeLiveQuery` reports `not-live-capable` with the offending ref in
175
+ `dependencies.unsupportedStateRefs`, and the runtime guards against a hand-built IR that
176
+ bypasses validation. Bind a runtime-varying value (a threshold, a cutoff) as a **query
177
+ parameter** and pass it from the action or trigger that opens the query.
128
178
 
129
179
  ---
130
180
 
@@ -223,6 +273,8 @@ transport prefers.
223
273
  | Code | When |
224
274
  | --- | --- |
225
275
  | `LIVE_QUERY_NOT_CAPABLE` | `openLiveQuery` for a `QueryDef` that reads `now` / `uuid`. |
276
+ | `QUERY_STATE_REF_NOT_ALLOWED` | A `QueryDef` clause or `ReadPolicy` predicate references a `StateDef` (rejected at compile; this is the runtime guard for a hand-built IR). |
277
+ | `LIVE_QUERY_PROVIDER_NOT_OBSERVABLE` | `openLiveQuery` for a query whose writable provider cannot make committed mutations observable across authorities (`mutationObservation: 'none'`). |
226
278
  | `LIVE_QUERY_CURSOR_INVALID` | A resume cursor that is tampered, unsigned, or minted for a different query / principal / arguments / policy. |
227
279
  | `LIVE_QUERY_CURSOR_INCOMPATIBLE` | A resume cursor minted under an incompatible schema / semantic build. |
228
280
  | `LIVE_QUERY_EVALUATION_FAILED` | Re-evaluating against the `DataProvider` failed; delivered as an `{ kind: 'error' }` message, last result stands. |
package/docs/LOCATIONS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Locations
2
2
 
3
- Axiom 0.13.0-alpha.1.
3
+ Axiom 0.14.0-alpha.1.
4
4
 
5
5
  ```text
6
6
  Expression = a value
@@ -1,6 +1,6 @@
1
1
  # Schema evolution & semantic migrations
2
2
 
3
- Axiom 0.13.0-alpha.1. The operational contract for evolving a deployed application's
3
+ Axiom 0.14.0-alpha.1. The operational contract for evolving a deployed application's
4
4
  semantic model and its persisted canonical data over time — adding a required field,
5
5
  splitting one field into two, removing an obsolete one, migrating millions of
6
6
  provider-backed rows — **without** an application-authored SQL migration, an ORM migration,
@@ -1,6 +1,6 @@
1
1
  # Presentation
2
2
 
3
- Axiom 0.13.0-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
3
+ Axiom 0.14.0-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
4
4
  node. It names roles, tokens and device classes. It never names a colour, a length, a media
5
5
  query or a CSS property.
6
6
 
package/docs/QUERIES.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Semantic data access & the query layer
2
2
 
3
- Axiom 0.13.0-alpha.1. The operational contract for demand-driven reads over authoritative
3
+ Axiom 0.14.0-alpha.1. The operational contract for demand-driven reads over authoritative
4
4
  data that is too large to materialize as a `StateDef` — 500,000 orders, 5,000,000 order
5
5
  lines, years of audit rows. `axiom.server.v6`.
6
6
 
@@ -58,6 +58,12 @@ Operators reuse `Expression` semantics: `binary('eq' | 'neq' | 'lt' | 'lte' | 'g
58
58
  | 'and' | 'or', …)`, `unary('not', …)`, and the `contains` built-in for substring/membership.
59
59
  There is one comparison language, not two.
60
60
 
61
+ **Expression scope.** Every `QueryDef` clause (and a `ReadPolicyDef` predicate) is evaluated
62
+ by the `DataProvider`. In scope: `ref(rowScopeId)`, `ref(<parameter id>)`, `ref(<relationship
63
+ bindAs>)`, `PRINCIPAL`, and nested iteration scopes. **Not** a `StateDef` — the provider
64
+ binds no authority state, so `ref(<state id>)` in a query clause is rejected
65
+ (`QUERY_STATE_REF_NOT_ALLOWED`). Pass a runtime-varying value as a query parameter.
66
+
61
67
  ### Null semantics (frozen)
62
68
 
63
69
  | Situation | Behaviour |
package/docs/RUNTIME.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Runtime
2
2
 
3
- Axiom 0.13.0-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
3
+ Axiom 0.14.0-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
4
4
  contains no knowledge of any application.
5
5
 
6
6
  ## Constructing
@@ -1,6 +1,6 @@
1
1
  # Semantic contract
2
2
 
3
- Axiom 0.13.0-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
3
+ Axiom 0.14.0-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
4
4
  does not teach. Where this file and any specification in `../specs/` disagree, this file
5
5
  describes the implementation and is authoritative.
6
6
 
package/docs/STATE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # State
2
2
 
3
- Axiom 0.13.0-alpha.1. A `StateDef` is a named application value: stored, or computed from
3
+ Axiom 0.14.0-alpha.1. A `StateDef` is a named application value: stored, or computed from
4
4
  other state.
5
5
 
6
6
  ```ts
package/docs/STORAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Storage and blobs
2
2
 
3
- Axiom 0.13.0-alpha.1. How an application stores, references, serves and deletes binary data —
3
+ Axiom 0.14.0-alpha.1. How an application stores, references, serves and deletes binary data —
4
4
  an attachment, a document, a photograph, a diagnostic log — with no filesystem path, no
5
5
  upload route and no download route anywhere in it.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Subscriptions
2
2
 
3
- Axiom 0.13.0-alpha.1. How an application receives a stream of external events — an MQTT
3
+ Axiom 0.14.0-alpha.1. How an application receives a stream of external events — an MQTT
4
4
  topic, a WebSocket feed, a queue consumer, a filesystem watcher, a serial port — without a
5
5
  client, a socket or a callback anywhere in the graph.
6
6
 
package/docs/TRIGGERS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Triggers
2
2
 
3
- Axiom 0.13.0-alpha.1. A `TriggerDef` says **when** an action should be invoked, without
3
+ Axiom 0.14.0-alpha.1. A `TriggerDef` says **when** an action should be invoked, without
4
4
  embedding callback code. `docs/AUTHORITY.md`
5
5
  [§ Triggers](AUTHORITY.md#triggers) is the load-bearing statement of the execution model;
6
6
  this file is the vocabulary.
package/docs/UI.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # UI
2
2
 
3
- Axiom 0.13.0-alpha.1. Eleven semantic UI node kinds describe **what exists and what it does**.
3
+ Axiom 0.14.0-alpha.1. Eleven semantic UI node kinds describe **what exists and what it does**.
4
4
  How it looks is [presentation](PRESENTATION.md).
5
5
 
6
6
  All eleven share `UIBase`:
@@ -1,6 +1,6 @@
1
1
  # Validation
2
2
 
3
- Axiom 0.13.0-alpha.1. Validation is authoring-time structural checking. It is not the same
3
+ Axiom 0.14.0-alpha.1. Validation is authoring-time structural checking. It is not the same
4
4
  as runtime constraint evaluation — see [`CONSTRAINTS.md`](CONSTRAINTS.md) for the four
5
5
  layers of correctness.
6
6
 
@@ -36,7 +36,7 @@ non-numeric collections, and obviously incompatible assignments.
36
36
 
37
37
  ## Codes
38
38
 
39
- 105 codes, exported as `VALIDATION_CODES`. Every one is reachable.
39
+ 121 codes, exported as `VALIDATION_CODES`. Every one is reachable.
40
40
 
41
41
  ### Ids and references
42
42
 
@@ -161,6 +161,7 @@ execute rather than letting it validate and then fail at the provider (spec 0.10
161
161
  | `INVALID_READ_POLICY` | A `ReadPolicyDef` with a non-boolean predicate, a missing entity, or a colliding row scope; also a query whose `readPolicyId` governs a different entity than the query's `source`. |
162
162
  | `DUPLICATE_READ_POLICY` | More than one `ReadPolicyDef` governing the same entity — the effective filter would depend on which the compiler picked. |
163
163
  | `INVALID_QUERY_PREDICATE` | A `QueryDef.filter` that is not a boolean expression. |
164
+ | `QUERY_STATE_REF_NOT_ALLOWED` | A `QueryDef` clause (`filter`, `sort`, `projection`, `groupBy`, `aggregate`) or a `ReadPolicyDef` predicate references a `StateDef`. A query executes on the `DataProvider`, which binds no authority state, so the reference would evaluate to nothing at run time. Bind a runtime-varying value through a query parameter instead. |
164
165
  | `INVALID_QUERY_SORT` | A `QuerySortKey` whose projected key is not an orderable type (`string`, `number`, `boolean`, `date`, `datetime`, `enum`). |
165
166
  | `INVALID_QUERY_PROJECTION` | A projected field that is not on the projection entity, or whose value type is incompatible with the declared field type. |
166
167
  | `INVALID_QUERY_AGGREGATE` | A `sum`/`average` aggregate over a non-numeric key, a `min`/`max` over an unorderable key, a `count` that carries a key, or an aggregate with no result field. |
@@ -190,6 +191,30 @@ connect schema 1 to it. A migration transform reads the old record through the r
190
191
  | `MIGRATION_TRANSFORM_IMPURE` | A migration transform expression that calls `now` or `uuid`, or reads a scope other than the old record (`MIGRATION_OLD_SCOPE`) and the operation's declared constants (spec11 §25, §26). |
191
192
  | `MIGRATION_TRANSFORM_TYPE_MISMATCH` | A `transform-field` whose declared `toType` does not match the field's type in the target schema (spec11 §77). |
192
193
 
194
+ ### Durable workflows (0.14)
195
+
196
+ `validateGraph` rejects an internally inconsistent `WorkflowDef` before it can be compiled
197
+ to `axiom.server.v8` or executed. Every failure is a structured diagnostic — never a thrown
198
+ `TypeError` on a malformed step.
199
+
200
+ | Code | Raised when |
201
+ | --- | --- |
202
+ | `WORKFLOW_ENTRY_NOT_FOUND` | `WorkflowDef.entry` does not name a step of that workflow. |
203
+ | `WORKFLOW_STEP_NOT_FOUND` | A control-flow edge (`next` / `onError` / `then` / `else` / `onTimeout`) or a binding's `producedBy` names a step that does not exist. |
204
+ | `WORKFLOW_DUPLICATE_STEP_ID` | Two steps in one workflow share an id, or a step id collides with a graph node id. |
205
+ | `WORKFLOW_INVALID_STEP` | A step with no id, or a `type` outside the six (`action`, `wait-event`, `timer`, `branch`, `complete`, `fail`). |
206
+ | `WORKFLOW_CYCLE_NOT_ALLOWED` | The workflow control-flow graph contains a cycle. Retries are runtime policy, not graph edges. |
207
+ | `WORKFLOW_ACTION_NOT_FOUND` | An `action` step references an `action` node that does not exist. |
208
+ | `WORKFLOW_EVENT_NOT_FOUND` | A `wait-event` step references an `event` node that does not exist. |
209
+ | `WORKFLOW_BINDING_NOT_FOUND` | A `bind` entry, or a `ref`, names a `WorkflowBinding` the workflow does not declare. |
210
+ | `WORKFLOW_DUPLICATE_BINDING` | A binding is assigned by more than one step, or a `bind` targets a binding whose declared `producedBy` is a different step. |
211
+ | `WORKFLOW_INVALID_RETRY_POLICY` | A `WorkflowRetryPolicy` with a non-positive `maxAttempts` / `initialDelaySeconds`, a `backoffMultiplier` below 1, or a `maxDelaySeconds` below `initialDelaySeconds`. |
212
+ | `WORKFLOW_INVALID_TIMER` | A `timer` step with neither `after` nor `at`, with both, with a non-positive `after.seconds`, or a `wait-event` timeout that is non-positive. |
213
+ | `WORKFLOW_UNREACHABLE_STEP` | A step not reachable from `entry` by control flow (an authoring mistake, so an error, not a warning). |
214
+ | `WORKFLOW_NO_TERMINAL` | A reachable step from which no control-flow path can reach `complete` or `fail` (and which is not an intentional unbounded `wait-event`). |
215
+ | `WORKFLOW_EXPRESSION_SCOPE` | A workflow expression references an id outside the workflow expression scope: `ref(<input id>)`, `ref(<binding id>)`, `ref('EVENT')` (only inside a `wait-event` `where` / `bind`), `ref('PRINCIPAL')`. Not a `StateDef`, not a `QueryDef`. |
216
+ | `WORKFLOW_NONDETERMINISTIC` | A workflow expression calls `now` / `uuid` / `random`. Workflow decisions must be deterministic and replayable. |
217
+
193
218
  ### UI and routing
194
219
 
195
220
  | Code | Raised when | Severity |
@@ -0,0 +1,282 @@
1
+ # Durable workflows
2
+
3
+ Axiom 0.14.0-alpha.1. The operational contract for **long-running semantic computations with
4
+ a durable control position** — orchestration that survives process death, authority
5
+ failover, retries, timer delivery, event delivery and ordinary distributed contention
6
+ without application-owned infrastructure. `axiom.server.v8`.
7
+
8
+ > The workflow graph owns durable orchestration meaning. The runtime owns scheduling,
9
+ > persistence, retries, leases, fencing, crash recovery and physical execution.
10
+
11
+ A durable workflow is **not** a background promise, a persisted callback, a job-queue entry,
12
+ a cron task, a mutable JSON state blob, a process-local event listener, or a bag of retries.
13
+ It is a `WorkflowDef` node: a closed step vocabulary and `Expression` trees over a closed
14
+ scope. There is no application script body of any kind.
15
+
16
+ ---
17
+
18
+ ## `WorkflowDef`
19
+
20
+ ```ts
21
+ interface WorkflowDef extends NodeBase {
22
+ kind: 'workflow';
23
+ inputs?: WorkflowInput[]; // { id, valueType: TypeRef, required?: boolean } — immutable after start
24
+ bindings?: WorkflowBinding[]; // { id, valueType, producedBy: <step id> } — single-assignment, read-only elsewhere
25
+ entry: NodeId; // the first step
26
+ steps: WorkflowStep[];
27
+ }
28
+ ```
29
+
30
+ Inputs use canonical portable value types only — no host objects, closures, file
31
+ descriptors, sockets or class instances. Bindings replace a mutable workflow blob: each is
32
+ assigned **once** by its producing step and only read afterwards, which is what keeps replay
33
+ safe, static analysis strong and the model portable.
34
+
35
+ ### Step vocabulary — the six of 0.14
36
+
37
+ | `type` | Fields | Meaning |
38
+ | --- | --- | --- |
39
+ | `action` | `action: NodeId`, `arguments?`, `next`, `onError?`, `retry?` | Invoke a canonical `ActionDef` under the workflow principal. |
40
+ | `wait-event` | `event: NodeId`, `where?` (bool), `bind?` (→ binding ids), `next`, `timeout?`, `onTimeout?` | Wait for a matching canonical `EventDef` occurrence. |
41
+ | `timer` | exactly one of `after: { seconds }` / `at: Expression`, `next` | Wait until a durable time. |
42
+ | `branch` | `when` (bool), `then`, `else` | Deterministic edge choice over durable workflow context. |
43
+ | `complete` | `output?` | Terminal → `completed`. |
44
+ | `fail` | `error?` | Terminal → `failed`. |
45
+
46
+ Deferred (a later release): `parallel`, `race`, `map`, `foreach`, child workflows,
47
+ `wait-query`, human tasks, compensation / sagas, loops. The graph **should be acyclic**;
48
+ `validateGraph` rejects a control-flow cycle (`WORKFLOW_CYCLE_NOT_ALLOWED`). Retries are
49
+ runtime policy and are not graph edges.
50
+
51
+ ### Workflow expression scope
52
+
53
+ Every workflow expression (an action argument, `wait-event.where` / `bind`, `branch.when`,
54
+ `timer.at`, `complete.output`, `fail.error`) resolves against a **closed** scope:
55
+
56
+ - `ref(<input id>)` — an immutable start input;
57
+ - `ref(<binding id>)` — a durable single-assignment binding;
58
+ - `ref('EVENT')` — the matched event payload, **only** inside a `wait-event` step's `where`
59
+ / `bind`;
60
+ - `ref('PRINCIPAL')` — the bound workflow principal.
61
+
62
+ **Not** a `StateDef` (`WORKFLOW_EXPRESSION_SCOPE`) — model dynamic application state as a
63
+ canonical `ActionDef` *before* the branch. **Not** a `QueryDef` — a branch never hides a
64
+ network/storage read. **Not** `now` / `uuid` / `random` (`WORKFLOW_NONDETERMINISTIC`) —
65
+ workflow decisions must be replayable; time enters only through a `timer` step's
66
+ captured-once target instant.
67
+
68
+ ---
69
+
70
+ ## Starting a workflow
71
+
72
+ ```ts
73
+ const started = await server.startWorkflow({
74
+ workflowId, arguments, credential, idempotencyKey,
75
+ });
76
+ // { instanceId, status } | { error: { code, message } }
77
+ ```
78
+
79
+ **Idempotent start.** `WorkflowStartIdentity = { workflowId, principalFingerprint,
80
+ idempotencyKey, compatibilityFingerprint }`. Two `startWorkflow` calls with the same
81
+ `(workflowId, principal, idempotencyKey)` return the **same** `instanceId` — a retry after a
82
+ lost response is one logical instance, not two. Two different callers reusing the same
83
+ textual key under different principals never collide. Omitting `idempotencyKey` starts a
84
+ fresh instance every call.
85
+
86
+ **Principal.** The canonical principal the workflow was started under is bound to the
87
+ instance and every workflow-driven `ActionDef` executes as it — no authority-local service
88
+ principal, no privilege escalation. Capturing the principal does not freeze authorization:
89
+ each later action step re-evaluates current canonical authorization under that principal, and
90
+ a step whose action is no longer authorized **fails semantically** (it follows `onError` if
91
+ declared, else the workflow fails).
92
+
93
+ ---
94
+
95
+ ## Logical identity and exactly-once
96
+
97
+ - **`instanceRevision`** — a monotone durable integer. Every logical transition is a fenced
98
+ compare-and-swap: `expected instanceRevision R + fence F → R+1`, atomically, with the
99
+ check *inside* the write transaction. This linearizes every race — event vs timeout,
100
+ cancel vs completion, two authorities on one step, stale owner vs new owner.
101
+ - **`activationId`** — identifies *this arrival at this step*, distinct from the step id so a
102
+ future loop feature can revisit a step without changing instance identity. For 0.14
103
+ (acyclic) it is `"<stepId>#0"`.
104
+ - **Action invocation identity** = `"<instanceId>/<activationId>"`. The workflow action step
105
+ invokes the `ActionDef` through the ordinary authority path with this as the idempotency
106
+ key. The authority commits a **durable idempotency record** in the *same* persistence
107
+ transaction as the ActionDef's state writes, so a crash between "action committed" and
108
+ "workflow transition recorded" — including a **full process restart** with no in-memory
109
+ state — is reconciled: a recovery authority proves the invocation already committed and
110
+ recovers its canonical outcome (`PersistenceAdapter.loadIdempotentResponse`) rather than
111
+ executing the action a second time. The record can never be present without the matching
112
+ durable state, and vice versa.
113
+ - **Exactly-once logical transition** for each activated step is guaranteed.
114
+ **Exactly-once physical execution of an external effect is not** — that remains governed
115
+ by the effect system. The distinction is deliberate and load-bearing: an effect an action
116
+ dispatched from a workflow keeps a stable logical effect identity across workflow retries.
117
+
118
+ ---
119
+
120
+ ## Event waits — no gap
121
+
122
+ When a workflow durably enters a `wait-event` step, the transition record **includes** the
123
+ durable wait registration (`eventId`, correlation, `sinceEventSeq`, `activationId`),
124
+ committed in the **same** transaction — there is no "state = waiting, then subscribe"
125
+ window. Matching is driven by Axiom's single inbound event pipeline (the same one webhooks
126
+ and effect outcomes use): every accepted event is offered to matching waits whose `where`
127
+ predicate the payload satisfies. Every accepted event is first appended to a **durable
128
+ `WorkflowStore` journal** with a store-global monotone `seq`; `sinceEventSeq` is captured
129
+ from that journal's high-water mark. On startup / failover — and on every poll tick — every
130
+ event-waiting instance is rediscovered and the journal is replayed against it from
131
+ `sinceEventSeq`, so a matching event **survives the death of the authority that routed it**:
132
+ another compatible authority replays it from the shared journal with no client resend, no
133
+ manual replay, no sticky routing and no application polling. Existing external-event dedup
134
+ is unchanged and runs upstream, so a redelivered physical event transitions a wait **at most
135
+ once**; replaying the same journalled event any number of times yields **one** logical
136
+ transition. A matching event unblocks *every* independently-matching waiting instance
137
+ (fanout); a wait never globally consumes an event. An event that committed strictly before a
138
+ wait became live is deterministically **not** matched (it is in the past); an event
139
+ concurrent with wait activation is matched exactly once — no event is lost in the handoff.
140
+
141
+ `bind: { <bindingId>: Expression }` assigns declared bindings from `ref('EVENT')` when the
142
+ event matches and the transition commits. A `wait-event` step may declare a `timeout` and an
143
+ `onTimeout` edge; if the event and the timeout are concurrent, **exactly one** logical
144
+ transition wins (`instanceRevision` decides), never both.
145
+
146
+ ---
147
+
148
+ ## Timers
149
+
150
+ `timer` waits until a durable time. On activation the **target instant is computed once**
151
+ (`activationInstant + after.seconds`, or the resolved `at`) and stored in the transition
152
+ record — a restart does not recompute `now + after` and extend the wait. Scheduler firing
153
+ may be physically at-least-once; the workflow transition it causes is logically exactly-once.
154
+ A firing after the target time is still valid — the guarantee is *not before* the target,
155
+ *eventually after*, subject to scheduler availability. 0.14 does not promise hard real-time.
156
+ Recovery needs nothing special: the waiting row *is* the timer.
157
+
158
+ ---
159
+
160
+ ## Retries
161
+
162
+ An `action` step may declare `retry: { maxAttempts, initialDelaySeconds, backoffMultiplier,
163
+ maxDelaySeconds }`. Automatic retries apply only to **runtime-classified retryable**
164
+ failures (a transient infrastructure condition, a lease interruption before logical commit)
165
+ — never to a semantic business refusal. `retryable` vs terminal is a structured distinction;
166
+ workflow logic never parses an error-message string. Each physical attempt has its own
167
+ attempt number; the logical action identity is constant across attempts. The attempt count
168
+ and the next eligible execution time are **durable** — authority death does not reset
169
+ `attempt = 4` back to `1`. Lease/fencing ensures one current executor, so multiple
170
+ authorities never retry the same step as independent logical owners.
171
+
172
+ ---
173
+
174
+ ## Cancellation
175
+
176
+ ```ts
177
+ await server.cancelWorkflow(instanceId, credential); // { ok: true, status } | { error }
178
+ ```
179
+
180
+ Idempotent. Cancellation is a fenced durable transition to `cancelled`. It means **do not
181
+ continue executing future workflow steps** — it is **not** rollback: it does not undo
182
+ already-committed actions, it does not reverse dispatched external effects, and it is not a
183
+ distributed transaction. 0.14 has no automatic compensation. A later timer or event delivery
184
+ for a `cancelled` (or `completed` / `failed`) instance does not transition it. A
185
+ cancel-versus-transition race linearizes on `instanceRevision`: whichever fenced CAS commits
186
+ first wins.
187
+
188
+ Terminal statuses — `completed`, `failed`, `cancelled` — are durable and irreversible under
189
+ normal execution. A stale authority cannot resurrect a terminal workflow.
190
+
191
+ ---
192
+
193
+ ## Crash recovery and multi-authority behaviour
194
+
195
+ Workflow execution is **leaderless** — any compatible authority may advance any eligible
196
+ instance; there is no workflow leader, primary node or master process. A short per-instance
197
+ lease + fence (reused from the 0.12 `CoordinationProvider`) gates execution ownership; every
198
+ durable mutation under ownership is fenced, so a resumed stale owner is refused.
199
+
200
+ After startup an authority discovers instances that are runnable / retry-due / timer-due /
201
+ recoverably waiting and advances them — the application does **not** scan stuck workflows,
202
+ call `resumeWorkflow`, re-register timers or re-register event waits for normal supported
203
+ recovery. Discovery is bounded and indexed (`status, next_eligible_at`).
204
+
205
+ **Topology transparency:** the logical workflow result of a valid `WorkflowDef` is identical
206
+ whether one authority or N run it, for identical committed semantic history. A process
207
+ crash, authority crash, retry, lease expiry or request-routing change does not change the
208
+ logical outcome.
209
+
210
+ ---
211
+
212
+ ## Compatibility
213
+
214
+ A running instance stores the `AuthorityCompatibilityKey` (`{ schemaVersion,
215
+ schemaFingerprint, serverContract, semanticFingerprint }`) at start. An authority whose key
216
+ differs **refuses to advance it**, fail-closed — the same mechanism the distributed work
217
+ store applies. A presentation-only graph change moves no fingerprint and stays compatible. A
218
+ semantic workflow change makes new instances incompatible with old authorities; old
219
+ instances are not auto-migrated — they run to completion on a compatible authority or are
220
+ surfaced as stranded. A graph with **no** `WorkflowDef` compiles to the byte-identical
221
+ `axiom.server.v1`–`v7` document it always did, and its `semanticFingerprint` is unchanged.
222
+
223
+ ---
224
+
225
+ ## Inspection
226
+
227
+ `server.getWorkflow(instanceId)` / `server.inspectWorkflows(limit)` return the semantic
228
+ fields — `status`, `currentStepId`, `activationId`, `attempt`, `waitingReason`,
229
+ `nextEligibleAt`, `createdAt`, `updatedAt`, `instanceRevision`, `failure`, `output` — and
230
+ **no** secrets (no HMAC keys, database paths, raw SQL, credentials).
231
+ `server.workflowHistory(instanceId)` is the durable semantic transition log (`started`,
232
+ `step-activated`, `step-succeeded`, `step-failed`, `retry-scheduled`, `event-matched`,
233
+ `timer-fired`, `timeout-fired`, `branch-chosen`, `completed`, `failed`, `cancelled`) — a
234
+ bounded record, not an event-sourcing requirement.
235
+
236
+ `AgentAPI.analyzeWorkflow(workflowId)` is the static, graph-derived view: inputs, entry,
237
+ every step and its edges, the `ActionDef` / `EventDef` dependency sets, timer waits, branch
238
+ conditions, retry policies, the reachable terminal outcomes, acyclicity, and the kinds of
239
+ `waitingReason` an instance can produce.
240
+
241
+ ---
242
+
243
+ ## `WorkflowStore`
244
+
245
+ A provider-independent durable persistence abstraction: `createIdempotent`, `load`,
246
+ `loadByStart`, `transition` (fenced CAS), `recordActionOutcome` / `loadActionOutcome`,
247
+ `recoverRunnable`, `findEventWaits`, `history`, `list`. `createMemoryWorkflowStore()` is the
248
+ single-process reference; `createSqliteWorkflowStore({ location })` is the real cross-process
249
+ reference — independent OS-process connections, `BEGIN IMMEDIATE` with the revision + fence
250
+ check *inside* the transaction, `busy_timeout` + bounded retry so `SQLITE_BUSY` never
251
+ surfaces as application semantics, and `CREATE TABLE IF NOT EXISTS` + `INSERT OR IGNORE`
252
+ init so concurrent startup is safe. No SQLite path / table / rowid / WAL position appears in
253
+ the contract or the graph.
254
+
255
+ ---
256
+
257
+ ## Portability
258
+
259
+ `axiom.conformance.v8` (`conformance/workflow/`) is the portable tier. A fixture is a
260
+ compiled `axiom.server.v8` Server IR + start arguments + a deterministic driver script
261
+ (advance the virtual clock, deliver an event, mark an action outcome) + the **required
262
+ ordered logical transition history** and terminal state. `runWorkflowConformanceFixture` /
263
+ `runWorkflowConformanceSuite` run it over the in-memory store; physical attempts may be
264
+ duplicated, the logical history must match exactly. A future independent runtime implements
265
+ the fixtures from the contract alone.
266
+
267
+ ---
268
+
269
+ ## Limitations
270
+
271
+ - The deferred step kinds above are not available.
272
+ - 0.14 has no `wait-query` (a workflow does not wait on a `QueryDef` result).
273
+ - Cancellation cannot interrupt an already-dispatched physical external effect; the
274
+ framework stays honest about uncertain physical execution.
275
+ - **Physical** effect execution remains at-least-once (the effect system's contract); only
276
+ the *logical* ActionDef invocation and each workflow step transition are exactly-once.
277
+ - The durable event journal and the durable idempotency table are **bounded** buffers
278
+ (default 8192 journal entries / an idempotency window per authority config). They cover
279
+ crash and failover windows, not indefinite history; a wait parked longer than the journal
280
+ is retained past its `sinceEventSeq` is an implementation-owned retention concern.
281
+ - `WorkflowStore` bounded retention of terminal instances and history is
282
+ implementation-owned; an active workflow never expires.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Axiom
2
2
 
3
- > AI-native semantic application framework, version 0.13.0-alpha.1. An Axiom application is a
3
+ > AI-native semantic application framework, version 0.14.0-alpha.1. An Axiom application is a
4
4
  > typed semantic graph — state, behavior, constraints, UI structure, presentation and
5
5
  > authority as structured data — executed by generic runtimes. The JavaScript, HTML and CSS
6
6
  > that reach a browser are compiler output and are never authored or edited. The primary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom",
3
- "version": "0.13.0-alpha.1",
3
+ "version": "0.14.0-alpha.1",
4
4
  "description": "AI-native semantic web application framework.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",
@@ -34,10 +34,10 @@
34
34
  }
35
35
  },
36
36
  "dependencies": {
37
- "@cynodia/axiom-core": "0.13.0-alpha.1",
38
- "@cynodia/axiom-runtime": "0.13.0-alpha.1",
39
- "@cynodia/axiom-compiler": "0.13.0-alpha.1",
40
- "@cynodia/axiom-agent-api": "0.13.0-alpha.1"
37
+ "@cynodia/axiom-core": "0.14.0-alpha.1",
38
+ "@cynodia/axiom-runtime": "0.14.0-alpha.1",
39
+ "@cynodia/axiom-compiler": "0.14.0-alpha.1",
40
+ "@cynodia/axiom-agent-api": "0.14.0-alpha.1"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsc -b tsconfig.json"