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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.0-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.0-alpha.1. The machine-facing interface. Agents query semantics and apply
4
4
  structural transformations; they never edit generated code.
5
5
 
6
6
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.7.0-alpha.2. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.8.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:
@@ -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,50 @@ 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`.
609
+ 3. **EFFECT INVARIANT** — external effects are not rollback-capable state mutations. Reaching `integration-effect` only records intent; the adapter runs only after commit.
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.
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
+
615
+ ```ts
616
+ { kind: 'integration', id: INTEGRATION_DEVICE_PROVIDER }
617
+ {
618
+ kind: 'integration-operation', id: OP_FETCH_STATUS, integrationId: INTEGRATION_DEVICE_PROVIDER,
619
+ mode: 'query', resultType: primitiveType('string'),
620
+ }
621
+ {
622
+ kind: 'action', id: ACTION_REFRESH,
623
+ operations: [
624
+ { kind: 'integration-query', operationId: OP_FETCH_STATUS, bindAs: SCOPE_STATUS },
625
+ { kind: 'set', target: stateLocation(STATE_STATUS), value: ref(SCOPE_STATUS) },
626
+ ],
627
+ }
628
+ { kind: 'trigger', id: TRIGGER_POLL, actionId: ACTION_REFRESH, when: { kind: 'interval', everyMs: 5000 } }
629
+ ```
630
+
631
+ - `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`.
632
+ - 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.
633
+ - 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.
634
+ - `createDeterministicServerHost().advance(ms)` fires due timers deterministically; no trigger test waits on a real clock.
635
+
636
+ ```ts
637
+ agent.listIntegrations() / agent.listIntegrationOperations(id?);
638
+ agent.getActionsUsingIntegration(id) / agent.getEffectsForAction(actionId);
639
+ agent.getTriggersForAction(actionId) / agent.getTimedTriggers();
640
+ agent.getActionsTriggeredByEvent(eventId) / agent.getWebhookEvents();
641
+ agent.getExternalDependencies(); // { integrations, operations } — the deployment manifest
642
+ ```
643
+
593
644
  Portable artifacts, for a runtime written in another language:
594
645
 
595
646
  ```
@@ -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.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
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.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
@@ -32,6 +32,13 @@ 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).
35
42
 
36
43
  ## Authority and persistence are different questions
37
44
 
@@ -311,14 +318,23 @@ Transport-independent by construction. One endpoint, semantic requests:
311
318
  ```ts
312
319
  { kind: 'snapshot', protocol: 'axiom.protocol.v1', credential?, sinceRevision? }
313
320
  { kind: 'invoke', protocol: 'axiom.protocol.v1', actionId, arguments?, credential?, requestId? }
321
+ { kind: 'event', protocol: 'axiom.protocol.v1', eventId, payload, credential? }
314
322
  ```
315
323
 
316
324
  ```ts
317
- { kind: 'result', ok, diagnostics, changes, revision, requestId?, replayed? }
318
- { kind: 'snapshot', snapshot: { revision, states, partial? } }
319
- { kind: 'error', diagnostics }
325
+ { kind: 'result', ok, diagnostics, changes, revision, requestId?, replayed? }
326
+ { kind: 'snapshot', snapshot: { revision, states, partial? } }
327
+ { kind: 'error', diagnostics }
328
+ { kind: 'event-result', ok, diagnostics }
320
329
  ```
321
330
 
331
+ `event` (spec 0.8) is an **additive** request kind under the same `axiom.protocol.v1`
332
+ identifier, not a new protocol version: unlike a Server IR document, a protocol message
333
+ carries no document-wide vocabulary ceiling a receiver could silently misinterpret. A
334
+ pre-0.8 server's `isServerRequest` check already rejects an unrecognized `kind` as
335
+ malformed rather than misreading it as something else, so an older implementation degrades
336
+ safely without needing to know the new kind exists.
337
+
322
338
  ### `sinceRevision`
323
339
 
324
340
  A snapshot request may name a revision the caller already holds. The answer is then
@@ -412,6 +428,13 @@ does for a local failure.
412
428
  | `CONCURRENCY_CONFLICT` | Another transaction committed the same state first. Nothing was applied. |
413
429
  | `MALFORMED_REQUEST` | The request was not an Axiom semantic request, or spoke an unknown protocol. |
414
430
  | `AUTHORITY_UNREACHABLE` | The authority could not be reached, timed out, or answered with a transport error. |
431
+ | `EFFECT_FAILED` | An external effect's adapter reported failure after exhausting its retry policy. |
432
+ | `TRIGGER_INVOCATION_FAILED` | A trigger's target action reported failure, or its arguments failed to evaluate. |
433
+ | `EVENT_PAYLOAD_INVALID` | An external event's payload did not conform to its declared `EventDef.payloadType`. |
434
+ | `TRIGGER_OVERLAP_SKIPPED` | An interval trigger's tick fired while its previous invocation was still running, and the default `'skip'` overlap policy discarded it. |
435
+ | `INTEGRATION_ADAPTER_MISSING` | The Server IR requires an integration with no registered adapter — refused at `start()`, never deferred to first invocation. |
436
+ | `EVENT_DISPATCH_DEPTH_EXCEEDED` | An event → action → effect → event chain was stopped before it could recurse unboundedly. |
437
+ | `WEBHOOK_VERIFICATION_FAILED` | A webhook delivery failed provider signature verification and was refused before an event was ever constructed. |
415
438
 
416
439
  Two client-side codes belong to the boundary as well:
417
440
 
@@ -505,6 +528,8 @@ of them and no others. No fixture is permitted to disagree with the shipped runt
505
528
 
506
529
  ```
507
530
  @cynodia/axiom-server/schema/server-ir.v1.schema.json
531
+ @cynodia/axiom-server/schema/server-ir.v2.schema.json
532
+ @cynodia/axiom-server/schema/server-ir.v3.schema.json
508
533
  @cynodia/axiom-server/schema/protocol.v1.schema.json
509
534
  ```
510
535
 
@@ -523,15 +548,16 @@ page plus the conformance fixtures.
523
548
  | --- | --- | --- |
524
549
  | `axiom.server.v1` | the frozen 0.6.1 contract, below | 0.6.1 |
525
550
  | `axiom.server.v2` | the expression kinds `group` and `expression-ref`, and the `expressionDefs` they resolve against | 0.7.0 |
551
+ | `axiom.server.v3` | integrations, integration operations, events, triggers, and the `integration-query`/`integration-effect` operation kinds | 0.8.0 |
526
552
 
527
- `SERVER_IR_CONTRACTS` enumerates both. The rules:
553
+ `SERVER_IR_CONTRACTS` enumerates all three. The rules:
528
554
 
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.
555
+ - **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
556
  - **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.
557
+ - **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
558
 
533
559
  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`.
560
+ each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json`.
535
561
 
536
562
  **`group`.** Partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`. Groups appear
537
563
  in the order their key was **first seen** in the source; members keep source order; two keys are
@@ -598,16 +624,111 @@ result is identical to some serial order. Across processes, correctness rests on
598
624
  persistence adapter's revision check — the contract guarantees that a commit from a stale
599
625
  snapshot is refused, not that two processes coordinate.
600
626
 
601
- ## Not in 0.7.0
627
+ ## External systems
628
+
629
+ 0.8 adds a typed boundary to systems Axiom does not own: an `IntegrationDef` names a
630
+ capability domain (a shipping provider, a device fleet), and an `IntegrationOperationDef`
631
+ names one typed operation of it, with a declared `mode: 'query' | 'effect'`. The graph
632
+ never mentions an SDK, a host name, an HTTP client or a secret — those are supplied by an
633
+ `IntegrationAdapter`, registered with the authority (`AxiomServerOptions.integrations`),
634
+ keyed by integration id. **Integrations default server-only** (secrets, trust, CORS,
635
+ auditability, deterministic authority): an operation is client-invokable only if it
636
+ declares `clientSafe: true`, and client safety is never inferred from the absence of a
637
+ declared secret.
638
+
639
+ **A missing adapter fails `start()`, not the first invocation.** Every integration a
640
+ document requires is checked against the registry before any request is accepted
641
+ (`INTEGRATION_ADAPTER_MISSING`).
642
+
643
+ **A query is explicit execution, never a pure `Expression`.** An `integration-query`
644
+ operation calls its adapter and binds the (type-checked) result into scope as
645
+ `ref(bindAs)`, resolved **before the transaction opens** — ahead of guards, so a query
646
+ never runs mid-transaction and a guard can never reference its result. A malformed
647
+ provider response is rejected at this boundary (`INTEGRATION_RESULT_INVALID`) rather than
648
+ handed to the application as `unknown`. Full model: `docs/INTEGRATIONS.md`.
649
+
650
+ ## External effects
651
+
652
+ **An external effect is not a rollback-capable state mutation**, and 0.8 does not pretend
653
+ otherwise (spec §15,16). Axiom can roll back a state write; it cannot roll back an email,
654
+ a payment or a shipment request.
655
+
656
+ Reaching an `integration-effect` operation only **records intent** — appended to the same
657
+ per-transaction log a mutation is, and discarded on rollback the same way. The adapter is
658
+ never called during the transaction. Only once the transaction **commits** — effect intent
659
+ persisted atomically with the state write that requested it, the transactional outbox
660
+ invariant — does an `EffectRunner` dispatch it, and the response the caller receives never
661
+ waits for that: "action committed, effect pending," not "action committed and its effect
662
+ succeeded." A `PersistenceAdapter` that implements `loadPendingEffects`/
663
+ `recordEffectAttempt` (both shipped adapters do) resumes any intent that was committed but
664
+ never reached a terminal status, so a crash between commit and dispatch does not lose it —
665
+ **at-least-once delivery**, not exactly-once. Effect operations may declare `idempotent:
666
+ true` and a `retry` policy (`'none' | 'fixed' | 'exponential'`); an idempotency key,
667
+ computed from `idempotencyKey`, is handed to the adapter on every attempt so a provider can
668
+ deduplicate a retried call.
669
+
670
+ An effect's outcome is never folded back into the transaction that requested it. Instead,
671
+ its declared `succeededEventId`/`failedEventId` — an ordinary `EventDef` — is dispatched
672
+ through the same event pipeline an external webhook uses, once the outcome is known. There
673
+ is no automatic compensation: a semantic inverse (`refundPayment`) is another explicit
674
+ action, never an implicit `rollback(createPayment)`. Full model: `docs/EFFECTS.md`.
675
+
676
+ ## Triggers
677
+
678
+ A `TriggerDef` says **when** an action should be invoked — `interval`, `delay`,
679
+ `lifecycle` (`application-start`, `runtime-ready` on the server; `route-enter`,
680
+ `route-leave` on the client) or `event` — without embedding callback code. **A triggered
681
+ action runs through exactly the same semantics any other caller does**: the same guards,
682
+ constraints, transition constraints and authorization. There is no weaker, trigger-specific
683
+ execution path.
684
+
685
+ Timed and event triggers whose target action is server-authority run **on the authority**,
686
+ continuing whether or not a browser is connected; `application-start`/`runtime-ready`
687
+ triggers run once, in startup order, before requests are accepted (see
688
+ [Startup](#startup)). An interval trigger's default overlap policy is `'skip'`: a tick that
689
+ fires while the previous invocation is still running is discarded, not queued and never run
690
+ concurrently (`TRIGGER_OVERLAP_SKIPPED`); `'queue'` runs one pending tick immediately after.
691
+
692
+ **Timed and event-originated invocations run under a system context, never an impersonated
693
+ user.** `ExecutionContext.principal` is `null` — exactly what an anonymous client request's
694
+ is — and `.source` is `'system'`, carried only for observability. Authorization still
695
+ evaluates against that; it is never bypassed. An action whose authorization rule can never
696
+ be satisfied by a `null` principal is correctly refused when a trigger invokes it — the
697
+ graph decides, by declaring authorization or not on the actions it targets. Full model:
698
+ `docs/TRIGGERS.md`.
699
+
700
+ ## External events
701
+
702
+ An `EventDef` is a typed fact — a webhook delivery, an effect's outcome — never work
703
+ itself; a `TriggerDef{when:{kind:'event'}}` is what says what happens next. The semantic
704
+ protocol's `EventRequest` (`kind: 'event'`) carries only `eventId` and `payload`; the
705
+ payload is validated against `EventDef.payloadType` **before any action sees it**
706
+ (`EVENT_PAYLOAD_INVALID` otherwise) — malformed input never reaches trusted code.
707
+
708
+ **Provider authenticity is verified before an event is even constructed.** A webhook route
709
+ is registered on the Node host (`serveOverHttp({ webhooks })`), never declared by the
710
+ application: `verify` runs over the raw request first, and an unverified delivery never
711
+ reaches `decode` or the semantic layer. A provider `deliveryId`, when supplied, is
712
+ deduplicated against a bounded recent-deliveries window per route — a duplicate within that
713
+ window is acknowledged without dispatching the event again, with no claim of durable,
714
+ unbounded deduplication.
715
+
716
+ **Event dispatch is depth-guarded**, so a cycle (an event whose triggered effect's own
717
+ success re-fires it) is stopped rather than recursing unboundedly
718
+ (`EVENT_DISPATCH_DEPTH_EXCEEDED`, `MAX_EVENT_DISPATCH_DEPTH` dispatches deep). Full model:
719
+ `docs/EVENTS.md`.
720
+
721
+ ## Not in 0.8.0
602
722
 
603
723
  Stated plainly rather than left to discovery:
604
724
 
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.
725
+ - **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
726
  - **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.
727
+ - **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.
728
+ - **Client-side execution of interval, delay and lifecycle triggers.** `ApplicationIR.triggers` carries client-authority triggers for inspection, but the browser runtime does not yet schedule or dispatch them itself — only the authoritative runtime does. A client-authority trigger declared in a graph today is compiled and analyzable, not executed.
729
+ - **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
730
  - **Realtime synchronization**, subscriptions and collaboration. Request/response only.
610
731
  - **Query semantics.** Authoritative collections are loaded into runtime state; large-data querying needs its own design.
611
732
  - **Relational schema generation**, migrations and ORM behaviour.
612
733
  - **Multi-node distributed execution.** Correctness is guaranteed within one authority process.
613
- - **File storage, background jobs, scheduling.**
734
+ - **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.0-alpha.1. Two constructs, answering different questions. They are not
4
4
  interchangeable.
5
5
 
6
6
  | | Question | Sees |
@@ -0,0 +1,142 @@
1
+ # Effects
2
+
3
+ Axiom 0.8.0-alpha.1. External effects are not rollback-capable state mutations. This file
4
+ is the delivery model; [`AUTHORITY.md`](AUTHORITY.md#external-effects) is the load-bearing
5
+ statement of why, and [`INTEGRATIONS.md`](INTEGRATIONS.md) is the operation vocabulary this
6
+ builds on.
7
+
8
+ ## The operation
9
+
10
+ ```ts
11
+ {
12
+ kind: 'integration-effect',
13
+ operationId: NodeId,
14
+ arguments?: Record<string, Expression>,
15
+ idempotencyKey?: Expression,
16
+ succeededEventId?: NodeId,
17
+ failedEventId?: NodeId,
18
+ }
19
+ ```
20
+
21
+ `operationId` must name an `IntegrationOperationDef` with `mode: 'effect'`
22
+ (`INTEGRATION_OPERATION_MODE_MISMATCH` otherwise). Legal only at an action's top level,
23
+ never inside `for-each`.
24
+
25
+ ## Execution model — commit, then dispatch
26
+
27
+ ```text
28
+ Action
29
+ ↓
30
+ state transaction opens
31
+ ↓
32
+ `integration-effect` reached → intent recorded (transaction-local, like a mutation)
33
+ ↓
34
+ guards / constraints / transition constraints evaluated
35
+ ↓
36
+ commit — state writes AND effect intent persisted atomically (the outbox invariant)
37
+ ↓
38
+ response returned to the caller: "committed, effect pending"
39
+ ↓
40
+ EffectRunner dispatches the intent → IntegrationAdapter.effect(...)
41
+ ↓
42
+ terminal status (succeeded/failed) → declared event dispatched, if any
43
+ ```
44
+
45
+ Reaching `integration-effect` **never calls the adapter**. It only appends an intent to a
46
+ transaction-scoped log — the same log a mutation is recorded in, `AxiomRuntime
47
+ .getEffectIntents()`, discarded on rollback exactly the way a rolled-back mutation is. The
48
+ adapter is called only **after** the surrounding transaction commits, by a separate
49
+ `EffectRunner`, and the invoking request's response never waits for that call — spec §123's
50
+ "action committed, effect pending" is literal: `outcome`/`ok` in the response describes
51
+ whether the **state transaction** committed, not whether the effect has succeeded yet.
52
+
53
+ ## The outbox invariant
54
+
55
+ Effect intent is committed **atomically with the state write that requested it** —
56
+ `PersistenceAdapter.commit()` receives both `writes` and `effects` in the same call, and a
57
+ durable adapter persists them together. `createMemoryPersistence` and
58
+ `createSqlitePersistence` both implement the adapter's optional `loadPendingEffects()` /
59
+ `recordEffectAttempt()` pair, so a restarted authority resumes any intent that was
60
+ committed but never reached a terminal status — a crash between commit and the adapter
61
+ call does not lose the intent.
62
+
63
+ **Delivery is at-least-once, never exactly-once.** A resumed dispatch gets a fresh full
64
+ retry budget rather than picking up a partially-spent one, because a process that crashed
65
+ mid-call was never told whether its one call succeeded. This is why `idempotent: true` and
66
+ an `idempotencyKey` matter: a provider capable of deduplicating a retried call is what
67
+ turns at-least-once delivery into an effectively-once outcome.
68
+
69
+ ## Retry
70
+
71
+ ```ts
72
+ retry?: { policy: 'none' | 'fixed' | 'exponential'; maxAttempts?: number; delayMs?: number }
73
+ ```
74
+
75
+ Declared on the `IntegrationOperationDef`, not on the calling action — retry is an
76
+ external-effect-execution concern, not business UI. `policy: 'none'` (the default) is one
77
+ attempt. `'fixed'` waits `delayMs` (default 1000ms) between attempts; `'exponential'`
78
+ doubles it each time. The wait uses the host's own scheduling
79
+ (`ServerHost.scheduleOnce`), so a test can drive it with `createDeterministicServerHost()`
80
+ + `advance(ms)` and never wait on a real clock.
81
+
82
+ ## Effect status and observability
83
+
84
+ ```ts
85
+ type EffectDispatchStatus = 'pending' | 'running' | 'succeeded' | 'failed';
86
+
87
+ interface EffectRecord {
88
+ id: string;
89
+ operationId: NodeId;
90
+ arguments: Record<string, unknown>;
91
+ status: EffectDispatchStatus;
92
+ attempts: number;
93
+ lastError?: { code: string; message: string; retryable?: boolean };
94
+ result?: unknown; // the adapter's returned value, once succeeded
95
+ }
96
+
97
+ server.effectLog(); // EffectRecord[]
98
+ ```
99
+
100
+ `server.effectLog()` is distinct from `server.mutationLog()` — an effect is not a state
101
+ mutation, and mixing the two would misrepresent what actually happened (spec §73). Host
102
+ `report()` events cover the whole lifecycle: `effect-requested`, `effect-attempted`,
103
+ `effect-succeeded`, `effect-failed`.
104
+
105
+ ## The result reaches an action only through an event
106
+
107
+ An effect's outcome is never folded back into the transaction that requested it. Instead,
108
+ `succeededEventId`/`failedEventId` — ordinary `EventDef` nodes — are dispatched through
109
+ the same event pipeline an external webhook uses (see [`EVENTS.md`](EVENTS.md)), once the
110
+ outcome is known:
111
+
112
+ - **Success payload** is the effect operation's own `resultType` value — the adapter's
113
+ returned result, unchanged.
114
+ - **Failure payload** is the error formatted as text, `"<code>: <message>"` — declare
115
+ `failedEventId`'s `payloadType` as `primitiveType('string')` to receive it.
116
+
117
+ Both are checked against the declared `EventDef.payloadType` the same way any event is,
118
+ so a mismatched declaration is caught rather than silently dropped.
119
+
120
+ ## No automatic compensation
121
+
122
+ 0.8 does not implement compensation. If an effect has a semantic inverse, it is another
123
+ explicit action and effect — `refundPayment`, never `rollback(createPayment)`.
124
+
125
+ ## Validation and diagnostics
126
+
127
+ | Code | Raised when |
128
+ | --- | --- |
129
+ | `UNKNOWN_EVENT` | `succeededEventId`/`failedEventId` naming something that is not an `EventDef`. |
130
+ | `EFFECT_FAILED` | An adapter reported failure after the retry policy was exhausted. |
131
+
132
+ Full tables: [`VALIDATION.md`](VALIDATION.md#integrations-effects-triggers-and-events),
133
+ [`AUTHORITY.md`](AUTHORITY.md#diagnostics).
134
+
135
+ ## AgentAPI
136
+
137
+ ```ts
138
+ agent.getEffectsForAction(actionId); // IntegrationOperationDef[] — the effect-mode operations it calls
139
+ ```
140
+
141
+ "What can this application do to an external system, and from where" is answerable without
142
+ reading source — spec §78's example question, made concrete.
package/docs/EVENTS.md ADDED
@@ -0,0 +1,115 @@
1
+ # Events
2
+
3
+ Axiom 0.8.0-alpha.1. An event is a typed fact — something that happened — never work
4
+ itself. [`AUTHORITY.md`](AUTHORITY.md#external-events) is the load-bearing statement;
5
+ this file is the vocabulary and the webhook delivery mechanism.
6
+
7
+ ## The model
8
+
9
+ ```ts
10
+ interface EventDef {
11
+ id: NodeId;
12
+ kind: 'event';
13
+ payloadType: TypeRef;
14
+ }
15
+ ```
16
+
17
+ Nothing more: an event is a name and a typed payload. What happens when one occurs is a
18
+ `TriggerDef{when:{kind:'event', eventId}}` — see [`TRIGGERS.md`](TRIGGERS.md). Keeping
19
+ "a fact occurred" (`EventDef`) and "do this work" (`ActionDef`, via a trigger) as separate
20
+ node kinds is deliberate (spec §46): an event may have zero, one or several triggers bound
21
+ to it, and none of them is embedded in the event's own declaration.
22
+
23
+ ## Event vs. action
24
+
25
+ | | Answers |
26
+ | --- | --- |
27
+ | `EventDef` | "What happened?" |
28
+ | `ActionDef` | "What should be done about it?" |
29
+
30
+ An event never mutates state itself. Only the action a trigger invokes does, through the
31
+ ordinary governed write path.
32
+
33
+ ## Sources
34
+
35
+ An event reaches the semantic layer three ways, and all three funnel through the same
36
+ `dispatchEvent`/`TriggerRuntime.fireEvent` path and the same payload validation:
37
+
38
+ 1. **An external webhook**, decoded and verified by a registered host adapter (below).
39
+ 2. **An effect's own outcome** — `succeededEventId`/`failedEventId` on an
40
+ `integration-effect` operation (see [`EFFECTS.md`](EFFECTS.md)).
41
+ 3. **The semantic protocol directly** — `EventRequest{ kind: 'event', eventId, payload }`,
42
+ for a host that already trusts its own caller (an internal service, a test).
43
+
44
+ ## Payload validation
45
+
46
+ Every event's payload is checked against its declared `EventDef.payloadType` — the same
47
+ `validateValueAgainstType` walk that checks action arguments and seed data — **before any
48
+ trigger's action runs**. A malformed payload never reaches trusted code:
49
+ `EVENT_PAYLOAD_INVALID`, and no action is invoked.
50
+
51
+ ## Webhooks
52
+
53
+ ```ts
54
+ import { serveOverHttp } from '@cynodia/axiom-server';
55
+
56
+ await serveOverHttp({
57
+ server,
58
+ webhooks: {
59
+ '/webhooks/device-provider': {
60
+ verify: (request) => verifySignature(request.headers, request.rawBody, secret),
61
+ decode: (request) => {
62
+ const body = JSON.parse(request.rawBody.toString('utf8'));
63
+ return { eventId: EVENT_DEVICE_STATUS_CHANGED, payload: body.status, deliveryId: body.id };
64
+ },
65
+ },
66
+ },
67
+ });
68
+ ```
69
+
70
+ An application author never declares an HTTP route (spec §54) — `webhooks` is a
71
+ *deployment* concern, registered where the Node host is stood up, and the graph never
72
+ mentions it. `verify` runs over the **raw, unparsed request**: signature verification is
73
+ over the exact bytes a provider signed, and it runs strictly before `decode` — an
74
+ unverified request never reaches the semantic layer at all (`WEBHOOK_VERIFICATION_FAILED`,
75
+ 401). Provider-specific protocol — headers, signing scheme, payload shape — stays entirely
76
+ in `verify`/`decode`; nothing crosses into `ApplicationGraph`.
77
+
78
+ ## Duplicate delivery
79
+
80
+ `decode`'s optional `deliveryId` is deduplicated against a **bounded, per-route,
81
+ most-recent window** (512 entries): a duplicate delivery within that window is
82
+ acknowledged (`ok: true`) without dispatching the event again. This is not a durable,
83
+ unbounded guarantee — a delivery older than the window, or a restart, is not remembered.
84
+ State that. Do not claim more.
85
+
86
+ ## Event loop protection
87
+
88
+ A cascade — an event triggers an action, whose effect's success re-fires the same event —
89
+ is bounded: `MAX_EVENT_DISPATCH_DEPTH` (8) dispatches deep, then further dispatch stops
90
+ (`EVENT_DISPATCH_DEPTH_EXCEEDED`) rather than recursing without limit. Depth is carried
91
+ forward from the invocation that created an effect intent to the event its outcome
92
+ dispatches, so the guard holds across the commit/dispatch boundary, not only within one
93
+ synchronous call chain.
94
+
95
+ ## Validation and diagnostics
96
+
97
+ | Code | Raised when |
98
+ | --- | --- |
99
+ | `UNKNOWN_EVENT` | A trigger's `eventId`, or an effect's `succeededEventId`/`failedEventId`, does not resolve to an `EventDef`. |
100
+ | `EVENT_PAYLOAD_INVALID` | A payload does not conform to its declared `payloadType`. |
101
+ | `EVENT_DISPATCH_DEPTH_EXCEEDED` | A cascade was stopped at the depth guard. |
102
+
103
+ Full tables: [`VALIDATION.md`](VALIDATION.md#integrations-effects-triggers-and-events),
104
+ [`AUTHORITY.md`](AUTHORITY.md#diagnostics).
105
+
106
+ ## AgentAPI
107
+
108
+ ```ts
109
+ agent.getWebhookEvents(); // EventDef[] — events at least one trigger reacts to
110
+ agent.getActionsTriggeredByEvent(id); // ActionDef[]
111
+ ```
112
+
113
+ "Which webhook can mutate `Order`?" (spec §78) is answerable by following
114
+ `getWebhookEvents()` through `getActionsTriggeredByEvent()` to the actions it can reach,
115
+ without reading a single handler.
@@ -1,6 +1,6 @@
1
1
  # Expressions
2
2
 
3
- Axiom 0.7.0-alpha.2. An expression describes **what value is computed**. It is a tree of
3
+ Axiom 0.8.0-alpha.1. An expression describes **what value is computed**. It is a tree of
4
4
  plain data, never source text and never a callback. Evaluation is pure: an expression MUST
5
5
  NOT change state.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Graph model
2
2
 
3
- Axiom 0.7.0-alpha.2. The `ApplicationGraph` is the authoritative representation of an
3
+ Axiom 0.8.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
 
@@ -25,7 +25,7 @@ edited.
25
25
  ## API
26
26
 
27
27
  ```ts
28
- const graph = new ApplicationGraph(id, name, version?); // version defaults to '0.7.0'
28
+ const graph = new ApplicationGraph(id, name, version?); // version defaults to '0.8.0'
29
29
 
30
30
  graph.addNode<T>(node): NodeId // generates an id if omitted; throws if it exists
31
31
  graph.getNode<T>(id): T | undefined // deep clone
@@ -0,0 +1,152 @@
1
+ # Integrations
2
+
3
+ Axiom 0.8.0-alpha.1. How an application declares and calls an external system, without
4
+ embedding a transport, an SDK or a secret in the graph. The authority boundary this
5
+ depends on is [`AUTHORITY.md`](AUTHORITY.md#external-systems); this file is the vocabulary.
6
+
7
+ ## The model
8
+
9
+ ```ts
10
+ interface IntegrationDef {
11
+ id: NodeId;
12
+ kind: 'integration';
13
+ name?: string;
14
+ }
15
+
16
+ interface IntegrationOperationDef {
17
+ id: NodeId;
18
+ kind: 'integration-operation';
19
+ integrationId: NodeId;
20
+ mode: 'query' | 'effect';
21
+ parameters?: IntegrationOperationParameter[];
22
+ resultType: TypeRef;
23
+ clientSafe?: boolean;
24
+ idempotent?: boolean;
25
+ retry?: { policy: 'none' | 'fixed' | 'exponential'; maxAttempts?: number; delayMs?: number };
26
+ }
27
+
28
+ interface IntegrationOperationParameter {
29
+ id: NodeId;
30
+ name?: string;
31
+ valueType: TypeRef;
32
+ required?: boolean;
33
+ }
34
+ ```
35
+
36
+ `IntegrationDef` names a capability domain — a shipping provider, a device fleet, a
37
+ payments processor. `IntegrationOperationDef` is one typed operation of it. Neither ever
38
+ carries an SDK name, a host name, a secret or an HTTP client: those live in the host, as an
39
+ `IntegrationAdapter` (`@cynodia/axiom-server`), registered by integration id.
40
+
41
+ **`INTEGRATION_OPERATION_MODES`** is `['query', 'effect']`, and the distinction is
42
+ load-bearing:
43
+
44
+ | Mode | Meaning | May its result feed the same transaction? | Rollback-capable? |
45
+ | --- | --- | --- | --- |
46
+ | `'query'` | Observes the external system; does not intentionally mutate it. | Yes — bound into scope, resolved before the transaction opens. | N/A — nothing to roll back. |
47
+ | `'effect'` | May mutate or cause an irreversible external consequence. | No — its outcome reaches state only through a follow-up action invoked from a dispatched event. | **No.** See [`EFFECTS.md`](EFFECTS.md). |
48
+
49
+ `clientSafe` defaults to absent, which means server-only. Client safety is never inferred
50
+ from the absence of a declared secret — it is stated, or it is not granted.
51
+
52
+ ## Calling an operation
53
+
54
+ Two new `Operation` kinds, added to `OPERATION_KINDS`; both are legal only at an action's
55
+ top level, never nested inside a `for-each`:
56
+
57
+ ```ts
58
+ { kind: 'integration-query', operationId: NodeId, arguments?: Record<string, Expression>, bindAs: NodeId, timeoutMs?: number }
59
+ { kind: 'integration-effect', operationId: NodeId, arguments?: Record<string, Expression>, idempotencyKey?: Expression, succeededEventId?: NodeId, failedEventId?: NodeId }
60
+ ```
61
+
62
+ `integration-query` calls its adapter and binds the (type-checked) result into scope:
63
+ later operations in the same action refer to it as `ref(bindAs)`, the same way a
64
+ `for-each`'s `scopeId` introduces the current member — except the whole result is bound,
65
+ not a collection member. Resolution happens **before the transaction opens**, ahead of
66
+ every guard: guards are validated against the scope as it stood before any
67
+ `integration-query`, so a guard can never reference `ref(bindAs)` — that is checked
68
+ statically, not merely documented. Full operation semantics: [`ACTIONS_TRANSACTIONS.md`
69
+ § `integration-query`](ACTIONS_TRANSACTIONS.md#integration-query).
70
+
71
+ `integration-effect` is described in [`EFFECTS.md`](EFFECTS.md).
72
+
73
+ ## Expression purity is unaffected
74
+
75
+ `Expression` evaluation stays pure with respect to external systems: nothing in
76
+ `EXPRESSION_KINDS` performs I/O, and this is unchanged by 0.8. An integration query is
77
+ reached only through an explicit `Operation`, never through an `Expression` — there is no
78
+ `fieldDisplay.value = queryWeather()`. A pure `Expression` remains deterministic over
79
+ semantic state alone.
80
+
81
+ ## Result typing
82
+
83
+ A provider's response is checked against `resultType` at the adapter boundary. A response
84
+ that does not conform is never handed to the application as `unknown` — it is rejected as
85
+ `INTEGRATION_RESULT_INVALID` (a runtime diagnostic; see [`RUNTIME.md`](RUNTIME.md)) before
86
+ `ref(bindAs)` would ever resolve to it.
87
+
88
+ ## Registering an adapter
89
+
90
+ ```ts
91
+ import { createAxiomServer, createFakeIntegrationAdapter, createHttpIntegrationAdapter } from '@cynodia/axiom-server';
92
+
93
+ const server = createAxiomServer({
94
+ ir: compileToServerIR(graph),
95
+ integrations: {
96
+ [INTEGRATION_DEVICE_PROVIDER]: createHttpIntegrationAdapter({
97
+ baseUrl: 'https://devices.example.com/',
98
+ operations: {
99
+ [OPERATION_FETCH_STATUS]: { method: 'GET', path: 'devices/{deviceId}/status' },
100
+ [OPERATION_REBOOT]: { method: 'POST', path: 'devices/{deviceId}/reboot' },
101
+ },
102
+ }),
103
+ },
104
+ });
105
+ ```
106
+
107
+ `createAxiomServer.start()` refuses to start if any integration the Server IR requires has
108
+ no registered adapter (`INTEGRATION_ADAPTER_MISSING`) — checked once, at startup, never
109
+ deferred to the first request that happens to need it.
110
+
111
+ `createHttpIntegrationAdapter` is a generic, lower-level reference adapter for an arbitrary
112
+ REST service: a base URL, a method and a path template per operation (`{param}`
113
+ substituted from the operation's arguments), a JSON body built from the remaining
114
+ arguments, and a timeout via `AbortController`. It is explicitly not the canonical
115
+ integration model — a typed `IntegrationOperationDef` is — only a way to prove one works
116
+ without writing a bespoke adapter for a demo. `createFakeIntegrationAdapter({ query?,
117
+ effect? })` returns deterministic, caller-supplied results: what conformance fixtures and
118
+ tests use, since semantics must never depend on a real network call.
119
+
120
+ ## Validation
121
+
122
+ | Code | Raised when |
123
+ | --- | --- |
124
+ | `UNKNOWN_INTEGRATION` | An `IntegrationOperationDef.integrationId` that does not resolve to an `integration` node. |
125
+ | `UNKNOWN_INTEGRATION_OPERATION` | An `integration-query`/`integration-effect` operation's `operationId` that does not resolve to an `integration-operation` node. |
126
+ | `INTEGRATION_OPERATION_MODE_MISMATCH` | An `integration-query` naming an effect operation, or the reverse. |
127
+ | `INTEGRATION_ARGUMENT_MISMATCH` | A missing required argument, or one the operation declares no parameter for. |
128
+
129
+ Full table: [`VALIDATION.md`](VALIDATION.md#integrations-effects-triggers-and-events).
130
+
131
+ ## Runtime diagnostics
132
+
133
+ | Code | Meaning |
134
+ | --- | --- |
135
+ | `INTEGRATION_UNAVAILABLE` | An `integration-query` operation ran, but no host capable of executing one is configured. |
136
+ | `INTEGRATION_TIMEOUT` | A query did not answer within its declared `timeoutMs`. |
137
+ | `INTEGRATION_RESULT_INVALID` | A provider's response did not conform to `resultType`. |
138
+ | `INTEGRATION_QUERY_FAILED` | A query failed for any other reason. Never carries a provider secret. |
139
+
140
+ Full table: [`RUNTIME.md`](RUNTIME.md#diagnostic-codes).
141
+
142
+ ## AgentAPI
143
+
144
+ ```ts
145
+ agent.listIntegrations(); // IntegrationDef[]
146
+ agent.listIntegrationOperations(integrationId?); // IntegrationOperationDef[]
147
+ agent.getActionsUsingIntegration(integrationId); // ActionDef[]
148
+ agent.getExternalDependencies(); // { integrations, operations }
149
+ ```
150
+
151
+ `getExternalDependencies()` is the machine-discoverable manifest spec §115 asks for — what
152
+ an application requires before it can be deployed.
package/docs/LOCATIONS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Locations
2
2
 
3
- Axiom 0.7.0-alpha.2.
3
+ Axiom 0.8.0-alpha.1.
4
4
 
5
5
  ```text
6
6
  Expression = a value
@@ -1,6 +1,6 @@
1
1
  # Presentation
2
2
 
3
- Axiom 0.7.0-alpha.2. Presentation is **semantic UX intent**, expressed as data on a UI
3
+ Axiom 0.8.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/RUNTIME.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Runtime
2
2
 
3
- Axiom 0.7.0-alpha.2. The runtime executes an `ApplicationIR`. It is domain-independent: it
3
+ Axiom 0.8.0-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
4
4
  contains no knowledge of any application.
5
5
 
6
6
  ## Constructing
@@ -50,9 +50,15 @@ interface HostEnvironment {
50
50
  uuid(): string;
51
51
  storage?: { read(key): string | null; write(key, value): void };
52
52
  report?(message: string): void;
53
+ queryIntegration?(operationId: string, args, options: { timeoutMs? }): Promise<IntegrationQueryOutcome>;
53
54
  }
54
55
  ```
55
56
 
57
+ `queryIntegration` is only ever called by the authoritative runtime executing an
58
+ `integration-query` operation. A browser host never implements it: no client-compiled
59
+ action ever contains one, because integrations default server-only. See
60
+ [`INTEGRATIONS.md`](INTEGRATIONS.md).
61
+
56
62
  Memory-host helpers for driving and inspecting a rendered tree: `findAll`, `findByNodeId`,
57
63
  `findByTag`, `textOf`, `typeInto`, `toggle`, `click`, `submit`. Every rendered element
58
64
  carries `data-node="<node id>"`.
@@ -72,12 +78,14 @@ carries `data-node="<node id>"`.
72
78
  | `clearDiagnostics()` | — | Empties the running log **and** every recorded action outcome. |
73
79
  | `getActionOutcome(id)` | — | The outcome of that action's most recent invocation. See below. |
74
80
  | `getMutationLog()` | — | Every attempted mutation, with source, path and outcome. |
81
+ | `getEffectIntents()` | — | Every `integration-effect` intent recorded so far — a log distinct from the mutation log, because an effect is not a state mutation. See [`EFFECTS.md`](EFFECTS.md). |
75
82
  | `registerNativeOperation(id, fn)` | — | Registers an implementation for a `native` operation. |
76
- | `invokeActionAsync(id, args?)` | **yes** | Awaits the outcome, including an authority's answer. |
83
+ | `invokeActionAsync(id, args?)` | **yes** | Awaits the outcome, including an authority's answer. For an action with a top-level `integration-query` operation, this is also what awaits the query itself — see [`INTEGRATIONS.md`](INTEGRATIONS.md). |
77
84
  | `syncAuthoritativeState()` | — | Loads the authoritative snapshot and applies it. Idempotent; may be called at any time. See [`AUTHORITY.md`](AUTHORITY.md). |
78
85
  | `authoritativeStateLoaded()` | — | Whether a snapshot has been applied. `false` also after a failed load, which is why it is not the same question as "is this collection empty". |
79
86
  | `settled()` | — | Resolves when no remote invocation is outstanding. An action started by a click or a form submit leaves no promise for a caller to hold; this is how to wait for it without guessing a delay. |
80
87
  | `evaluate(expression)` | — | Evaluates in the root scope, reporting rather than throwing. A pure read. |
88
+ | `evaluateWithBindings(expression, bindings)` | — | Evaluates with extra ids bound in scope, keyed by id. How a trigger's `arguments`/`enabledWhen` resolve `ref()` of the trigger's own id to read an event payload. |
81
89
 
82
90
  ### `hydrateState` bypasses semantic enforcement
83
91
 
@@ -214,6 +222,10 @@ interface RuntimeDiagnostic {
214
222
  | `NATIVE_OPERATION_MISSING` | No implementation registered for an `implementationId`. | `native` operation | — |
215
223
  | `REMOTE_ACTION_UNAVAILABLE` | An action belonging to the authority was invoked with no gateway configured, or the transport failed. | remote invocation | — |
216
224
  | `AUTHORITY_UNREACHABLE` | **Warning.** `start()` could not load authoritative state: no answer from the authority. The page renders with what it has, and `authoritativeStateLoaded()` stays false. | startup | — |
225
+ | `INTEGRATION_UNAVAILABLE` | An `integration-query` operation ran, but the host has no `queryIntegration` capability configured. | `integration-query` | — |
226
+ | `INTEGRATION_TIMEOUT` | An integration query did not answer within its declared `timeoutMs`. | `integration-query` | `operationId` |
227
+ | `INTEGRATION_RESULT_INVALID` | A provider's response did not conform to the operation's declared `resultType`. | `integration-query` | `operationId` |
228
+ | `INTEGRATION_QUERY_FAILED` | An integration query failed for any other reason. Never carries a provider secret. | `integration-query` | `operationId`, `retryable` |
217
229
  | `UI_NODE_MISSING` | A child id that is not a UI node in the IR. | render | — |
218
230
  | `UNSUPPORTED_UI_NODE` | An unknown UI node kind. | render | — |
219
231
  | `PERSISTED_STATE_UNREADABLE` | **Warning.** A stored value could not be parsed; the initial value was used. | startup | — |
@@ -1,6 +1,6 @@
1
1
  # Semantic contract
2
2
 
3
- Axiom 0.7.0-alpha.2. Runtime guarantees, stated formally. This file defines behavior; it
3
+ Axiom 0.8.0-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
4
4
  does not teach. Where this file and any specification in `../specs/` disagree, this file
5
5
  describes the implementation and is authoritative.
6
6
 
package/docs/STATE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # State
2
2
 
3
- Axiom 0.7.0-alpha.2. A `StateDef` is a named application value: stored, or computed from
3
+ Axiom 0.8.0-alpha.1. A `StateDef` is a named application value: stored, or computed from
4
4
  other state.
5
5
 
6
6
  ```ts
@@ -0,0 +1,165 @@
1
+ # Triggers
2
+
3
+ Axiom 0.8.0-alpha.1. A `TriggerDef` says **when** an action should be invoked, without
4
+ embedding callback code. `docs/AUTHORITY.md`
5
+ [§ Triggers](AUTHORITY.md#triggers) is the load-bearing statement of the execution model;
6
+ this file is the vocabulary.
7
+
8
+ ## The model
9
+
10
+ ```ts
11
+ interface TriggerDef {
12
+ id: NodeId;
13
+ kind: 'trigger';
14
+ actionId: NodeId;
15
+ when: TriggerSpec;
16
+ arguments?: Record<string, Expression>;
17
+ enabledWhen?: Expression;
18
+ }
19
+
20
+ type TriggerSpec =
21
+ | { kind: 'interval'; everyMs: number; overlap?: 'skip' | 'queue' }
22
+ | { kind: 'delay'; afterMs: number }
23
+ | { kind: 'lifecycle'; event: 'application-start' | 'runtime-ready' | 'route-enter' | 'route-leave'; routeId?: NodeId }
24
+ | { kind: 'event'; eventId: NodeId };
25
+ ```
26
+
27
+ `TRIGGER_KINDS` is `['interval', 'delay', 'lifecycle', 'event']`. `everyMs`/`afterMs` are
28
+ plain numbers, never expressions — scheduling stays static; only `enabledWhen` is dynamic,
29
+ evaluated each time the trigger would otherwise fire. This is deliberate (spec §62): a
30
+ rapidly-changing interval would be a scheduling platform, which 0.8 is not (spec §58).
31
+
32
+ ## A trigger invokes an action exactly as any other caller does
33
+
34
+ **There is no weaker execution path for a triggered action** (spec §102). The same
35
+ `invoke` pipeline the client's `InvokeRequest` uses runs it: the same argument checking,
36
+ the same authorization evaluation, the same guards, the same transaction, the same
37
+ constraints and transition constraints. A constraint violation from a triggered action
38
+ rolls back exactly like one from a client request does.
39
+
40
+ The one thing that differs is who is asking: a triggered invocation runs under
41
+ `ExecutionContext.principal: null`, `source: 'system'` — no credential is authenticated,
42
+ because there is no caller to authenticate (spec §68). An action whose `.authorization`
43
+ can only be satisfied by a real, authenticated principal is correctly refused when a
44
+ trigger targets it; this is not a special case in the authorization check, it is the
45
+ ordinary one applied to a `null` principal, exactly as an anonymous client request gets.
46
+
47
+ ## Where a trigger executes
48
+
49
+ Derived from where its target action executes, not declared:
50
+
51
+ | `when.kind` | Runs | Because |
52
+ | --- | --- | --- |
53
+ | `interval`, `delay` | server, if the target action is server-authority; otherwise client | either authority can run a timer |
54
+ | `lifecycle: 'application-start' \| 'runtime-ready'` | server | these are authority startup moments |
55
+ | `lifecycle: 'route-enter' \| 'route-leave'` | client | routes are a client-IR concept |
56
+ | `event` | server | only the server dispatches events |
57
+
58
+ `event` triggers targeting a client-authority action, and `route-enter`/`route-leave`
59
+ triggers targeting a server-authority one, are rejected at validation
60
+ (`TRIGGER_WRONG_AUTHORITY`) — the mismatch can never reach a runtime that would silently
61
+ do nothing with it.
62
+
63
+ **Client-authority `interval`/`delay`/`route-enter`/`route-leave` triggers compile into
64
+ `ApplicationIR.triggers` for inspection, but the browser runtime does not yet schedule or
65
+ execute them.** Only the authoritative runtime does, today. See [Not in
66
+ 0.8.0](AUTHORITY.md#not-in-080).
67
+
68
+ ## Interval semantics
69
+
70
+ ```ts
71
+ { kind: 'interval', everyMs: 5000, overlap?: 'skip' | 'queue' } // overlap defaults to 'skip'
72
+ ```
73
+
74
+ - First execution is `everyMs` after the trigger runtime starts — there is no
75
+ fire-immediately option.
76
+ - `overlap: 'skip'` (default): a tick that fires while the previous invocation of the same
77
+ trigger is still running is discarded — reported as `TRIGGER_OVERLAP_SKIPPED` — never
78
+ queued and never run concurrently with it.
79
+ - `overlap: 'queue'`: one pending tick runs immediately after the in-flight one finishes.
80
+ Never more than one queued at a time.
81
+ - A failed invocation (a refused guard, a rolled-back constraint) is reported the ordinary
82
+ way and does not cancel the schedule; the next tick still fires.
83
+
84
+ ## Lifecycle triggers and startup order
85
+
86
+ `application-start` and `runtime-ready` triggers fire once, in that order, as the last
87
+ step of `AxiomServer.start()` — after persistence has loaded, effect resumption has begun,
88
+ and every required integration adapter has been validated present, and before any request
89
+ is accepted:
90
+
91
+ ```text
92
+ load IR → initialize persistence → restore state → initialize adapters →
93
+ validate required capabilities → runtime-ready triggers run → accept requests
94
+ ```
95
+
96
+ (`application-start` triggers run first, ahead of `runtime-ready`, within that same step.)
97
+
98
+ ## Event triggers and the payload scope
99
+
100
+ ```ts
101
+ { kind: 'event', eventId: EVENT_DEVICE_STATUS_CHANGED }
102
+ ```
103
+
104
+ An `event`-kind trigger's `arguments`/`enabledWhen` may `ref` **the trigger's own id** to
105
+ read the event's payload — the same mechanism a `for-each`/`map`'s `scopeId` provides,
106
+ except the whole payload is bound, not a collection member:
107
+
108
+ ```ts
109
+ graph.addNode<TriggerDef>({
110
+ id: TRIGGER_STATUS_CHANGED,
111
+ kind: 'trigger',
112
+ actionId: ACTION_APPLY_STATUS,
113
+ when: { kind: 'event', eventId: EVENT_DEVICE_STATUS_CHANGED },
114
+ arguments: { [String(PARAM_STATUS)]: ref(TRIGGER_STATUS_CHANGED) },
115
+ });
116
+ ```
117
+
118
+ Full event semantics: [`EVENTS.md`](EVENTS.md).
119
+
120
+ ## Enablement
121
+
122
+ ```ts
123
+ enabledWhen?: Expression
124
+ ```
125
+
126
+ Evaluated each time the trigger would otherwise fire; a `false`/unevaluable result means
127
+ this occurrence does not run, but the schedule continues. This is the mechanism for
128
+ "poll only while the integration is enabled" (spec §61) — dynamic enablement, never a
129
+ dynamic interval.
130
+
131
+ ## A deterministic test clock
132
+
133
+ ```ts
134
+ const host = createDeterministicServerHost();
135
+ const server = createAxiomServer({ ir, host, integrations });
136
+ await server.start();
137
+
138
+ host.advance(5000); // fires every timer due within the next 5000ms, in due-time order
139
+ ```
140
+
141
+ No test verifying interval/delay behavior needs to wait on a real second (spec §85,141).
142
+ `advance(ms)` fires every host timer that becomes due, re-scheduling intervals, in the
143
+ order they would fire on a real clock.
144
+
145
+ ## Validation
146
+
147
+ | Code | Raised when |
148
+ | --- | --- |
149
+ | `TRIGGER_ACTION_NOT_FOUND` | `actionId` does not resolve to an action. |
150
+ | `TRIGGER_INTERVAL_NOT_POSITIVE` | `everyMs`/`afterMs` is not a positive number. |
151
+ | `UNKNOWN_EVENT` | An `event` trigger's `eventId` does not resolve to an `EventDef`. |
152
+ | `TRIGGER_WRONG_AUTHORITY` | An authority mismatch between the trigger kind and its target action, described above. |
153
+
154
+ Full table: [`VALIDATION.md`](VALIDATION.md#integrations-effects-triggers-and-events).
155
+
156
+ ## AgentAPI
157
+
158
+ ```ts
159
+ agent.getTriggersForAction(actionId); // TriggerDef[]
160
+ agent.getTimedTriggers(); // TriggerDef[] — interval and delay only
161
+ agent.getActionsTriggeredByEvent(eventId); // ActionDef[]
162
+ ```
163
+
164
+ "What runs automatically" and "what happens every 5 seconds" (spec §78) are answerable
165
+ without reading source.
package/docs/UI.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # UI
2
2
 
3
- Axiom 0.7.0-alpha.2. Eleven semantic UI node kinds describe **what exists and what it does**.
3
+ Axiom 0.8.0-alpha.1. Eleven semantic UI node kinds describe **what exists and what it does**.
4
4
  How it looks is [presentation](PRESENTATION.md).
5
5
 
6
6
  All eleven share `UIBase`:
@@ -1,6 +1,6 @@
1
1
  # Validation
2
2
 
3
- Axiom 0.7.0-alpha.2. Validation is authoring-time structural checking. It is not the same
3
+ Axiom 0.8.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
- 55 codes, exported as `VALIDATION_CODES`. Every one is reachable.
39
+ 72 codes, exported as `VALIDATION_CODES`. Every one is reachable.
40
40
 
41
41
  ### Ids and references
42
42
 
@@ -115,6 +115,22 @@ Recursive, with a `path` naming the position.
115
115
  | `MISSING_IDENTITY_FIELD` | A transition constraint on an entity that declares no `identityFieldId`. |
116
116
  | `UNSUPPORTED_CONSTRAINT_SCOPE` | A constraint scope the runtime cannot evaluate. |
117
117
 
118
+ ### Integrations, effects, triggers and events
119
+
120
+ Full model: [`INTEGRATIONS.md`](INTEGRATIONS.md), [`EFFECTS.md`](EFFECTS.md),
121
+ [`TRIGGERS.md`](TRIGGERS.md), [`EVENTS.md`](EVENTS.md).
122
+
123
+ | Code | Raised when |
124
+ | --- | --- |
125
+ | `UNKNOWN_INTEGRATION` | An `IntegrationOperationDef.integrationId` that does not resolve to an `integration` node. |
126
+ | `UNKNOWN_INTEGRATION_OPERATION` | An `integration-query`/`integration-effect` operation's `operationId` that does not resolve to an `integration-operation` node. |
127
+ | `INTEGRATION_OPERATION_MODE_MISMATCH` | An `integration-query` naming an operation whose `mode` is `'effect'`, or an `integration-effect` naming one whose `mode` is `'query'`. |
128
+ | `INTEGRATION_ARGUMENT_MISMATCH` | A missing required argument to an integration operation, or an argument it declares no parameter for. |
129
+ | `TRIGGER_ACTION_NOT_FOUND` | A `TriggerDef.actionId` that does not resolve to an action. |
130
+ | `TRIGGER_INTERVAL_NOT_POSITIVE` | An `interval` trigger's `everyMs`, or a `delay` trigger's `afterMs`, that is not a positive number. |
131
+ | `UNKNOWN_EVENT` | An event id that does not resolve to an `EventDef` — a trigger's `eventId`, or an `integration-effect`'s `succeededEventId`/`failedEventId`. |
132
+ | `TRIGGER_WRONG_AUTHORITY` | An `event` trigger targeting a client-authority action (only the server dispatches events), or a `route-enter`/`route-leave` trigger targeting a server-authority one (only the client router dispatches those). |
133
+
118
134
  ### UI and routing
119
135
 
120
136
  | Code | Raised when | Severity |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom",
3
- "version": "0.7.0-alpha.2",
3
+ "version": "0.8.0-alpha.1",
4
4
  "description": "AI-native semantic web application framework.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",
@@ -32,10 +32,10 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "@cynodia/axiom-core": "0.7.0-alpha.2",
36
- "@cynodia/axiom-runtime": "0.7.0-alpha.2",
37
- "@cynodia/axiom-compiler": "0.7.0-alpha.2",
38
- "@cynodia/axiom-agent-api": "0.7.0-alpha.2"
35
+ "@cynodia/axiom-core": "0.8.0-alpha.1",
36
+ "@cynodia/axiom-runtime": "0.8.0-alpha.1",
37
+ "@cynodia/axiom-compiler": "0.8.0-alpha.1",
38
+ "@cynodia/axiom-agent-api": "0.8.0-alpha.1"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b tsconfig.json"