@cynodia/axiom 0.7.0-alpha.2 → 0.8.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,8 @@ Axiom represents application behavior, state, UI structure and presentation as s
6
6
  semantic data executed by generic runtimes. An application is a typed graph, not source
7
7
  files: the JavaScript and HTML that reach the browser are output, and are never edited.
8
8
 
9
- **Status: experimental / alpha (0.7.0-alpha.x).** The API may change between alpha
10
- releases. The documentation in `docs/` describes this exact version.
9
+ **Status: experimental / alpha.** The API may change between alpha releases. The
10
+ documentation in `docs/` describes this exact version.
11
11
 
12
12
  ## Installation
13
13
 
@@ -1,6 +1,6 @@
1
1
  # Actions and transactions
2
2
 
3
- Axiom 0.7.0-alpha.2. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.8.1-alpha.1. An action is behavior expressed as data, executed as a transaction.
4
4
 
5
5
  ```ts
6
6
  {
@@ -86,7 +86,7 @@ All four failure sources are evaluated; the action does not stop at the first.
86
86
 
87
87
  ## Operations
88
88
 
89
- Seven kinds, enumerated by `OPERATION_KINDS`. `set`, `insert` and `remove` are the
89
+ Nine kinds, enumerated by `OPERATION_KINDS`. `set`, `insert` and `remove` are the
90
90
  mutations; each addresses a [`Location`](LOCATIONS.md).
91
91
 
92
92
  ### `set`
@@ -179,6 +179,37 @@ The controlled boundary for behavior the operation vocabulary cannot express.
179
179
  **Use it only where no semantic primitive exists.** A native operation is opaque to every
180
180
  analysis Axiom offers.
181
181
 
182
+ ### `integration-query`
183
+
184
+ ```ts
185
+ { kind: 'integration-query', operationId: NodeId, arguments?: Record<string, Expression>, bindAs: NodeId, timeoutMs?: number }
186
+ ```
187
+
188
+ Calls a `mode: 'query'` `IntegrationOperationDef` and binds its result into scope: later
189
+ operations in the same action refer to it as `ref(bindAs)`, the same way a `for-each`'s
190
+ `scopeId` introduces the current member. Resolved **before the transaction opens**, ahead
191
+ of guards — a query is awaited, but never mid-transaction. Never legal inside `for-each`.
192
+ Full model: [`INTEGRATIONS.md`](INTEGRATIONS.md).
193
+
194
+ ### `integration-effect`
195
+
196
+ ```ts
197
+ {
198
+ kind: 'integration-effect',
199
+ operationId: NodeId,
200
+ arguments?: Record<string, Expression>,
201
+ idempotencyKey?: Expression,
202
+ succeededEventId?: NodeId,
203
+ failedEventId?: NodeId,
204
+ }
205
+ ```
206
+
207
+ Calls a `mode: 'effect'` `IntegrationOperationDef`. It **never calls the adapter during the
208
+ transaction**: reaching this operation only records intent, discarded on rollback exactly
209
+ like a mutation is. The adapter runs only after the transaction commits, and the response
210
+ this action's caller gets back never waits for it — "committed, effect pending." Never legal
211
+ inside `for-each`. Full model: [`EFFECTS.md`](EFFECTS.md).
212
+
182
213
  ## Authorization
183
214
 
184
215
  ```ts
package/docs/AGENT_API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent API
2
2
 
3
- Axiom 0.7.0-alpha.2. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.8.1-alpha.1. The machine-facing interface. Agents query semantics and apply
4
4
  structural transformations; they never edit generated code.
5
5
 
6
6
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.7.0-alpha.2. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.8.1-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:
@@ -31,7 +31,7 @@ One canonical term per concept. These are not interchangeable.
31
31
  ## Graph construction
32
32
 
33
33
  ```ts
34
- const graph = new ApplicationGraph(id, name); // version defaults to '0.7.0'
34
+ const graph = new ApplicationGraph(id, name); // version defaults to '0.8.0'
35
35
  graph.addNode<StateDef>({ id, kind: 'state', ... }); // returns NodeId; throws if id exists
36
36
  graph.getNode<StateDef>(id); // deep clone, or undefined
37
37
  graph.updateNode(node); // write a modified node back
@@ -477,7 +477,7 @@ reads nothing from globals.
477
477
 
478
478
  ## Diagnostics
479
479
 
480
- 23 runtime codes, all in `RUNTIME_DIAGNOSTIC_CODES`. Match on `code`, never on the message.
480
+ 30 runtime codes, all in `RUNTIME_DIAGNOSTIC_CODES`. Match on `code`, never on the message.
481
481
  Full table with `details` fields: [`RUNTIME.md`](RUNTIME.md#diagnostic-codes).
482
482
 
483
483
  ```ts
@@ -493,7 +493,7 @@ if (!result.ok) {
493
493
  ## Validation
494
494
 
495
495
  `validateGraph(graph)` → `{ valid, errors, warnings }`. `valid` is `errors.length === 0`;
496
- warnings never make a graph invalid. 49 codes in `VALIDATION_CODES`, grouped in
496
+ warnings never make a graph invalid. 72 codes in `VALIDATION_CODES`, grouped in
497
497
  [`VALIDATION.md`](VALIDATION.md).
498
498
 
499
499
  ## Agent API
@@ -529,6 +529,13 @@ authoring an application that crosses the trust boundary.
529
529
  11. **IDEMPOTENCY** — a generated request id is unique across runtime instances; records are scoped by principal.
530
530
  12. **CHANGES** — `changes` names every observable state whose value moved, and no others.
531
531
  13. **PORTABILITY** — `axiom.server.v1` is frozen and language-independent.
532
+ 14. **INTEGRATION** — external systems are accessed through typed integration operations.
533
+ 15. **QUERY** — an external query is explicit action/trigger execution, never a pure `Expression`.
534
+ 16. **EFFECT** — external effects are not rollback-capable state mutations.
535
+ 17. **OUTBOX** — effect intent is committed before external execution, atomically with the state write that requested it.
536
+ 18. **TRIGGER** — triggers invoke ordinary actions, under the same guards, constraints and authorization.
537
+ 19. **EVENT** — events are typed facts; actions perform work.
538
+ 20. **SECRET** — credentials never live in graph semantics.
532
539
 
533
540
  ```ts
534
541
  { id: STATE_PRODUCTS, kind: 'state', authority: 'server' } // the authority owns it
@@ -590,6 +597,53 @@ through the same action-outcome lifecycle a local refusal uses — so a `diagnos
590
597
  presents a server refusal exactly as it presents a local one, and the control that started it
591
598
  renders `aria-busy` and refuses a second press until it settles.
592
599
 
600
+ ## INTEGRATIONS, EFFECTS, TRIGGERS
601
+
602
+ Full model: [`INTEGRATIONS.md`](INTEGRATIONS.md), [`EFFECTS.md`](EFFECTS.md),
603
+ [`TRIGGERS.md`](TRIGGERS.md), [`EVENTS.md`](EVENTS.md). These are the invariants to know
604
+ before authoring an application that reaches an external system or reacts to time or an
605
+ event.
606
+
607
+ 1. **INTEGRATION INVARIANT** — external systems are accessed through typed integration operations; the graph never carries an SDK, a host name or a secret.
608
+ 2. **QUERY INVARIANT** — external queries are explicit execution, resolved before the transaction they feed opens — never a pure `Expression`. `timeoutMs` is enforced by the runtime itself, not left to adapter cooperation; a non-cooperating adapter cannot wedge the invocation past its deadline (spec 8.1 §15-25).
609
+ 3. **EFFECT INVARIANT** — external effects are not rollback-capable state mutations. Reaching `integration-effect` only records intent; the adapter runs only after commit. The outcome reaches a follow-up action as a structured envelope (`effectOutcomeEntity` — `EFFECT_ID_FIELD`/`EFFECT_OPERATION_ID_FIELD`/…), never a raw result or a formatted string requiring text parsing to correlate (spec 8.1 §37-41).
610
+ 4. **OUTBOX INVARIANT** — effect intent is committed atomically with the state write that requested it, before external execution, so a crash between the two does not lose it.
611
+ 5. **TRIGGER INVARIANT** — a trigger invokes an ordinary action, under exactly the guards, constraints, transition constraints and authorization any other caller is subject to. Every invocation this authority runs — client request or trigger tick — is serialized against every other one, so simultaneous same-period triggers commit one at a time identically on the deterministic and the real host (spec 8.1 §26-30).
612
+ 6. **EVENT INVARIANT** — an event is a typed fact, validated against its declared payload type before any action sees it; an action is where work happens.
613
+ 7. **SECRET INVARIANT** — integration credentials live in host configuration (`AxiomServerOptions.integrations`), never in `ApplicationGraph`.
614
+ 8. **INVOCATION SOURCE INVARIANT** — a system-originated invocation (trigger, event, effect outcome) and an anonymous client request are distinct authoritative facts; a client cannot forge the former (`ExecutionContext.source` is server-computed, never read from protocol data), and `ActionDef.invocation.allowedSources` lets an action restrict which it accepts independently of `authorization`'s identity check (spec 8.1 §3-14).
615
+
616
+ ```ts
617
+ { kind: 'integration', id: INTEGRATION_DEVICE_PROVIDER }
618
+ {
619
+ kind: 'integration-operation', id: OP_FETCH_STATUS, integrationId: INTEGRATION_DEVICE_PROVIDER,
620
+ mode: 'query', resultType: primitiveType('string'),
621
+ }
622
+ {
623
+ kind: 'action', id: ACTION_REFRESH,
624
+ operations: [
625
+ { kind: 'integration-query', operationId: OP_FETCH_STATUS, bindAs: SCOPE_STATUS },
626
+ { kind: 'set', target: stateLocation(STATE_STATUS), value: ref(SCOPE_STATUS) },
627
+ ],
628
+ }
629
+ { kind: 'trigger', id: TRIGGER_POLL, actionId: ACTION_REFRESH, when: { kind: 'interval', everyMs: 5000 } }
630
+ ```
631
+
632
+ - `mode: 'query'` may bind its result into scope (`bindAs`) for later operations in the same action; `mode: 'effect'` never runs synchronously and its outcome reaches an action only through a dispatched `succeededEventId`/`failedEventId`.
633
+ - A trigger's target action runs where the action itself runs — server if it writes server state or calls an integration, client only for `route-enter`/`route-leave`. Derived, never declared, exactly like ordinary action authority.
634
+ - A triggered/event-originated invocation runs with `principal: null`, `source: 'system'` — the same as an anonymous client request, never an impersonated user. Authorization still evaluates, and `invocation.allowedSources` is checked before it: `{ invocation: { allowedSources: ['system'] } }` refuses a direct client `InvokeRequest` with `INVOCATION_SOURCE_NOT_ALLOWED`, which is what protects a webhook- or effect-outcome-only action from being forged by a client that guessed its id.
635
+ - `createDeterministicServerHost().advance(ms)` fires due timers deterministically; no trigger test waits on a real clock.
636
+
637
+ ```ts
638
+ agent.listIntegrations() / agent.listIntegrationOperations(id?);
639
+ agent.getActionsUsingIntegration(id) / agent.getEffectsForAction(actionId);
640
+ agent.getTriggersForAction(actionId) / agent.getTimedTriggers();
641
+ agent.getActionsTriggeredByEvent(eventId) / agent.getWebhookEvents();
642
+ agent.getExternalDependencies(); // { integrations, operations } — the deployment manifest
643
+ agent.getSystemOnlyActions() / agent.getTriggersTargetingClientOnlyActions();
644
+ agent.isClientInvocable(actionId) / agent.isSystemOnly(actionId);
645
+ ```
646
+
593
647
  Portable artifacts, for a runtime written in another language:
594
648
 
595
649
  ```
@@ -600,8 +654,9 @@ Portable artifacts, for a runtime written in another language:
600
654
  ```
601
655
 
602
656
  Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZATION_DENIED`
603
- `CONCURRENCY_CONFLICT` `MALFORMED_REQUEST` `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE`
604
- and `REMOTE_ACTION_UNAVAILABLE` on the client.
657
+ `INVOCATION_SOURCE_NOT_ALLOWED` `CONCURRENCY_CONFLICT` `MALFORMED_REQUEST`
658
+ `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE` and `REMOTE_ACTION_UNAVAILABLE` on the
659
+ client.
605
660
 
606
661
  ## Metadata classes
607
662
 
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.7.0-alpha.2. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.8.1-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
@@ -390,8 +390,11 @@ credential belongs to the host, not the graph.
390
390
  { kind: 'native', implementationId: 'app.sendEmail' }
391
391
  ```
392
392
 
393
- External effects are deliberately unsolved in 0.6. A design involving commands, a
394
- transactional outbox and idempotency is future work; do not smuggle one in meanwhile.
393
+ Use `IntegrationDef` + an `integration-effect` operation instead — a typed semantic
394
+ operation, dispatched post-commit through the durable outbox, with retry and an
395
+ idempotency key, whose outcome reaches a follow-up action as a structured event rather than
396
+ a side effect nothing can roll back. See [`INTEGRATIONS.md`](INTEGRATIONS.md) and
397
+ [`EFFECTS.md`](EFFECTS.md).
395
398
 
396
399
  ## 31. Restating what the model already knows
397
400
 
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.7.0-alpha.2. How an application crosses the trust boundary.
3
+ Axiom 0.8.1-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
@@ -32,6 +32,14 @@ describes both halves, so there is no backend to write.
32
32
  11. **IDEMPOTENCY** — an automatically generated request id is unique across runtime instances, whatever the host's uuid provider does. See [Idempotency](#idempotency).
33
33
  12. **CHANGES** — `InvokeResponse.changes` names every observable state whose value moved, and no others. See [Observing authoritative state](#observing-authoritative-state).
34
34
  13. **PORTABILITY** — `axiom.server.v1` semantics are language-independent, defined normatively by this document, the [schemas](#machine-readable-contracts) and the [conformance fixtures](#conformance). See [Server IR v1](#server-ir-v1-is-frozen).
35
+ 14. **INTEGRATION** — external systems are accessed through typed integration operations, never through `NativeOperation` or a raw request embedded in the graph. See [External systems](#external-systems).
36
+ 15. **QUERY** — an external query is explicit action/trigger execution, resolved before the transaction it feeds opens — never a pure `Expression`. See [External systems](#external-systems).
37
+ 16. **EFFECT** — an external effect is not a rollback-capable state mutation. It is recorded as intent, and dispatched only after the transaction that requested it commits. See [External effects](#external-effects).
38
+ 17. **OUTBOX** — effect intent is committed atomically with the state write that requested it, before the adapter is ever called. See [External effects](#external-effects).
39
+ 18. **TRIGGER** — a trigger invokes an ordinary action, under the same guards, constraints, transition constraints and authorization any other caller is subject to. See [Triggers](#triggers).
40
+ 19. **EVENT** — an event is a typed fact, validated against its declared payload type before any action sees it; an action is where work happens. See [External events](#external-events).
41
+ 20. **SECRET** — integration credentials live in host configuration, never in `ApplicationGraph`. See [External systems](#external-systems).
42
+ 21. **INVOCATION SOURCE** — a system-originated invocation (trigger, event, effect outcome) and an anonymous client request are distinct authoritative facts; a client cannot forge the former, and an action may restrict which it accepts independently of caller identity. See [Invocation source](#invocation-source).
35
43
 
36
44
  ## Authority and persistence are different questions
37
45
 
@@ -81,8 +89,49 @@ claim that validation already happened.
81
89
  | Forge an action id | resolved from the authority's own IR → `UNKNOWN_SERVER_ACTION` |
82
90
  | Send an argument of the wrong shape | checked against the declared type → `ARGUMENT_TYPE_MISMATCH` |
83
91
  | Invoke without permission | `authorization`, evaluated on the authority → `AUTHORIZATION_DENIED` |
92
+ | Invoke an action reachable only by a trigger, event or effect outcome | `invocation.allowedSources` → `INVOCATION_SOURCE_NOT_ALLOWED` |
93
+ | Claim to be a trigger, event or effect outcome | `context.source` is server-computed; no protocol field lets a request supply it |
84
94
  | Read server-only state | it is not in the client IR, the snapshot or any answer |
85
95
 
96
+ ## Invocation source
97
+
98
+ `authorization` answers *who* may invoke an action; invocation source answers *how the
99
+ invocation reached the authority at all* — a distinct question, because the two can diverge.
100
+ An action meant only as a webhook's target, or only as an `integration-effect`'s
101
+ `succeededEventId`/`failedEventId` handler, may declare no `authorization` at all — it was
102
+ never meant to need one, since only the trigger runtime was ever supposed to call it. Before
103
+ this existed, any client that guessed the action's id could invoke it directly, forging a
104
+ fake webhook delivery or a fake effect outcome (spec 8.1 §3-9).
105
+
106
+ ```ts
107
+ {
108
+ id: ACTION_APPLY_STATUS_CHANGE,
109
+ kind: 'action',
110
+ invocation: { allowedSources: ['system'] }, // a client InvokeRequest is refused
111
+ operations: [ /* … */ ],
112
+ }
113
+ ```
114
+
115
+ `allowedSources` is `['client', 'system']` — both — unless declared otherwise, so every
116
+ existing graph keeps its current behavior. `'system'` covers every trigger kind (interval,
117
+ delay, lifecycle, event) and every effect-outcome dispatch; there is no finer distinction,
118
+ because a trigger invokes an action "through the same semantics as any other caller"
119
+ ([TRIGGER](#load-bearing-server-invariants)) and does not get a different one here either.
120
+
121
+ The source itself is `ExecutionContext.source`, computed by the authority and never read
122
+ from client-supplied protocol data — `InvokeRequest` and `EventRequest` carry no `source`
123
+ field to forge in the first place. A client request is always `'client'`; a trigger-, event-
124
+ or effect-outcome-originated invocation is always `'system'`, with `principal: null` exactly
125
+ as an anonymous client's is, so `authorization` still evaluates and cannot be bypassed by a
126
+ trigger either (spec §69,104) — invocation source and principal are deliberately separate
127
+ questions, and a system invocation is not a stand-in identity.
128
+
129
+ `checkInvocationSource` runs before `authorization`, before argument-driven work, and before
130
+ any transaction opens: a refusal changes nothing, commits nothing, and dispatches no effect
131
+ or event. `validateGraph` also catches the case a graph author can state statically — a
132
+ trigger targeting an action whose `allowedSources` excludes `'system'` could never succeed —
133
+ with `TRIGGER_TARGET_SOURCE_MISMATCH`.
134
+
86
135
  ## Server IR
87
136
 
88
137
  ```ts
@@ -311,14 +360,23 @@ Transport-independent by construction. One endpoint, semantic requests:
311
360
  ```ts
312
361
  { kind: 'snapshot', protocol: 'axiom.protocol.v1', credential?, sinceRevision? }
313
362
  { kind: 'invoke', protocol: 'axiom.protocol.v1', actionId, arguments?, credential?, requestId? }
363
+ { kind: 'event', protocol: 'axiom.protocol.v1', eventId, payload, credential? }
314
364
  ```
315
365
 
316
366
  ```ts
317
- { kind: 'result', ok, diagnostics, changes, revision, requestId?, replayed? }
318
- { kind: 'snapshot', snapshot: { revision, states, partial? } }
319
- { kind: 'error', diagnostics }
367
+ { kind: 'result', ok, diagnostics, changes, revision, requestId?, replayed? }
368
+ { kind: 'snapshot', snapshot: { revision, states, partial? } }
369
+ { kind: 'error', diagnostics }
370
+ { kind: 'event-result', ok, diagnostics }
320
371
  ```
321
372
 
373
+ `event` (spec 0.8) is an **additive** request kind under the same `axiom.protocol.v1`
374
+ identifier, not a new protocol version: unlike a Server IR document, a protocol message
375
+ carries no document-wide vocabulary ceiling a receiver could silently misinterpret. A
376
+ pre-0.8 server's `isServerRequest` check already rejects an unrecognized `kind` as
377
+ malformed rather than misreading it as something else, so an older implementation degrades
378
+ safely without needing to know the new kind exists.
379
+
322
380
  ### `sinceRevision`
323
381
 
324
382
  A snapshot request may name a revision the caller already holds. The answer is then
@@ -412,6 +470,14 @@ does for a local failure.
412
470
  | `CONCURRENCY_CONFLICT` | Another transaction committed the same state first. Nothing was applied. |
413
471
  | `MALFORMED_REQUEST` | The request was not an Axiom semantic request, or spoke an unknown protocol. |
414
472
  | `AUTHORITY_UNREACHABLE` | The authority could not be reached, timed out, or answered with a transport error. |
473
+ | `EFFECT_FAILED` | An external effect's adapter reported failure after exhausting its retry policy. |
474
+ | `TRIGGER_INVOCATION_FAILED` | A trigger's target action reported failure, or its arguments failed to evaluate. |
475
+ | `EVENT_PAYLOAD_INVALID` | An external event's payload did not conform to its declared `EventDef.payloadType`. |
476
+ | `TRIGGER_OVERLAP_SKIPPED` | An interval trigger's tick fired while its previous invocation was still running, and the default `'skip'` overlap policy discarded it. |
477
+ | `INTEGRATION_ADAPTER_MISSING` | The Server IR requires an integration with no registered adapter — refused at `start()`, never deferred to first invocation. |
478
+ | `EVENT_DISPATCH_DEPTH_EXCEEDED` | An event → action → effect → event chain was stopped before it could recurse unboundedly. |
479
+ | `WEBHOOK_VERIFICATION_FAILED` | A webhook delivery failed provider signature verification and was refused before an event was ever constructed. |
480
+ | `INVOCATION_SOURCE_NOT_ALLOWED` | The action's `invocation.allowedSources` does not include this invocation's source (spec 8.1 §3-9) — refused before `authorization` is even evaluated, because no caller reaching the authority this way may invoke the action at all. |
415
481
 
416
482
  Two client-side codes belong to the boundary as well:
417
483
 
@@ -505,6 +571,8 @@ of them and no others. No fixture is permitted to disagree with the shipped runt
505
571
 
506
572
  ```
507
573
  @cynodia/axiom-server/schema/server-ir.v1.schema.json
574
+ @cynodia/axiom-server/schema/server-ir.v2.schema.json
575
+ @cynodia/axiom-server/schema/server-ir.v3.schema.json
508
576
  @cynodia/axiom-server/schema/protocol.v1.schema.json
509
577
  ```
510
578
 
@@ -523,15 +591,16 @@ page plus the conformance fixtures.
523
591
  | --- | --- | --- |
524
592
  | `axiom.server.v1` | the frozen 0.6.1 contract, below | 0.6.1 |
525
593
  | `axiom.server.v2` | the expression kinds `group` and `expression-ref`, and the `expressionDefs` they resolve against | 0.7.0 |
594
+ | `axiom.server.v3` | integrations, integration operations, events, triggers, and the `integration-query`/`integration-effect` operation kinds | 0.8.0 |
526
595
 
527
- `SERVER_IR_CONTRACTS` enumerates both. The rules:
596
+ `SERVER_IR_CONTRACTS` enumerates all three. The rules:
528
597
 
529
- - **A document declares the oldest contract that can carry it.** `compileToServerIR` computes the label from the vocabulary the document actually uses, so an application that uses nothing from 0.7 produces a byte-identical `axiom.server.v1` document, and the committed v1 conformance fixtures are unchanged.
598
+ - **A document declares the oldest contract that can carry it.** `compileToServerIR` computes the label from the vocabulary the document actually uses, so an application that uses nothing from 0.7 or 0.8 produces a byte-identical `axiom.server.v1` document, and the committed v1 conformance fixtures are unchanged.
530
599
  - **A runtime MUST refuse a contract it does not implement**, and MUST refuse a document whose vocabulary exceeds its declared contract. A v2 runtime executing a v1-labelled document that uses `group` would accept what a conforming v1 runtime elsewhere refuses, and the two would then disagree about the same file. `createAxiomServer` raises rather than executing one.
531
- - **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref` or `expressionDefs`, and `server-ir.v1.schema.json` is byte-frozen. Vocabulary arrives under a new identifier or not at all.
600
+ - **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref`, `expressionDefs`, an integration, a trigger, an event, or the `integration-query`/`integration-effect` operation kinds, and `server-ir.v1.schema.json` is byte-frozen. Vocabulary arrives under a new identifier or not at all.
532
601
 
533
602
  There is one JSON Schema per contract, each generated from the runtime's own vocabulary and
534
- each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`.
603
+ each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json`.
535
604
 
536
605
  **`group`.** Partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`. Groups appear
537
606
  in the order their key was **first seen** in the source; members keep source order; two keys are
@@ -598,16 +667,111 @@ result is identical to some serial order. Across processes, correctness rests on
598
667
  persistence adapter's revision check — the contract guarantees that a commit from a stale
599
668
  snapshot is refused, not that two processes coordinate.
600
669
 
601
- ## Not in 0.7.0
670
+ ## External systems
671
+
672
+ 0.8 adds a typed boundary to systems Axiom does not own: an `IntegrationDef` names a
673
+ capability domain (a shipping provider, a device fleet), and an `IntegrationOperationDef`
674
+ names one typed operation of it, with a declared `mode: 'query' | 'effect'`. The graph
675
+ never mentions an SDK, a host name, an HTTP client or a secret — those are supplied by an
676
+ `IntegrationAdapter`, registered with the authority (`AxiomServerOptions.integrations`),
677
+ keyed by integration id. **Integrations default server-only** (secrets, trust, CORS,
678
+ auditability, deterministic authority): an operation is client-invokable only if it
679
+ declares `clientSafe: true`, and client safety is never inferred from the absence of a
680
+ declared secret.
681
+
682
+ **A missing adapter fails `start()`, not the first invocation.** Every integration a
683
+ document requires is checked against the registry before any request is accepted
684
+ (`INTEGRATION_ADAPTER_MISSING`).
685
+
686
+ **A query is explicit execution, never a pure `Expression`.** An `integration-query`
687
+ operation calls its adapter and binds the (type-checked) result into scope as
688
+ `ref(bindAs)`, resolved **before the transaction opens** — ahead of guards, so a query
689
+ never runs mid-transaction and a guard can never reference its result. A malformed
690
+ provider response is rejected at this boundary (`INTEGRATION_RESULT_INVALID`) rather than
691
+ handed to the application as `unknown`. Full model: `docs/INTEGRATIONS.md`.
692
+
693
+ ## External effects
694
+
695
+ **An external effect is not a rollback-capable state mutation**, and 0.8 does not pretend
696
+ otherwise (spec §15,16). Axiom can roll back a state write; it cannot roll back an email,
697
+ a payment or a shipment request.
698
+
699
+ Reaching an `integration-effect` operation only **records intent** — appended to the same
700
+ per-transaction log a mutation is, and discarded on rollback the same way. The adapter is
701
+ never called during the transaction. Only once the transaction **commits** — effect intent
702
+ persisted atomically with the state write that requested it, the transactional outbox
703
+ invariant — does an `EffectRunner` dispatch it, and the response the caller receives never
704
+ waits for that: "action committed, effect pending," not "action committed and its effect
705
+ succeeded." A `PersistenceAdapter` that implements `loadPendingEffects`/
706
+ `recordEffectAttempt` (both shipped adapters do) resumes any intent that was committed but
707
+ never reached a terminal status, so a crash between commit and dispatch does not lose it —
708
+ **at-least-once delivery**, not exactly-once. Effect operations may declare `idempotent:
709
+ true` and a `retry` policy (`'none' | 'fixed' | 'exponential'`); an idempotency key,
710
+ computed from `idempotencyKey`, is handed to the adapter on every attempt so a provider can
711
+ deduplicate a retried call.
712
+
713
+ An effect's outcome is never folded back into the transaction that requested it. Instead,
714
+ its declared `succeededEventId`/`failedEventId` — an ordinary `EventDef` — is dispatched
715
+ through the same event pipeline an external webhook uses, once the outcome is known. There
716
+ is no automatic compensation: a semantic inverse (`refundPayment`) is another explicit
717
+ action, never an implicit `rollback(createPayment)`. Full model: `docs/EFFECTS.md`.
718
+
719
+ ## Triggers
720
+
721
+ A `TriggerDef` says **when** an action should be invoked — `interval`, `delay`,
722
+ `lifecycle` (`application-start`, `runtime-ready` on the server; `route-enter`,
723
+ `route-leave` on the client) or `event` — without embedding callback code. **A triggered
724
+ action runs through exactly the same semantics any other caller does**: the same guards,
725
+ constraints, transition constraints and authorization. There is no weaker, trigger-specific
726
+ execution path.
727
+
728
+ Timed and event triggers whose target action is server-authority run **on the authority**,
729
+ continuing whether or not a browser is connected; `application-start`/`runtime-ready`
730
+ triggers run once, in startup order, before requests are accepted (see
731
+ [Startup](#startup)). An interval trigger's default overlap policy is `'skip'`: a tick that
732
+ fires while the previous invocation is still running is discarded, not queued and never run
733
+ concurrently (`TRIGGER_OVERLAP_SKIPPED`); `'queue'` runs one pending tick immediately after.
734
+
735
+ **Timed and event-originated invocations run under a system context, never an impersonated
736
+ user.** `ExecutionContext.principal` is `null` — exactly what an anonymous client request's
737
+ is — and `.source` is `'system'`, carried only for observability. Authorization still
738
+ evaluates against that; it is never bypassed. An action whose authorization rule can never
739
+ be satisfied by a `null` principal is correctly refused when a trigger invokes it — the
740
+ graph decides, by declaring authorization or not on the actions it targets. Full model:
741
+ `docs/TRIGGERS.md`.
742
+
743
+ ## External events
744
+
745
+ An `EventDef` is a typed fact — a webhook delivery, an effect's outcome — never work
746
+ itself; a `TriggerDef{when:{kind:'event'}}` is what says what happens next. The semantic
747
+ protocol's `EventRequest` (`kind: 'event'`) carries only `eventId` and `payload`; the
748
+ payload is validated against `EventDef.payloadType` **before any action sees it**
749
+ (`EVENT_PAYLOAD_INVALID` otherwise) — malformed input never reaches trusted code.
750
+
751
+ **Provider authenticity is verified before an event is even constructed.** A webhook route
752
+ is registered on the Node host (`serveOverHttp({ webhooks })`), never declared by the
753
+ application: `verify` runs over the raw request first, and an unverified delivery never
754
+ reaches `decode` or the semantic layer. A provider `deliveryId`, when supplied, is
755
+ deduplicated against a bounded recent-deliveries window per route — a duplicate within that
756
+ window is acknowledged without dispatching the event again, with no claim of durable,
757
+ unbounded deduplication.
758
+
759
+ **Event dispatch is depth-guarded**, so a cycle (an event whose triggered effect's own
760
+ success re-fires it) is stopped rather than recursing unboundedly
761
+ (`EVENT_DISPATCH_DEPTH_EXCEEDED`, `MAX_EVENT_DISPATCH_DEPTH` dispatches deep). Full model:
762
+ `docs/EVENTS.md`.
763
+
764
+ ## Not in 0.8.0
602
765
 
603
766
  Stated plainly rather than left to discovery:
604
767
 
605
- - **Generated values cannot be bound within an action.** An operation cannot name a value an earlier operation produced: `uuid()` evaluated in one `insert` cannot be referred to by a later `insert` in the same action. Give the record an identity the action already has — a parameter, or a field of something it read — or perform the second write in a second action. A semantic binding for this (`bindAs` on an operation, `ref` to it later) needs a lexical lifetime, a type, `for-each` and nested-invoke semantics, serialization and dependency analysis all decided together; doing that hastily would weaken a contract that is now frozen, so it is deferred to 0.7.
768
+ - **Generated values cannot be bound within an action, in general.** An operation cannot name a value an earlier operation produced: `uuid()` evaluated in one `insert` cannot be referred to by a later `insert` in the same action. Give the record an identity the action already has — a parameter, or a field of something it read — or perform the second write in a second action. 0.8 adds exactly one narrow, purpose-built exception: an `integration-query`'s `bindAs` result, resolved before the transaction opens (see [External systems](#external-systems)) — not a general operation-result binding mechanism.
606
769
  - **Read authorization per caller or per record.** Visibility is per state.
607
-
608
- - **External effects.** A database write rolls back; an email does not. Nothing here makes an external side effect participate in a transaction, and `NativeOperation` MUST NOT be used to smuggle one in. A deliberate effect model — commands, a transactional outbox, idempotency — is future work.
770
+ - **Absolute/cron schedules.** `interval` and `delay` triggers cover "every N milliseconds" and "once after N milliseconds"; a calendar schedule ("every day at 09:00") is not modeled.
771
+ - **Client-side execution of interval, delay and lifecycle triggers.** The browser runtime implements no trigger kind at all. Before spec 8.1 a client-authority trigger silently compiled into `ApplicationIR.triggers` and simply never fired; now `validateGraph`/`compileToIR` reject it with `CLIENT_TRIGGER_UNSUPPORTED` instead, so the gap is a compile-time error rather than a runtime discovery. Only the authoritative runtime executes triggers today.
772
+ - **Durable effect delivery beyond the two shipped `PersistenceAdapter`s.** At-least-once delivery across a restart is real for `createMemoryPersistence` (within the process) and `createSqlitePersistence`; a third adapter earns the same claim only by implementing `loadPendingEffects`/`recordEffectAttempt` itself.
609
773
  - **Realtime synchronization**, subscriptions and collaboration. Request/response only.
610
774
  - **Query semantics.** Authoritative collections are loaded into runtime state; large-data querying needs its own design.
611
775
  - **Relational schema generation**, migrations and ORM behaviour.
612
776
  - **Multi-node distributed execution.** Correctness is guaranteed within one authority process.
613
- - **File storage, background jobs, scheduling.**
777
+ - **File storage, background worker fleets, a general job queue, a saga engine, a workflow language.** Effects and triggers are deliberately not a distributed job system (spec §2).
@@ -1,6 +1,6 @@
1
1
  # Constraints
2
2
 
3
- Axiom 0.7.0-alpha.2. Two constructs, answering different questions. They are not
3
+ Axiom 0.8.1-alpha.1. Two constructs, answering different questions. They are not
4
4
  interchangeable.
5
5
 
6
6
  | | Question | Sees |