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