@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 +3 -2
- package/docs/ACTIONS_TRANSACTIONS.md +1 -1
- package/docs/AGENT_API.md +1 -1
- package/docs/AGENT_REFERENCE.md +101 -2
- package/docs/ANTI_PATTERNS.md +130 -1
- package/docs/AUTHORITY.md +4 -2
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/DISTRIBUTED_AUTHORITY.md +24 -12
- package/docs/EFFECTS.md +1 -1
- package/docs/EVENTS.md +1 -1
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +1 -1
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/LIVE_QUERIES.md +1 -1
- package/docs/LOCATIONS.md +1 -1
- package/docs/MIGRATIONS.md +1 -1
- package/docs/PRESENTATION.md +1 -1
- package/docs/QUERIES.md +1 -1
- package/docs/RUNTIME.md +1 -1
- package/docs/SEMANTIC_CONTRACT.md +1 -1
- package/docs/STATE.md +1 -1
- package/docs/STORAGE.md +1 -1
- package/docs/SUBSCRIPTIONS.md +1 -1
- package/docs/TRIGGERS.md +1 -1
- package/docs/UI.md +1 -1
- package/docs/VALIDATION.md +26 -2
- package/docs/WORKFLOWS.md +321 -0
- package/llms.txt +1 -1
- package/package.json +5 -5
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.
|
|
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.
|
|
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
|
package/docs/AGENT_API.md
CHANGED
package/docs/AGENT_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent reference
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|
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
|
package/docs/ANTI_PATTERNS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-patterns
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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
|
|
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:
|
package/docs/CONSTRAINTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Distributed authority
|
|
2
2
|
|
|
3
|
-
*This document describes Axiom `0.
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
308
|
-
key differs **refuses to claim** that work
|
|
309
|
-
not claimed and stays visible as
|
|
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.
|
|
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.
|
|
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
|
package/docs/EXPRESSIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Expressions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|
package/docs/GRAPH_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Graph model
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Integrations
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|
package/docs/LIVE_QUERIES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Realtime — live canonical queries
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
package/docs/MIGRATIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Schema evolution & semantic migrations
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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,
|
package/docs/PRESENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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
|
# Semantic contract
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
package/docs/STORAGE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Storage and blobs
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|
package/docs/SUBSCRIPTIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Subscriptions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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.
|
|
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`:
|
package/docs/VALIDATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Validation
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
38
|
-
"@cynodia/axiom-runtime": "0.
|
|
39
|
-
"@cynodia/axiom-compiler": "0.
|
|
40
|
-
"@cynodia/axiom-agent-api": "0.
|
|
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"
|