@cynodia/axiom 0.13.1-alpha.1 → 0.14.0-alpha.2

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.1-alpha.1`.
36
+ documentation in `docs/` describes this exact version, `0.14.0-alpha.2`.
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.1-alpha.1`.
50
+ `npm install @cynodia/axiom@0.14.0-alpha.2`.
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.1-alpha.1. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.14.0-alpha.2. 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)
@@ -1119,6 +1120,104 @@ Diagnostics: `LIVE_QUERY_NOT_CAPABLE` `LIVE_QUERY_CURSOR_INVALID`
1119
1120
  `LIVE_QUERY_CURSOR_INCOMPATIBLE` `LIVE_QUERY_EVALUATION_FAILED`
1120
1121
  `LIVE_QUERY_PROVIDER_NOT_OBSERVABLE` `QUERY_STATE_REF_NOT_ALLOWED`.
1121
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.
1185
+
1186
+ Compatibility (safety boundary, not deployment metadata): an instance durably records the
1187
+ `AuthorityCompatibilityKey` at creation. `semanticFingerprint` covers `WorkflowDef`
1188
+ executable meaning — **changing a step's `action` / `event` target, argument or `where`
1189
+ expression, `retry` policy, `timer` duration, `branch` predicate, `complete`/`fail` output,
1190
+ or any control-flow edge (`next` / `then` / `else` / `onError` / `onTimeout` / `entry`), or
1191
+ the body of a referenced `ActionDef` / `EventDef`, makes existing in-flight instances
1192
+ incompatible with the new build.** An incompatible authority fails closed *before* any
1193
+ semantic step — no transition, no `ActionDef` invoke, no event/timer/branch, no
1194
+ `instanceRevision` advance — and leaves the instance for a compatible authority; it is never
1195
+ auto-failed or auto-cancelled, and `cancelWorkflow` from an incompatible build is refused.
1196
+ Presentation-only changes (`name` / `description` / `label`) and step declaration order are
1197
+ **not** semantic. A semantically identical fresh process recovers instances normally. There
1198
+ is no workflow instance migration in 0.14. Structurally invalid workflow IR is refused at
1199
+ `createAxiomServer` (`WorkflowIRError`); a malformed step reaches `validateGraph` as a
1200
+ `WORKFLOW_INVALID_STEP` diagnostic, never a native error.
1201
+
1202
+ Inspection: `server.getWorkflow(instanceId)` / `inspectWorkflows(limit)` — `status`,
1203
+ `currentStepId`, `activationId`, `attempt`, `waitingReason`, `nextEligibleAt`,
1204
+ `instanceRevision`, `failure`, `output`, `compatible` /
1205
+ `incompatibleReason: 'incompatible-build'` (no secrets). `server.workflowHistory(instanceId)`
1206
+ — the durable transition log. `AgentAPI.analyzeWorkflow(workflowId)` — static: inputs, steps
1207
+ + edges, action/event dependencies, terminal outcomes, acyclicity, possible wait reasons.
1208
+
1209
+ `WorkflowStore`: `createMemoryWorkflowStore()` (single process), `createSqliteWorkflowStore({
1210
+ location })` (cross-process — `BEGIN IMMEDIATE`, check inside the transaction, `busy_timeout`,
1211
+ `CREATE TABLE IF NOT EXISTS` + `INSERT OR IGNORE` init). Portable tier
1212
+ `axiom.conformance.v8` (`conformance/workflow/`), `runWorkflowConformanceFixture` / `Suite`.
1213
+
1214
+ Diagnostics (validation): `WORKFLOW_ENTRY_NOT_FOUND` `WORKFLOW_STEP_NOT_FOUND`
1215
+ `WORKFLOW_DUPLICATE_STEP_ID` `WORKFLOW_INVALID_STEP` `WORKFLOW_CYCLE_NOT_ALLOWED`
1216
+ `WORKFLOW_ACTION_NOT_FOUND` `WORKFLOW_EVENT_NOT_FOUND` `WORKFLOW_BINDING_NOT_FOUND`
1217
+ `WORKFLOW_DUPLICATE_BINDING` `WORKFLOW_INVALID_RETRY_POLICY` `WORKFLOW_INVALID_TIMER`
1218
+ `WORKFLOW_UNREACHABLE_STEP` `WORKFLOW_NO_TERMINAL` `WORKFLOW_EXPRESSION_SCOPE`
1219
+ `WORKFLOW_NONDETERMINISTIC`.
1220
+
1122
1221
  ## Metadata classes
1123
1222
 
1124
1223
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.13.1-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.14.0-alpha.2. 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
@@ -965,3 +965,132 @@ return an empty (or wrong) result. Before 0.13.1, `validateGraph` accepted this,
965
965
  layer rejects it consistently. Bind the threshold as a **query parameter** and pass it from
966
966
  the action, trigger or client that runs the query. (A `ReadPolicyDef` predicate shares the
967
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.
1049
+
1050
+ ## 78. Treating matching ids, or a matching Server IR version, as workflow compatibility
1051
+
1052
+ ```ts
1053
+ // WRONG — every one of these can be true while the executable meaning has changed.
1054
+ if (a.workflowId === b.workflowId && sameStepIds(a, b)) continueUnderNewBuild();
1055
+ if (a.contract === 'axiom.server.v8' && b.contract === 'axiom.server.v8') safe();
1056
+ if (a.packageVersion === b.packageVersion) safe();
1057
+ ```
1058
+
1059
+ Why wrong: Phase 22 proved a workflow's meaning can change while `workflowId`, every step id
1060
+ and the active step id stay identical — a different `action` / `event` target, a different
1061
+ `branch` predicate, a different `timer` duration, a rewired `next` edge. IR-version
1062
+ compatibility only means both runtimes understand the *vocabulary*; it says nothing about
1063
+ whether two graphs contain equivalent executable semantics, and neither does the package
1064
+ version string. The only authority is the executable `semanticFingerprint`, which now covers
1065
+ `WorkflowDef`. Keep distinct: IR protocol compatibility, graph semantic compatibility,
1066
+ workflow instance compatibility.
1067
+
1068
+ ## 79. Continuing an in-flight workflow under changed executable semantics
1069
+
1070
+ ```ts
1071
+ // WRONG — reinterpreting durable meaning under a new definition.
1072
+ const inst = load(id); // created under build A
1073
+ replayHistory(inst, buildB.workflowDef); // "catch up" under B
1074
+ advance(inst, buildB.workflowDef); // B chooses B's branch / event / timer
1075
+ ```
1076
+
1077
+ Why wrong: a durable workflow instance is bound to the semantics it was created and advanced
1078
+ under. An authority whose graph changes the executable meaning must **fail closed** — leave
1079
+ the instance untouched for a compatible authority — not replay its history under the new
1080
+ definition, not pick the new branch, not match the new event, not recompute the new timer,
1081
+ and not "find the closest step" for a renamed one. There is no workflow instance migration
1082
+ in 0.14. This is framework-owned: the application does not route by build, keep a
1083
+ compatibility registry, or migrate instances by hand.
1084
+
1085
+ ## 80. Hashing workflow presentation metadata to make mixed-build checks stricter
1086
+
1087
+ ```ts
1088
+ // WRONG — an over-tight fingerprint breaks rolling deploys for no safety gain.
1089
+ const key = sha256(JSON.stringify(workflowDef)); // includes name / description / step order
1090
+ ```
1091
+
1092
+ Why wrong: a compatibility check that fires on a `description` edit or a reordered (but
1093
+ edge-identical) step list is a defect too — it strands instances that a semantically
1094
+ identical authority could safely resume. `semanticFingerprint` deliberately strips
1095
+ `name` / `description` / `label` and is independent of step declaration order. Fingerprint
1096
+ *meaning*, not bytes.
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.13.1-alpha.1. How an application crosses the trust boundary.
3
+ Axiom 0.14.0-alpha.2. 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
  | --- | --- |
@@ -689,8 +690,9 @@ page plus the conformance fixtures.
689
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 |
690
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 |
691
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 |
692
694
 
693
- `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
694
696
  tested against — `packages/demo/test/documentation.test.ts` fails if a contract in
695
697
  `SERVER_IR_CONTRACTS` has no row here, or a row here names a contract the code does not
696
698
  declare (spec 8.2 §7-8). The rules:
@@ -1,6 +1,6 @@
1
1
  # Constraints
2
2
 
3
- Axiom 0.13.1-alpha.1. Two constructs, answering different questions. They are not
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1`.*
3
+ *This document describes Axiom `0.14.0-alpha.2`.*
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
@@ -294,19 +294,31 @@ An authority's **compatibility key** is four fields:
294
294
  - `schemaVersion` / `schemaFingerprint` — the 0.11 persistence-relevant identity. A schema
295
295
  mismatch is already fatal per 0.11 migration safety.
296
296
  - `serverContract` — the Server IR contract the document declares.
297
- - `semanticFingerprint` — a **new**, versioned, deterministic hash over the *executable
298
- server-side meaning*: action bodies, guards and operations, integration operation
299
- definitions, triggers, events, subscription policy, read-policy predicates, query
300
- semantics, expression definitions, constraints. It **excludes** everything a rename
301
- touches — names, descriptions, labels, free-form metadata, all UI / routes / themes /
302
- presentation, and declaration order. It is distinct from `schemaFingerprint`, which
303
- deliberately excludes executable meaning: two graphs whose actions do entirely different
304
- things but store the same shapes have the same `schemaFingerprint` and different
297
+ - `semanticFingerprint` — a versioned, deterministic hash over the *executable server-side
298
+ meaning* of **every** executable graph kind (`core`'s single `EXECUTABLE_KINDS` list):
299
+ action bodies, guards and operations, integration operation definitions, triggers, events,
300
+ subscription policy, read-policy predicates, query semantics, expression definitions,
301
+ constraints, storage authorization, relationships, **and `WorkflowDef` executable meaning**
302
+ — a workflow's inputs / bindings / entry, and every step's kind, control-flow edges and
303
+ step-specific semantics (the `ActionDef` / `EventDef` it targets and, transitively, those
304
+ bodies; argument / `where` / `bind` / `when` / output expressions; `retry` policy; `timer`
305
+ duration). The authority-side projection derives from the same `EXECUTABLE_KINDS` list as
306
+ the graph-level `semanticFingerprint`, so a future graph primitive cannot alter executable
307
+ meaning yet escape authority compatibility. It **excludes** everything a rename touches —
308
+ names, descriptions, labels, free-form metadata, all UI / routes / themes / presentation,
309
+ and declaration order (workflow steps included). It is distinct from `schemaFingerprint`,
310
+ which deliberately excludes executable meaning: two graphs whose actions do entirely
311
+ different things but store the same shapes have the same `schemaFingerprint` and different
305
312
  `semanticFingerprint`s.
306
313
 
307
- Durable work records the compatibility key of the build that created it. An authority whose
308
- key differs **refuses to claim** that work (`INCOMPATIBLE_AUTHORITY` / the item is simply
309
- not claimed and stays visible as incompatible). A compatible authority runs it. During a
314
+ Durable work — and every **durable workflow instance** — records the compatibility key of
315
+ the build that created it. An authority whose key differs **refuses to claim** that work
316
+ (`INCOMPATIBLE_AUTHORITY` / the item is simply not claimed and stays visible as
317
+ incompatible). For a workflow instance the refusal is checked before *any* semantic step
318
+ (action invocation, event match, timer fire, branch, retry, terminal transition, or a
319
+ cancellation that would transition it); the instance is left untouched for a compatible
320
+ authority to resume — never auto-failed or auto-cancelled. A compatible authority — topology
321
+ and process identity aside — runs it. During a
310
322
  schema migration, incompatible workers stop claiming new work, ordinary serving is refused
311
323
  per 0.11, the migration completes, and compatible new authorities resume — migration
312
324
  ownership stays host-controlled and separate from ordinary distributed-work ownership; there
package/docs/EFFECTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Effects
2
2
 
3
- Axiom 0.13.1-alpha.1. External effects are not rollback-capable state mutations. This file
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. An event is a typed fact — something that happened — never work
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. An expression describes **what value is computed**. It is a tree of
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. The `ApplicationGraph` is the authoritative representation of an
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. How an application declares and calls an external system, without
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. The operational contract for **observing a `QueryDef` result over
3
+ Axiom 0.14.0-alpha.2. 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).
package/docs/LOCATIONS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Locations
2
2
 
3
- Axiom 0.13.1-alpha.1.
3
+ Axiom 0.14.0-alpha.2.
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.1-alpha.1. The operational contract for evolving a deployed application's
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. The operational contract for demand-driven reads over authoritative
3
+ Axiom 0.14.0-alpha.2. 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
 
package/docs/RUNTIME.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Runtime
2
2
 
3
- Axiom 0.13.1-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. A `StateDef` is a named application value: stored, or computed from
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. How an application stores, references, serves and deletes binary data —
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. How an application receives a stream of external events — an MQTT
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. A `TriggerDef` says **when** an action should be invoked, without
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. Eleven semantic UI node kinds describe **what exists and what it does**.
3
+ Axiom 0.14.0-alpha.2. 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.1-alpha.1. Validation is authoring-time structural checking. It is not the same
3
+ Axiom 0.14.0-alpha.2. 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
- 106 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
 
@@ -191,6 +191,30 @@ connect schema 1 to it. A migration transform reads the old record through the r
191
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). |
192
192
  | `MIGRATION_TRANSFORM_TYPE_MISMATCH` | A `transform-field` whose declared `toType` does not match the field's type in the target schema (spec11 §77). |
193
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, a non-object step (`null`, a string, an array), or a `type` outside the six (`action`, `wait-event`, `timer`, `branch`, `complete`, `fail`). Any malformed step shape produces this diagnostic — never a native `TypeError` (spec14pt3 F1). |
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
+
194
218
  ### UI and routing
195
219
 
196
220
  | Code | Raised when | Severity |
@@ -0,0 +1,321 @@
1
+ # Durable workflows
2
+
3
+ Axiom 0.14.0-alpha.2. 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
+ > Durable workflow instances are bound to **executable semantic compatibility**. An
215
+ > authority whose graph changes the executable meaning of the workflow must fail closed
216
+ > rather than continue the instance under changed semantics.
217
+
218
+ A running instance durably stores the `AuthorityCompatibilityKey` (`{ schemaVersion,
219
+ schemaFingerprint, serverContract, semanticFingerprint }`) at creation, in the same
220
+ transaction that creates it. Before **any** semantic step — action invocation, event match,
221
+ timer fire, branch evaluation, retry, `complete` / `fail`, or a cancellation that would
222
+ transition the instance — an authority checks that key against its own. If it differs the
223
+ authority **refuses**: no `instanceRevision` advance, no `ActionDef` invocation, no event or
224
+ timer transition, no binding write. The instance is left exactly as it stands for a
225
+ compatible authority to resume; incompatibility is an execution-environment condition, never
226
+ an automatic `failed` or `cancelled`.
227
+
228
+ `semanticFingerprint` covers `WorkflowDef` executable meaning: `inputs`, `bindings`, `entry`,
229
+ every step's kind and control-flow edges, and step-specific semantics — the `ActionDef` /
230
+ `EventDef` a step targets (and, transitively, those definitions' own bodies), an `action`
231
+ step's argument expressions / `retry` policy, a `wait-event` step's `where` / `bind` /
232
+ `timeout`, a `timer` step's `after` / `at`, a `branch` step's `when` and edges, and
233
+ `complete` / `fail` output/error expressions. It is computed from `core`'s single
234
+ `EXECUTABLE_KINDS` list, so it and the authority-compatibility fingerprint cannot disagree.
235
+
236
+ - **Semantically incompatible workflow definitions fail closed.** Changing any of the above
237
+ while an instance is in flight strands that instance on incompatible authorities.
238
+ - **Presentation-only changes stay compatible.** `name`, `description`, `label` — anywhere
239
+ in the workflow — move no fingerprint. Step *declaration order* is not semantic either
240
+ (control flow is by explicit edges).
241
+ - **Authority / process identity is irrelevant.** A fresh process running a semantically
242
+ identical build recovers the instance normally; topology and authority count may change
243
+ freely.
244
+ - **Workflow migration across incompatible definitions is not provided in 0.14.** An old
245
+ instance waits for a compatible authority; there is no instance upgrader, step remapping
246
+ or binding migration, and none is inferred ("closest step" recovery never happens).
247
+ - A graph with **no** `WorkflowDef` compiles to the byte-identical `axiom.server.v1`–`v7`
248
+ document it always did, and its `semanticFingerprint` / `schemaFingerprint` are unchanged.
249
+ - Pre-`0.14.0-alpha.2` instances carry a compatibility key computed before `WorkflowDef`
250
+ participated; a corrected authority treats them as incompatible and fails closed (these
251
+ are pre-freeze alpha releases — silent reinterpretation is the only unacceptable
252
+ outcome).
253
+
254
+ ---
255
+
256
+ ## Inspection
257
+
258
+ `server.getWorkflow(instanceId)` / `server.inspectWorkflows(limit)` return the semantic
259
+ fields — `status`, `currentStepId`, `activationId`, `attempt`, `waitingReason`,
260
+ `nextEligibleAt`, `createdAt`, `updatedAt`, `instanceRevision`, `failure`, `output`,
261
+ `compatible` (whether *this* build may advance it; `incompatibleReason: 'incompatible-build'`
262
+ when not) — and **no** secrets (no HMAC keys, database paths, raw SQL, credentials).
263
+ Read-only inspection of an incompatible instance stays available so an operator can see
264
+ *why* it is not progressing rather than finding it silently stuck.
265
+ `server.workflowHistory(instanceId)` is the durable semantic transition log (`started`,
266
+ `step-activated`, `step-succeeded`, `step-failed`, `retry-scheduled`, `event-matched`,
267
+ `timer-fired`, `timeout-fired`, `branch-chosen`, `completed`, `failed`, `cancelled`) — a
268
+ bounded record, not an event-sourcing requirement.
269
+
270
+ `AgentAPI.analyzeWorkflow(workflowId)` is the static, graph-derived view: inputs, entry,
271
+ every step and its edges, the `ActionDef` / `EventDef` dependency sets, timer waits, branch
272
+ conditions, retry policies, the reachable terminal outcomes, acyclicity, and the kinds of
273
+ `waitingReason` an instance can produce.
274
+
275
+ ---
276
+
277
+ ## `WorkflowStore`
278
+
279
+ A provider-independent durable persistence abstraction: `createIdempotent`, `load`,
280
+ `loadByStart`, `transition` (fenced CAS), `recordActionOutcome` / `loadActionOutcome`,
281
+ `recoverRunnable`, `findEventWaits`, `history`, `list`. `createMemoryWorkflowStore()` is the
282
+ single-process reference; `createSqliteWorkflowStore({ location })` is the real cross-process
283
+ reference — independent OS-process connections, `BEGIN IMMEDIATE` with the revision + fence
284
+ check *inside* the transaction, `busy_timeout` + bounded retry so `SQLITE_BUSY` never
285
+ surfaces as application semantics, and `CREATE TABLE IF NOT EXISTS` + `INSERT OR IGNORE`
286
+ init so concurrent startup is safe. No SQLite path / table / rowid / WAL position appears in
287
+ the contract or the graph.
288
+
289
+ ---
290
+
291
+ ## Portability
292
+
293
+ `axiom.conformance.v8` (`conformance/workflow/`) is the portable tier. A fixture is a
294
+ compiled `axiom.server.v8` Server IR + start arguments + a deterministic driver script
295
+ (advance the virtual clock, deliver an event, mark an action outcome) + the **required
296
+ ordered logical transition history** and terminal state. `runWorkflowConformanceFixture` /
297
+ `runWorkflowConformanceSuite` run it over the in-memory store; physical attempts may be
298
+ duplicated, the logical history must match exactly. A future independent runtime implements
299
+ the fixtures from the contract alone.
300
+
301
+ ---
302
+
303
+ ## Limitations
304
+
305
+ - The deferred step kinds above are not available.
306
+ - 0.14 has no `wait-query` (a workflow does not wait on a `QueryDef` result).
307
+ - Cancellation cannot interrupt an already-dispatched physical external effect; the
308
+ framework stays honest about uncertain physical execution.
309
+ - **Physical** effect execution remains at-least-once (the effect system's contract); only
310
+ the *logical* ActionDef invocation and each workflow step transition are exactly-once.
311
+ - The durable event journal and the durable idempotency table are **bounded** buffers
312
+ (default 8192 journal entries / an idempotency window per authority config). They cover
313
+ crash and failover windows, not indefinite history; a wait parked longer than the journal
314
+ is retained past its `sinceEventSeq` is an implementation-owned retention concern.
315
+ - `WorkflowStore` bounded retention of terminal instances and history is
316
+ implementation-owned; an active workflow never expires.
317
+ - Structurally invalid workflow IR (unknown step kind, dangling edge, malformed
318
+ timer/terminal, missing entry) is refused at authority admission — `createAxiomServer`
319
+ throws `WorkflowIRError` rather than starting with a workflow it cannot execute. Malformed
320
+ step input to `validateGraph` produces a `WORKFLOW_INVALID_STEP` diagnostic, never a
321
+ native error.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Axiom
2
2
 
3
- > AI-native semantic application framework, version 0.13.1-alpha.1. An Axiom application is a
3
+ > AI-native semantic application framework, version 0.14.0-alpha.2. 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.1-alpha.1",
3
+ "version": "0.14.0-alpha.2",
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.1-alpha.1",
38
- "@cynodia/axiom-runtime": "0.13.1-alpha.1",
39
- "@cynodia/axiom-compiler": "0.13.1-alpha.1",
40
- "@cynodia/axiom-agent-api": "0.13.1-alpha.1"
37
+ "@cynodia/axiom-core": "0.14.0-alpha.2",
38
+ "@cynodia/axiom-runtime": "0.14.0-alpha.2",
39
+ "@cynodia/axiom-compiler": "0.14.0-alpha.2",
40
+ "@cynodia/axiom-agent-api": "0.14.0-alpha.2"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsc -b tsconfig.json"