@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 +2 -2
- package/docs/ACTIONS_TRANSACTIONS.md +33 -2
- package/docs/AGENT_API.md +1 -1
- package/docs/AGENT_REFERENCE.md +55 -4
- package/docs/ANTI_PATTERNS.md +1 -1
- package/docs/AUTHORITY.md +134 -13
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EFFECTS.md +142 -0
- package/docs/EVENTS.md +115 -0
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +2 -2
- package/docs/INTEGRATIONS.md +152 -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 +165 -0
- package/docs/UI.md +1 -1
- package/docs/VALIDATION.md +18 -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.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
|
-
|
|
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.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.
|
|
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,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
|
```
|
package/docs/ANTI_PATTERNS.md
CHANGED
package/docs/AUTHORITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Authority
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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',
|
|
318
|
-
{ kind: 'snapshot',
|
|
319
|
-
{ kind: 'error',
|
|
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
|
|
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 `
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
- **
|
|
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
|
|
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).
|
package/docs/CONSTRAINTS.md
CHANGED
package/docs/EFFECTS.md
ADDED
|
@@ -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.
|
package/docs/EXPRESSIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Expressions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|
package/docs/GRAPH_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Graph model
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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
package/docs/PRESENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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.
|
|
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
package/docs/TRIGGERS.md
ADDED
|
@@ -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.
|
|
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`:
|
package/docs/VALIDATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Validation
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
36
|
-
"@cynodia/axiom-runtime": "0.
|
|
37
|
-
"@cynodia/axiom-compiler": "0.
|
|
38
|
-
"@cynodia/axiom-agent-api": "0.
|
|
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"
|