@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 +2 -2
- package/docs/ACTIONS_TRANSACTIONS.md +33 -2
- package/docs/AGENT_API.md +1 -1
- package/docs/AGENT_REFERENCE.md +61 -6
- package/docs/ANTI_PATTERNS.md +6 -3
- package/docs/AUTHORITY.md +177 -13
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EFFECTS.md +171 -0
- package/docs/EVENTS.md +119 -0
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +2 -2
- package/docs/INTEGRATIONS.md +171 -0
- package/docs/LOCATIONS.md +1 -1
- package/docs/PRESENTATION.md +1 -1
- package/docs/RUNTIME.md +14 -2
- package/docs/SEMANTIC_CONTRACT.md +1 -1
- package/docs/STATE.md +1 -1
- package/docs/TRIGGERS.md +203 -0
- package/docs/UI.md +1 -1
- package/docs/VALIDATION.md +21 -2
- package/package.json +5 -5
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
|
|
10
|
-
|
|
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.
|
|
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
|
-
|
|
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
package/docs/AGENT_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent reference
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
`
|
|
604
|
-
and `REMOTE_ACTION_UNAVAILABLE` on the
|
|
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
|
|
package/docs/ANTI_PATTERNS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-patterns
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
-
|
|
394
|
-
|
|
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.
|
|
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',
|
|
318
|
-
{ kind: 'snapshot',
|
|
319
|
-
{ kind: 'error',
|
|
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
|
|
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 `
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
- **
|
|
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
|
|
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).
|