@cynodia/axiom 0.8.0-alpha.1 → 0.8.2-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/docs/ACTIONS_TRANSACTIONS.md +1 -1
- package/docs/AGENT_API.md +1 -1
- package/docs/AGENT_REFERENCE.md +56 -11
- package/docs/ANTI_PATTERNS.md +6 -3
- package/docs/AUTHORITY.md +95 -13
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EFFECTS.md +162 -6
- package/docs/EVENTS.md +18 -4
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +2 -2
- package/docs/INTEGRATIONS.md +41 -4
- package/docs/LOCATIONS.md +1 -1
- package/docs/PRESENTATION.md +1 -1
- package/docs/RUNTIME.md +1 -1
- package/docs/SEMANTIC_CONTRACT.md +1 -1
- package/docs/STATE.md +1 -1
- package/docs/TRIGGERS.md +54 -6
- package/docs/UI.md +1 -1
- package/docs/VALIDATION.md +4 -1
- package/package.json +5 -5
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.8.
|
|
3
|
+
Axiom 0.8.2-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.8.
|
|
34
|
+
const graph = new ApplicationGraph(id, name); // version defaults to '0.8.2'
|
|
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
|
|
@@ -496,6 +496,22 @@ if (!result.ok) {
|
|
|
496
496
|
warnings never make a graph invalid. 72 codes in `VALIDATION_CODES`, grouped in
|
|
497
497
|
[`VALIDATION.md`](VALIDATION.md).
|
|
498
498
|
|
|
499
|
+
**`validateGraph(graph)` with no options is target-neutral by design** (spec 8.2 §2-4): a
|
|
500
|
+
graph is never rejected for a renderer or trigger runtime nobody named. This means a bare
|
|
501
|
+
`validateGraph(graph).valid === true` does **not** by itself guarantee the graph is
|
|
502
|
+
executable by the browser client — a UI node kind no renderer implements, or a
|
|
503
|
+
client-authority trigger kind the browser trigger runtime does not execute
|
|
504
|
+
(`CLIENT_TRIGGER_UNSUPPORTED`), both validate silently under the no-options call. Use
|
|
505
|
+
`validateForBrowser(graph)` (`@cynodia/axiom-compiler`) for a validate-only check against
|
|
506
|
+
real browser capabilities, or call `compileToIR(graph)` directly — it applies those same
|
|
507
|
+
capabilities and throws `GraphValidationError` on exactly what `validateForBrowser` would
|
|
508
|
+
report as an error. See [Renderability](#renderability) below for the parallel UI-kind gate.
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
validateGraph(graph).valid; // target-neutral: accepts every UI kind and trigger kind
|
|
512
|
+
validateForBrowser(graph).valid; // browser-real: same as compileToIR's validation step
|
|
513
|
+
```
|
|
514
|
+
|
|
499
515
|
## Agent API
|
|
500
516
|
|
|
501
517
|
```ts
|
|
@@ -605,12 +621,13 @@ before authoring an application that reaches an external system or reacts to tim
|
|
|
605
621
|
event.
|
|
606
622
|
|
|
607
623
|
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.
|
|
624
|
+
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).
|
|
625
|
+
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
626
|
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.
|
|
627
|
+
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
628
|
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
629
|
7. **SECRET INVARIANT** — integration credentials live in host configuration (`AxiomServerOptions.integrations`), never in `ApplicationGraph`.
|
|
630
|
+
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).
|
|
614
631
|
|
|
615
632
|
```ts
|
|
616
633
|
{ kind: 'integration', id: INTEGRATION_DEVICE_PROVIDER }
|
|
@@ -630,29 +647,44 @@ event.
|
|
|
630
647
|
|
|
631
648
|
- `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
649
|
- 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.
|
|
650
|
+
- 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.
|
|
634
651
|
- `createDeterministicServerHost().advance(ms)` fires due timers deterministically; no trigger test waits on a real clock.
|
|
635
652
|
|
|
636
653
|
```ts
|
|
637
654
|
agent.listIntegrations() / agent.listIntegrationOperations(id?);
|
|
638
655
|
agent.getActionsUsingIntegration(id) / agent.getEffectsForAction(actionId);
|
|
639
656
|
agent.getTriggersForAction(actionId) / agent.getTimedTriggers();
|
|
640
|
-
agent.getActionsTriggeredByEvent(eventId) / agent.
|
|
657
|
+
agent.getActionsTriggeredByEvent(eventId) / agent.getTriggeredEvents(); // graph-static: has a bound trigger, not "was delivered"
|
|
641
658
|
agent.getExternalDependencies(); // { integrations, operations } — the deployment manifest
|
|
659
|
+
agent.getSystemOnlyActions() / agent.getTriggersTargetingClientOnlyActions();
|
|
660
|
+
agent.isClientInvocable(actionId) / agent.isSystemOnly(actionId);
|
|
642
661
|
```
|
|
643
662
|
|
|
644
663
|
Portable artifacts, for a runtime written in another language:
|
|
645
664
|
|
|
646
665
|
```
|
|
647
|
-
@cynodia/axiom-server/conformance the fixture manifest
|
|
666
|
+
@cynodia/axiom-server/conformance the fixture manifest (fixture.format, below)
|
|
648
667
|
@cynodia/axiom-server/conformance/<name>.json one fixture, pure data
|
|
649
|
-
@cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for
|
|
668
|
+
@cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for axiom.server.v1 (frozen)
|
|
669
|
+
@cynodia/axiom-server/schema/server-ir.v2.schema.json JSON Schema for axiom.server.v2
|
|
670
|
+
@cynodia/axiom-server/schema/server-ir.v3.schema.json JSON Schema for axiom.server.v3
|
|
671
|
+
@cynodia/axiom-server/schema/server-ir.v4.schema.json JSON Schema for axiom.server.v4 (latest)
|
|
650
672
|
@cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
|
|
651
673
|
```
|
|
652
674
|
|
|
675
|
+
This list is generated/tested content, not hand-maintained prose: `packages/demo/test
|
|
676
|
+
/documentation.test.ts` fails if a shipped `schema/*.json` file is missing from it or a
|
|
677
|
+
listed file no longer exists (spec 8.2 §43-45). `runConformanceFixture`/
|
|
678
|
+
`runConformanceSuite` (`@cynodia/axiom-server`) are the public runner over that fixture
|
|
679
|
+
format — see [`AUTHORITY.md`](AUTHORITY.md#conformance) for `fixture.conformance` (the
|
|
680
|
+
fixture-format version) vs `fixture.serverIR.contract` (the per-fixture Server IR contract)
|
|
681
|
+
vs the manifest's own `conformance`/`protocol`/`release` fields — three separate concepts,
|
|
682
|
+
never conflated under one "contract" name (spec 8.2 §9-10).
|
|
683
|
+
|
|
653
684
|
Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZATION_DENIED`
|
|
654
|
-
`
|
|
655
|
-
and `REMOTE_ACTION_UNAVAILABLE` on the
|
|
685
|
+
`INVOCATION_SOURCE_NOT_ALLOWED` `CONCURRENCY_CONFLICT` `MALFORMED_REQUEST`
|
|
686
|
+
`AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE` and `REMOTE_ACTION_UNAVAILABLE` on the
|
|
687
|
+
client.
|
|
656
688
|
|
|
657
689
|
## Metadata classes
|
|
658
690
|
|
|
@@ -679,6 +711,19 @@ Capabilities describe **node-kind** support, not partial support: a renderer can
|
|
|
679
711
|
that it draws a kind but not one of its options. Nothing in the current vocabulary needs that,
|
|
680
712
|
and it is a deliberate future extension rather than an oversight.
|
|
681
713
|
|
|
714
|
+
The same gate exists for client-authority triggers, since the browser trigger runtime
|
|
715
|
+
implements no `TriggerSpec.kind` at all:
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
validateGraph(graph, { triggerRuntime: BROWSER_TRIGGER_CAPABILITIES }); // CLIENT_TRIGGER_UNSUPPORTED
|
|
719
|
+
compileToIR(graph); // applies it by default
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
`CLIENT_TRIGGER_UNSUPPORTED`'s message states the remediation, not only the refusal: move
|
|
723
|
+
the trigger's target action to server authority, or compile for a trigger runtime that
|
|
724
|
+
publishes the kind (`compileToIR(graph, { triggerRuntime })`). Full model:
|
|
725
|
+
[`TRIGGERS.md`](TRIGGERS.md#where-a-trigger-executes).
|
|
726
|
+
|
|
682
727
|
## Named expressions
|
|
683
728
|
|
|
684
729
|
```ts
|
package/docs/ANTI_PATTERNS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-patterns
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-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.8.
|
|
3
|
+
Axiom 0.8.2-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
|
|
@@ -39,6 +39,7 @@ describes both halves, so there is no backend to write.
|
|
|
39
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
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
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).
|
|
42
43
|
|
|
43
44
|
## Authority and persistence are different questions
|
|
44
45
|
|
|
@@ -88,8 +89,49 @@ claim that validation already happened.
|
|
|
88
89
|
| Forge an action id | resolved from the authority's own IR → `UNKNOWN_SERVER_ACTION` |
|
|
89
90
|
| Send an argument of the wrong shape | checked against the declared type → `ARGUMENT_TYPE_MISMATCH` |
|
|
90
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 |
|
|
91
94
|
| Read server-only state | it is not in the client IR, the snapshot or any answer |
|
|
92
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
|
+
|
|
93
135
|
## Server IR
|
|
94
136
|
|
|
95
137
|
```ts
|
|
@@ -435,6 +477,7 @@ does for a local failure.
|
|
|
435
477
|
| `INTEGRATION_ADAPTER_MISSING` | The Server IR requires an integration with no registered adapter — refused at `start()`, never deferred to first invocation. |
|
|
436
478
|
| `EVENT_DISPATCH_DEPTH_EXCEEDED` | An event → action → effect → event chain was stopped before it could recurse unboundedly. |
|
|
437
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. |
|
|
438
481
|
|
|
439
482
|
Two client-side codes belong to the boundary as well:
|
|
440
483
|
|
|
@@ -506,6 +549,26 @@ Running them requires no part of this implementation. That is the point: the Ser
|
|
|
506
549
|
specification plus these fixtures are the whole contract, so an independent runtime in
|
|
507
550
|
another language can be held to exactly the same standard.
|
|
508
551
|
|
|
552
|
+
**A public reference runner is exported for the TypeScript reference runtime itself**
|
|
553
|
+
(spec 8.2 §14-16):
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
import { runConformanceFixture, runConformanceSuite } from '@cynodia/axiom-server';
|
|
557
|
+
|
|
558
|
+
const result = await runConformanceFixture(fixtureJson); // { name, ok, failures[] }
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
It imports only `@cynodia/axiom-server` and fixture data — no graph, no compiler, no
|
|
562
|
+
builder — and reports structured pass/fail rather than throwing on an ordinary fixture
|
|
563
|
+
mismatch. `packages/server/test/conformance.test.ts` runs every shipped fixture through
|
|
564
|
+
this exact function, and `npm run conformance:run` (`scripts/run-conformance.mjs`) runs the
|
|
565
|
+
whole suite from a standalone script using nothing but this public API and the manifest —
|
|
566
|
+
the "held to the same standard by an outside caller" claim, demonstrated rather than
|
|
567
|
+
asserted. The fixture **model** (`ConformanceFixture` and friends, `conformance-types.ts`)
|
|
568
|
+
is deliberately kept separate from this **adapter**
|
|
569
|
+
(`conformance-runner.ts`): a non-TypeScript implementation needs only the model's shape and
|
|
570
|
+
the semantics on this page, never this file.
|
|
571
|
+
|
|
509
572
|
Enumerate the suite from its manifest rather than by listing a directory:
|
|
510
573
|
|
|
511
574
|
```
|
|
@@ -513,12 +576,24 @@ Enumerate the suite from its manifest rather than by listing a directory:
|
|
|
513
576
|
@cynodia/axiom-server/conformance/<name>.json → one fixture
|
|
514
577
|
```
|
|
515
578
|
|
|
516
|
-
The manifest
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
the
|
|
579
|
+
The manifest carries three **separate, non-overlapping** version concepts (spec 8.2 §9-10;
|
|
580
|
+
do not conflate them under one "contract" name):
|
|
581
|
+
|
|
582
|
+
| Field | Means |
|
|
583
|
+
| --- | --- |
|
|
584
|
+
| `manifest.conformance` | The **fixture-format** version (`axiom.conformance.v1`/`v2`) — the shape of the JSON documents themselves: what top-level keys a fixture may have (`invocations` vs `steps`/`externalAdapters`, and so on). |
|
|
585
|
+
| `manifest.baseContract` | The **oldest** Server IR contract any fixture in this manifest may use — `axiom.server.v1`, always, since new fixtures are added without ever raising this floor. It does **not** describe what the newest fixture needs. |
|
|
586
|
+
| `manifest.fixtures[].contract` | The Server IR contract **that specific fixture** requires — this is what is authoritative for what running it needs. The suite ships fixtures spanning `v1` through `v4` simultaneously, each correctly labelled. |
|
|
587
|
+
| `manifest.release` | The `@cynodia/axiom` package version this snapshot of the suite shipped with — unrelated to either version above. |
|
|
588
|
+
|
|
589
|
+
Before 8.2 the manifest's top-level field was named `contract` and fixed at
|
|
590
|
+
`axiom.server.v1`, which read as a claim about the whole suite even after `v3`/`v4`
|
|
591
|
+
fixtures were added; it is now named `baseContract` with the meaning above, precisely
|
|
592
|
+
because per-fixture `contract` is what is actually authoritative. A runtime that does not
|
|
593
|
+
implement a contract a fixture names should refuse that fixture rather than discover the
|
|
594
|
+
mismatch mid-assertion. The files are plain JSON in a documented package directory, so a
|
|
595
|
+
non-JavaScript consumer can read them straight out of the tarball without Node's module
|
|
596
|
+
resolver.
|
|
522
597
|
|
|
523
598
|
Every fixture is executed against the reference runtime by this repository's own test suite,
|
|
524
599
|
and its expectations are exhaustive — a fixture that says which states changed must name all
|
|
@@ -530,6 +605,7 @@ of them and no others. No fixture is permitted to disagree with the shipped runt
|
|
|
530
605
|
@cynodia/axiom-server/schema/server-ir.v1.schema.json
|
|
531
606
|
@cynodia/axiom-server/schema/server-ir.v2.schema.json
|
|
532
607
|
@cynodia/axiom-server/schema/server-ir.v3.schema.json
|
|
608
|
+
@cynodia/axiom-server/schema/server-ir.v4.schema.json
|
|
533
609
|
@cynodia/axiom-server/schema/protocol.v1.schema.json
|
|
534
610
|
```
|
|
535
611
|
|
|
@@ -549,15 +625,21 @@ page plus the conformance fixtures.
|
|
|
549
625
|
| `axiom.server.v1` | the frozen 0.6.1 contract, below | 0.6.1 |
|
|
550
626
|
| `axiom.server.v2` | the expression kinds `group` and `expression-ref`, and the `expressionDefs` they resolve against | 0.7.0 |
|
|
551
627
|
| `axiom.server.v3` | integrations, integration operations, events, triggers, and the `integration-query`/`integration-effect` operation kinds | 0.8.0 |
|
|
628
|
+
| `axiom.server.v4` | `ActionDef.invocation.allowedSources` invocation-source restriction, and the structured effect-outcome envelope (`effectOutcomeEntity`, `EFFECT_ID_FIELD` and its sibling reserved fields) that every effect dispatch uses from 8.1 onward | 0.8.1 |
|
|
552
629
|
|
|
553
|
-
`SERVER_IR_CONTRACTS` enumerates all
|
|
630
|
+
`SERVER_IR_CONTRACTS` enumerates all four, and is the single source of truth this table is
|
|
631
|
+
tested against — `packages/demo/test/documentation.test.ts` fails if a contract in
|
|
632
|
+
`SERVER_IR_CONTRACTS` has no row here, or a row here names a contract the code does not
|
|
633
|
+
declare (spec 8.2 §7-8). The rules:
|
|
554
634
|
|
|
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.
|
|
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.
|
|
557
|
-
- **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref`, `expressionDefs`, an integration, a trigger, an event,
|
|
635
|
+
- **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. `usesV4Semantics` computes the v4 case specifically: an action's `invocation.allowedSources` genuinely restricting the default two-source set, or any `integration-operation` with `mode: 'effect'` (since every effect dispatch uses the structured v4 envelope) — a document that merely mentions `invocation` without restricting it only needs `axiom.server.v2`, the same tier `group`/`expression-ref` occupy.
|
|
636
|
+
- **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 — including refusing a document that **understates** its own contract (`understatedContract`).
|
|
637
|
+
- **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref`, `expressionDefs`, an integration, a trigger, an event, the `integration-query`/`integration-effect` operation kinds, `invocation`, or the structured effect-outcome envelope, and `server-ir.v1.schema.json` is byte-frozen. Vocabulary arrives under a new identifier or not at all.
|
|
638
|
+
- **`axiom.server.v4` is the latest contract as of 0.8.2** (`SERVER_IR_LATEST_CONTRACT`). 0.8.2 is polish-only — documentation, effect observability timing, fixture coverage and AgentAPI aliasing — and introduces no incompatible IR vocabulary change, so no `axiom.server.v5` was created (spec 8.2 §55-56).
|
|
558
639
|
|
|
559
640
|
There is one JSON Schema per contract, each generated from the runtime's own vocabulary and
|
|
560
|
-
each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json
|
|
641
|
+
each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json`,
|
|
642
|
+
`server-ir.v4.schema.json`.
|
|
561
643
|
|
|
562
644
|
**`group`.** Partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`. Groups appear
|
|
563
645
|
in the order their key was **first seen** in the source; members keep source order; two keys are
|
|
@@ -725,7 +807,7 @@ Stated plainly rather than left to discovery:
|
|
|
725
807
|
- **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.
|
|
726
808
|
- **Read authorization per caller or per record.** Visibility is per state.
|
|
727
809
|
- **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.**
|
|
810
|
+
- **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.
|
|
729
811
|
- **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.
|
|
730
812
|
- **Realtime synchronization**, subscriptions and collaboration. Request/response only.
|
|
731
813
|
- **Query semantics.** Authoritative collections are loaded into runtime state; large-data querying needs its own design.
|
package/docs/CONSTRAINTS.md
CHANGED
package/docs/EFFECTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Effects
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-alpha.1. External effects are not rollback-capable state mutations. This file
|
|
4
4
|
is the delivery model; [`AUTHORITY.md`](AUTHORITY.md#external-effects) is the load-bearing
|
|
5
5
|
statement of why, and [`INTEGRATIONS.md`](INTEGRATIONS.md) is the operation vocabulary this
|
|
6
6
|
builds on.
|
|
@@ -79,6 +79,22 @@ doubles it each time. The wait uses the host's own scheduling
|
|
|
79
79
|
(`ServerHost.scheduleOnce`), so a test can drive it with `createDeterministicServerHost()`
|
|
80
80
|
+ `advance(ms)` and never wait on a real clock.
|
|
81
81
|
|
|
82
|
+
**`IntegrationFailure.retryable: false` is control flow, not metadata.** It stops the
|
|
83
|
+
remaining retry policy immediately, regardless of `maxAttempts` — an adapter that always
|
|
84
|
+
answers `retryable: false` never retries at all, whatever `policy` says (spec 8.1 §73).
|
|
85
|
+
|
|
86
|
+
**The three states of `retryable` (spec 8.2 §38-39):**
|
|
87
|
+
|
|
88
|
+
| `retryable` | Meaning | Effect on the declared policy |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `false` | The adapter determined a retry cannot succeed. | Stops immediately — the remaining `maxAttempts` are never spent. |
|
|
91
|
+
| `true` | The adapter determined a retry may succeed. | Continues — the next attempt runs after the policy's delay. |
|
|
92
|
+
| absent | The adapter could not determine retryability either way. | Continues, exactly as `true` — an unknown answer is not treated as a refusal. |
|
|
93
|
+
|
|
94
|
+
`packages/server/test/effect-retry.test.ts` is the regression coverage for all three,
|
|
95
|
+
including a genuine multi-attempt retry sequence with a stable `idempotencyKey` across
|
|
96
|
+
every attempt.
|
|
97
|
+
|
|
82
98
|
## Effect status and observability
|
|
83
99
|
|
|
84
100
|
```ts
|
|
@@ -102,21 +118,161 @@ mutation, and mixing the two would misrepresent what actually happened (spec §7
|
|
|
102
118
|
`report()` events cover the whole lifecycle: `effect-requested`, `effect-attempted`,
|
|
103
119
|
`effect-succeeded`, `effect-failed`.
|
|
104
120
|
|
|
121
|
+
**Exact status semantics (spec 8.2 §17-21):**
|
|
122
|
+
|
|
123
|
+
| Status | Means |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `pending` | A durable effect intent exists (committed in the outbox), but no attempt is currently executing — either dispatch has not started yet, or it is waiting between retry attempts. |
|
|
126
|
+
| `running` | An adapter attempt has been started and has not yet settled. Set **before** `IntegrationAdapter.effect(...)` is called, synchronously with the durable `attempts` increment, so `effectLog()`'s public view reflects reality rather than lagging behind it. |
|
|
127
|
+
| `succeeded` | An attempt returned `{ ok: true }`. Terminal. |
|
|
128
|
+
| `failed` | Every attempt the retry policy allows was exhausted, or one returned `retryable: false`. Terminal. |
|
|
129
|
+
|
|
130
|
+
Before 8.2, `running` existed in the type but nothing updated `AxiomServer`'s in-memory
|
|
131
|
+
`effectLog()` view when an attempt actually started — a hung adapter call was
|
|
132
|
+
indistinguishable from an effect nobody had dispatched yet, both showing `status: 'pending',
|
|
133
|
+
attempts: 0` forever even though the adapter had genuinely been invoked. `attempts` counts
|
|
134
|
+
**invocation attempts started**, not merely attempts that settled — it increments at the
|
|
135
|
+
same moment `status` moves to `'running'`, before the adapter is called, so a hung attempt
|
|
136
|
+
is still counted. `packages/server/test/integrations.test.ts`'s hung-effect regression is
|
|
137
|
+
the test for this.
|
|
138
|
+
|
|
139
|
+
**Restart semantics for a `running` effect (spec 8.2 §20).** A record persisted as
|
|
140
|
+
`'running'` when the authority restarts means a previous process called the adapter and was
|
|
141
|
+
never told the outcome — that attempt is unaccounted for, not spent. It is treated as
|
|
142
|
+
resumable/unknown outstanding work: the resumed dispatch gets a **fresh** full retry budget
|
|
143
|
+
(local attempt count restarts at zero; the persisted `attempts` total still carries forward
|
|
144
|
+
honestly across the restart) rather than silently going idle at zero remaining attempts.
|
|
145
|
+
Axiom never claims to know whether the old process's call actually reached the provider —
|
|
146
|
+
only that at-least-once redelivery, paired with `idempotencyKey`, is what makes resuming it
|
|
147
|
+
safe.
|
|
148
|
+
|
|
149
|
+
**No runtime-enforced effect timeout in 0.8.2.** Unlike an `integration-query`'s
|
|
150
|
+
`timeoutMs` (deadline enforced by the runtime itself — see [`INTEGRATIONS.md`](INTEGRATIONS.md#timeout)),
|
|
151
|
+
a `running` effect attempt has no deadline: a non-cooperating adapter call can remain
|
|
152
|
+
`running` indefinitely, and nothing in this release will time it out or reclassify it. This
|
|
153
|
+
is a deliberate scope boundary, not an oversight (spec 8.2 §24) — see the note below for why.
|
|
154
|
+
|
|
155
|
+
**Research note: effect timeout is deferred, not merely unbuilt (spec 8.2 §25).** A future
|
|
156
|
+
effect timeout cannot safely map a timed-out attempt directly to `failed`: unlike a query,
|
|
157
|
+
an effect may have genuinely reached the provider and caused the external side effect
|
|
158
|
+
before the response was lost — declaring it `failed` could make an idempotent caller retry
|
|
159
|
+
a side effect that already happened once, and declaring it `succeeded` could be simply
|
|
160
|
+
wrong. The honest state that later work would need is something like `unknown` — distinct
|
|
161
|
+
from both terminal states — but that state is deliberately **not** introduced in 0.8.2. Do
|
|
162
|
+
not add a deadline just because `running` is now observable; effect timeout remains a
|
|
163
|
+
distinct future design topic.
|
|
164
|
+
|
|
105
165
|
## The result reaches an action only through an event
|
|
106
166
|
|
|
107
167
|
An effect's outcome is never folded back into the transaction that requested it. Instead,
|
|
108
168
|
`succeededEventId`/`failedEventId` — ordinary `EventDef` nodes — are dispatched through
|
|
109
169
|
the same event pipeline an external webhook uses (see [`EVENTS.md`](EVENTS.md)), once the
|
|
110
|
-
outcome is known:
|
|
170
|
+
outcome is known, as a **structured envelope** (spec 8.1 §37-41):
|
|
111
171
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
172
|
+
```ts
|
|
173
|
+
import { EFFECT_ID_FIELD, EFFECT_OPERATION_ID_FIELD, EFFECT_RESULT_FIELD, effectOutcomeEntity } from '@cynodia/axiom-core';
|
|
174
|
+
|
|
175
|
+
graph.addNode(effectOutcomeEntity(ENTITY_EFFECT_OUTCOME, primitiveType('string'))); // the operation's resultType
|
|
176
|
+
graph.addNode<EventDef>({ id: EVENT_SUCCEEDED, kind: 'event', payloadType: entityType(ENTITY_EFFECT_OUTCOME) });
|
|
177
|
+
graph.addNode<EventDef>({ id: EVENT_FAILED, kind: 'event', payloadType: entityType(ENTITY_EFFECT_OUTCOME) });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
One shape covers both outcomes — field ids are graph-global, so two entities could not both
|
|
181
|
+
declare `effectId`/`operationId`/`integrationId` without colliding:
|
|
182
|
+
|
|
183
|
+
| Field | Present on success | Present on failure |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `EFFECT_ID_FIELD` | always | always |
|
|
186
|
+
| `EFFECT_INTEGRATION_ID_FIELD` | always | always |
|
|
187
|
+
| `EFFECT_OPERATION_ID_FIELD` | always | always |
|
|
188
|
+
| `EFFECT_IDEMPOTENCY_KEY_FIELD` | when declared | when declared |
|
|
189
|
+
| `EFFECT_CORRELATION_ID_FIELD` | when the action's transaction has one | when the action's transaction has one |
|
|
190
|
+
| `EFFECT_RESULT_FIELD` | the operation's own `resultType` value | absent |
|
|
191
|
+
| `EFFECT_CODE_FIELD` | absent | the adapter's failure code |
|
|
192
|
+
| `EFFECT_MESSAGE_FIELD` | absent | the adapter's failure message |
|
|
193
|
+
| `EFFECT_RETRYABLE_FIELD` | absent | whether a retry might have succeeded |
|
|
194
|
+
|
|
195
|
+
A follow-up action correlates the outcome to the effect that caused it through
|
|
196
|
+
`EFFECT_ID_FIELD`/`EFFECT_OPERATION_ID_FIELD` — never by parsing text. Before 8.1, the
|
|
197
|
+
success payload was the raw `resultType` value with no envelope, and the failure payload
|
|
198
|
+
was a single formatted string `"<code>: <message>"`; an application still declaring
|
|
199
|
+
`primitiveType('string')` as either event's `payloadType` now fails validation, because the
|
|
200
|
+
dispatched value is always this entity shape.
|
|
116
201
|
|
|
117
202
|
Both are checked against the declared `EventDef.payloadType` the same way any event is,
|
|
118
203
|
so a mismatched declaration is caught rather than silently dropped.
|
|
119
204
|
|
|
205
|
+
**The full envelope, field by field (spec 8.2 §31-33):**
|
|
206
|
+
|
|
207
|
+
| Field | Present | Source, lifetime and stability |
|
|
208
|
+
| --- | --- | --- |
|
|
209
|
+
| `EFFECT_ID_FIELD` | always | The framework-generated id of this effect intent. Stable for the life of the intent (survives restart — it is what `EffectRecord.id` is keyed by), never consumer-settable. |
|
|
210
|
+
| `EFFECT_INTEGRATION_ID_FIELD` | always | The `IntegrationDef.id` the operation belongs to. Graph-static. |
|
|
211
|
+
| `EFFECT_OPERATION_ID_FIELD` | always | The `IntegrationOperationDef.id` invoked. Graph-static. |
|
|
212
|
+
| `EFFECT_IDEMPOTENCY_KEY_FIELD` | when the `integration-effect` operation declared one | Whatever the action's `idempotencyKey` expression evaluated to at commit time. Application-controlled, and the field the framework recommends for business correlation (below). |
|
|
213
|
+
| `EFFECT_CORRELATION_ID_FIELD` | when the committing action's transaction has one | See below — this is **not** a business correlation key. |
|
|
214
|
+
| `EFFECT_RESULT_FIELD` | success only | The operation's own `resultType` value, unwrapped from the adapter's `IntegrationSuccess.value`. |
|
|
215
|
+
| `EFFECT_CODE_FIELD` | failure only | The adapter's `IntegrationFailure.code`. |
|
|
216
|
+
| `EFFECT_MESSAGE_FIELD` | failure only | The adapter's `IntegrationFailure.message` — see the security note below before copying it into state. |
|
|
217
|
+
| `EFFECT_RETRYABLE_FIELD` | failure only | The adapter's `IntegrationFailure.retryable`, coerced to a boolean (`retryable === true`; both `false` and absent read as `false` here — the three-way distinction that matters for retry control flow is internal to the retry policy, not exposed on the terminal payload). |
|
|
218
|
+
|
|
219
|
+
Success and failure share this one envelope family — the always-present fields plus
|
|
220
|
+
`idempotencyKey`/`correlationId` when applicable — but they are **not symmetric**: success
|
|
221
|
+
never carries `EFFECT_CODE_FIELD`/`EFFECT_MESSAGE_FIELD`/`EFFECT_RETRYABLE_FIELD`, and
|
|
222
|
+
failure never carries `EFFECT_RESULT_FIELD`. Do not describe the two shapes as identical;
|
|
223
|
+
describe them as one family with disjoint success-only and failure-only fields.
|
|
224
|
+
|
|
225
|
+
**What `EFFECT_CORRELATION_ID_FIELD` actually is.** Its value is the internal Axiom
|
|
226
|
+
transaction id (`Transaction.id`, formatted like `tx_<n>`) of the action's transaction that
|
|
227
|
+
recorded the effect intent — the same id `RuntimeDiagnostic.transactionId` already carries
|
|
228
|
+
elsewhere. Concretely:
|
|
229
|
+
|
|
230
|
+
- **Source**: assigned by the runtime's own per-process transaction counter, never supplied
|
|
231
|
+
or influenced by the application or the adapter.
|
|
232
|
+
- **Lifetime**: exists only as long as the process that created it is running.
|
|
233
|
+
- **Uniqueness scope**: unique only within one running authority process — it is not a
|
|
234
|
+
globally unique identifier.
|
|
235
|
+
- **Stable across restart?** No. The counter resets when the authority restarts, so this
|
|
236
|
+
value cannot be used to correlate an effect across a restart, or across two different
|
|
237
|
+
authority processes.
|
|
238
|
+
- **Consumer-settable?** No — there is no way for a graph or an adapter to choose it.
|
|
239
|
+
- **Always present?** In practice yes, since every `integration-effect` operation only ever
|
|
240
|
+
runs inside an action's transaction, and every transaction is assigned an id — but it is
|
|
241
|
+
documented as conditional (`when the committing action's transaction has one`) because
|
|
242
|
+
that is a property of the runtime's transaction model, not a graph-level guarantee this
|
|
243
|
+
document freezes.
|
|
244
|
+
|
|
245
|
+
**Its string format (`tx_<n>`) is explicitly not part of the public contract** (spec 8.2
|
|
246
|
+
§27) — only its semantics are: "identifies the Axiom transaction that created this effect
|
|
247
|
+
intent, within this process's lifetime." A future implementation may change the format
|
|
248
|
+
without that being a breaking change, as long as the semantics hold.
|
|
249
|
+
|
|
250
|
+
**Business correlation guidance (spec 8.2 §28-30).** `EFFECT_CORRELATION_ID_FIELD` is an
|
|
251
|
+
**internal diagnostic aid**, not a business correlation key — it identifies "which
|
|
252
|
+
transaction" in this process, not "which order" or "which device" to a follow-up action.
|
|
253
|
+
**Effect outcomes do not automatically carry the original operation's arguments.** An
|
|
254
|
+
application that needs to correlate an outcome back to a specific business entity (which
|
|
255
|
+
order this reboot was for, which customer this notification was about) should use
|
|
256
|
+
`idempotencyKey` for that purpose when the value is naturally unique per business operation
|
|
257
|
+
(an order id, a reservation id) — it is already carried through to both the success and
|
|
258
|
+
failure envelope, application-controlled, and exists for exactly this shape of problem.
|
|
259
|
+
0.8.2 deliberately does **not** add a separate `correlationKey`/`correlationValue`
|
|
260
|
+
primitive: `idempotencyKey` already covers the common case cleanly, and a second field
|
|
261
|
+
whose only job is to avoid a documentation inconvenience was judged not worth the added
|
|
262
|
+
vocabulary (spec 8.2 §29-30). This may be revisited if a concrete use case proves
|
|
263
|
+
`idempotencyKey` alone is not conceptually clean enough — none has yet.
|
|
264
|
+
|
|
265
|
+
**Effect message security (spec 8.2 §33).** Framework diagnostics — anything Axiom itself
|
|
266
|
+
emits as a `RuntimeDiagnostic`/`ServerDiagnostic` — are sanitized according to
|
|
267
|
+
`DISCLOSABLE_DETAIL_KEYS` (see [`AUTHORITY.md`](AUTHORITY.md#diagnostics)) before crossing
|
|
268
|
+
the trust boundary. `EFFECT_MESSAGE_FIELD`/`EFFECT_CODE_FIELD` are different: they are
|
|
269
|
+
**application data**, populated verbatim from whatever `IntegrationFailure.message`/`.code`
|
|
270
|
+
the adapter returned, and dispatched into ordinary graph state through the event pipeline
|
|
271
|
+
like any other value. Once an application's own action copies an adapter's message into
|
|
272
|
+
state, **that application is responsible for what the message contains** — Axiom's
|
|
273
|
+
diagnostic sanitization does not reach into adapter-authored text. Never place a secret or
|
|
274
|
+
credential in an adapter's `IntegrationFailure.message`.
|
|
275
|
+
|
|
120
276
|
## No automatic compensation
|
|
121
277
|
|
|
122
278
|
0.8 does not implement compensation. If an effect has a semantic inverse, it is another
|
package/docs/EVENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Events
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-alpha.1. An event is a typed fact — something that happened — never work
|
|
4
4
|
itself. [`AUTHORITY.md`](AUTHORITY.md#external-events) is the load-bearing statement;
|
|
5
5
|
this file is the vocabulary and the webhook delivery mechanism.
|
|
6
6
|
|
|
@@ -75,6 +75,10 @@ unverified request never reaches the semantic layer at all (`WEBHOOK_VERIFICATIO
|
|
|
75
75
|
401). Provider-specific protocol — headers, signing scheme, payload shape — stays entirely
|
|
76
76
|
in `verify`/`decode`; nothing crosses into `ApplicationGraph`.
|
|
77
77
|
|
|
78
|
+
`serveAxiomApplication({ ..., webhooks })` accepts the same option directly (spec 8.1
|
|
79
|
+
§56-58) — a webhook-receiving application no longer has to drop to `createAxiomServer` +
|
|
80
|
+
`serveOverHttp` just to register one.
|
|
81
|
+
|
|
78
82
|
## Duplicate delivery
|
|
79
83
|
|
|
80
84
|
`decode`'s optional `deliveryId` is deduplicated against a **bounded, per-route,
|
|
@@ -106,10 +110,20 @@ Full tables: [`VALIDATION.md`](VALIDATION.md#integrations-effects-triggers-and-e
|
|
|
106
110
|
## AgentAPI
|
|
107
111
|
|
|
108
112
|
```ts
|
|
109
|
-
agent.
|
|
113
|
+
agent.getTriggeredEvents(); // EventDef[] — events at least one trigger reacts to
|
|
110
114
|
agent.getActionsTriggeredByEvent(id); // ActionDef[]
|
|
111
115
|
```
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
`getTriggeredEvents()` is a **graph-static** query — "which `EventDef`s have a `TriggerDef`
|
|
118
|
+
bound to them" — not "which webhook deliveries has this server received" (spec 8.2 §34-37).
|
|
119
|
+
An event it returns may be dispatched by a verified external webhook, by an effect's
|
|
120
|
+
`succeededEventId`/`failedEventId`, or by any other internal source; the graph does not
|
|
121
|
+
record which, so nothing here claims to. Webhook routes and deployment-level registration
|
|
122
|
+
are host/deployment concerns, deliberately outside what `GraphQueries` infers.
|
|
123
|
+
|
|
124
|
+
`getWebhookEvents()` is a deprecated alias, kept for backward compatibility; new code should
|
|
125
|
+
call `getTriggeredEvents()`.
|
|
126
|
+
|
|
127
|
+
"Which trigger-bound event can mutate `Order`?" (spec §78) is answerable by following
|
|
128
|
+
`getTriggeredEvents()` through `getActionsTriggeredByEvent()` to the actions it can reach,
|
|
115
129
|
without reading a single handler.
|
package/docs/EXPRESSIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Expressions
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-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.8.
|
|
3
|
+
Axiom 0.8.2-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.8.
|
|
28
|
+
const graph = new ApplicationGraph(id, name, version?); // version defaults to '0.8.2'
|
|
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
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Integrations
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-alpha.1. How an application declares and calls an external system, without
|
|
4
4
|
embedding a transport, an SDK or a secret in the graph. The authority boundary this
|
|
5
5
|
depends on is [`AUTHORITY.md`](AUTHORITY.md#external-systems); this file is the vocabulary.
|
|
6
6
|
|
|
@@ -85,6 +85,40 @@ that does not conform is never handed to the application as `unknown` — it is
|
|
|
85
85
|
`INTEGRATION_RESULT_INVALID` (a runtime diagnostic; see [`RUNTIME.md`](RUNTIME.md)) before
|
|
86
86
|
`ref(bindAs)` would ever resolve to it.
|
|
87
87
|
|
|
88
|
+
## Timeout
|
|
89
|
+
|
|
90
|
+
**The Axiom runtime enforces `timeoutMs`, not the adapter.** `queryIntegration` races
|
|
91
|
+
`adapter.query(...)` against a `ServerHost.scheduleOnce(timeoutMs, ...)` deadline (spec 8.1
|
|
92
|
+
§15-25) — a non-cooperating adapter whose promise never settles cannot wedge the semantic
|
|
93
|
+
invocation, or by extension a polling interval trigger, forever. On timeout, the invocation
|
|
94
|
+
fails with `INTEGRATION_TIMEOUT` immediately; the adapter's promise is never cancelled (Axiom
|
|
95
|
+
cannot know whether that is safe for an arbitrary provider call), but if it eventually
|
|
96
|
+
settles, that result is simply discarded — it can never mutate state or fire a follow-up,
|
|
97
|
+
because the deadline already answered pre-transaction.
|
|
98
|
+
|
|
99
|
+
An adapter MAY still race its own deadline internally (`createHttpIntegrationAdapter` does,
|
|
100
|
+
via `AbortController`) to cancel the underlying provider call early, but this is an
|
|
101
|
+
optimization, never a correctness requirement: the runtime's own enforcement is what a graph
|
|
102
|
+
author can rely on regardless of which adapter is registered.
|
|
103
|
+
|
|
104
|
+
### A hung query delays other work, bounded by `timeoutMs` (spec 8.2 §40-42)
|
|
105
|
+
|
|
106
|
+
The Axiom authority executes every invocation it runs — an ordinary client request, a
|
|
107
|
+
trigger tick, an effect-outcome event dispatch — through one serialized FIFO queue
|
|
108
|
+
(`AxiomServer`'s internal `serialize`, spec 8.1 §26-30). A query that hangs therefore does
|
|
109
|
+
not merely block *its own* invocation: it also blocks whatever unrelated request happened
|
|
110
|
+
to queue up behind it, even one that shares no state, integration or trigger with it at
|
|
111
|
+
all. This is expected, not a bug to route around — it is what makes same-instant triggers
|
|
112
|
+
commit one at a time identically on the deterministic and the real host.
|
|
113
|
+
|
|
114
|
+
**The delay this can cause is bounded by `timeoutMs`, never indefinite.** The hung query
|
|
115
|
+
itself cannot run past its declared deadline (above), so the request queued behind it is
|
|
116
|
+
released as soon as that deadline fires — worst case, `timeoutMs`, not forever. Do not
|
|
117
|
+
introduce concurrent query execution to remove this delay; the observed serialization is
|
|
118
|
+
correct under the current authority model (spec 8.2 §41) and changing it is out of scope.
|
|
119
|
+
See `'a hung query delays a genuinely unrelated queued Action, bounded by timeoutMs'`
|
|
120
|
+
(`packages/server/test/timeout-and-scheduling.test.ts`) for the regression test.
|
|
121
|
+
|
|
88
122
|
## Registering an adapter
|
|
89
123
|
|
|
90
124
|
```ts
|
|
@@ -113,9 +147,12 @@ REST service: a base URL, a method and a path template per operation (`{param}`
|
|
|
113
147
|
substituted from the operation's arguments), a JSON body built from the remaining
|
|
114
148
|
arguments, and a timeout via `AbortController`. It is explicitly not the canonical
|
|
115
149
|
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
|
-
|
|
118
|
-
|
|
150
|
+
without writing a bespoke adapter for a demo. `createFakeIntegrationAdapter({ query?, effect? })` returns deterministic, caller-supplied
|
|
151
|
+
results: what conformance fixtures and tests use, since semantics must never depend on a
|
|
152
|
+
real network call. Its callbacks receive the same `context` (`{ timeoutMs }` for a query,
|
|
153
|
+
`{ idempotencyKey }` for an effect) the real `IntegrationAdapter` interface does, so a test
|
|
154
|
+
can simulate a hanging call, a declared timeout, or a stable idempotency key without
|
|
155
|
+
dropping to a hand-written adapter.
|
|
119
156
|
|
|
120
157
|
## Validation
|
|
121
158
|
|
package/docs/LOCATIONS.md
CHANGED
package/docs/PRESENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-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
|
# Semantic contract
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Triggers
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-alpha.1. A `TriggerDef` says **when** an action should be invoked, without
|
|
4
4
|
embedding callback code. `docs/AUTHORITY.md`
|
|
5
5
|
[§ Triggers](AUTHORITY.md#triggers) is the load-bearing statement of the execution model;
|
|
6
6
|
this file is the vocabulary.
|
|
@@ -44,6 +44,14 @@ can only be satisfied by a real, authenticated principal is correctly refused wh
|
|
|
44
44
|
trigger targets it; this is not a special case in the authorization check, it is the
|
|
45
45
|
ordinary one applied to a `null` principal, exactly as an anonymous client request gets.
|
|
46
46
|
|
|
47
|
+
`source: 'system'` is also what `invocation.allowedSources` checks (spec 8.1 §3-14, full
|
|
48
|
+
model in [`AUTHORITY.md`](AUTHORITY.md#invocation-source)): an action meant only to be a
|
|
49
|
+
trigger's target — an effect's `succeededEventId`/`failedEventId` handler, a webhook's — can
|
|
50
|
+
declare `invocation: { allowedSources: ['system'] }` so an anonymous client that guessed its
|
|
51
|
+
id cannot invoke it directly. A trigger targeting an action that has opted out of
|
|
52
|
+
`'system'` entirely is rejected at validation (`TRIGGER_TARGET_SOURCE_MISMATCH`), since the
|
|
53
|
+
trigger could then never succeed.
|
|
54
|
+
|
|
47
55
|
## Where a trigger executes
|
|
48
56
|
|
|
49
57
|
Derived from where its target action executes, not declared:
|
|
@@ -60,10 +68,26 @@ triggers targeting a server-authority one, are rejected at validation
|
|
|
60
68
|
(`TRIGGER_WRONG_AUTHORITY`) — the mismatch can never reach a runtime that would silently
|
|
61
69
|
do nothing with it.
|
|
62
70
|
|
|
63
|
-
**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
**A client-authority trigger of a kind the browser cannot execute is a validation error,
|
|
72
|
+
not a silent no-op — for any call that actually names the browser's capabilities.** The
|
|
73
|
+
browser runtime implements no trigger kind at all today
|
|
74
|
+
(`BROWSER_TRIGGER_CAPABILITIES.supportedTriggerKinds` is empty), so `compileToIR` — which
|
|
75
|
+
applies `BROWSER_TRIGGER_CAPABILITIES` by default — and `validateForBrowser` both reject
|
|
76
|
+
such a trigger with `CLIENT_TRIGGER_UNSUPPORTED`, the same capability-gate pattern
|
|
77
|
+
`RendererCapabilities` already applies to UI node kinds. Before spec 8.1, the trigger
|
|
78
|
+
validated and compiled into `ApplicationIR.triggers` and simply never fired, which is
|
|
79
|
+
exactly the "publicly declared, typechecks, passes validation, has no defined runtime
|
|
80
|
+
behaviour" shape the framework forbids.
|
|
81
|
+
|
|
82
|
+
**`validateGraph(graph)` with no options is target-neutral by design (spec 8.2 §2-4) and
|
|
83
|
+
does not raise `CLIENT_TRIGGER_UNSUPPORTED`** — a graph is never rejected for a trigger
|
|
84
|
+
runtime nobody named, exactly as it is never rejected for a renderer nobody named. A
|
|
85
|
+
validate-only workflow that wants the browser-real answer without compiling calls
|
|
86
|
+
`validateForBrowser(graph)` (`@cynodia/axiom-compiler`) instead of the bare call; both it
|
|
87
|
+
and `compileToIR` apply the identical `BROWSER_TRIGGER_CAPABILITIES`, so they never
|
|
88
|
+
disagree. Compiling for a trigger runtime that *does* implement a kind
|
|
89
|
+
(`compileToIR(graph, { triggerRuntime })`) accepts it. Only the authoritative runtime
|
|
90
|
+
executes triggers today. See [Not in 0.8.0](AUTHORITY.md#not-in-080).
|
|
67
91
|
|
|
68
92
|
## Interval semantics
|
|
69
93
|
|
|
@@ -142,6 +166,23 @@ No test verifying interval/delay behavior needs to wait on a real second (spec
|
|
|
142
166
|
`advance(ms)` fires every host timer that becomes due, re-scheduling intervals, in the
|
|
143
167
|
order they would fire on a real clock.
|
|
144
168
|
|
|
169
|
+
**Every invocation — client request or trigger tick — is serialized against every other one
|
|
170
|
+
this authority runs** (spec 8.1 §26-30), the same FIFO ordering `AxiomServer.handle()`
|
|
171
|
+
already gave client requests. `advance(ms)` firing several same-period triggers in one call
|
|
172
|
+
does not race their commits against each other: each waits its turn, exactly as it would
|
|
173
|
+
under real, staggered timer callbacks. This is why the deterministic host and the real host
|
|
174
|
+
agree on outcome for the same schedule — the ordering guarantee does not depend on which
|
|
175
|
+
host is running. Only the overlap check itself (`inFlight`, above) runs unserialized, since
|
|
176
|
+
it exists specifically to detect *concurrent* ticks of the *same* trigger, which requires
|
|
177
|
+
running immediately when the timer fires.
|
|
178
|
+
|
|
179
|
+
**A hung query cannot wedge a trigger forever.** `integration-query`'s `timeoutMs` is
|
|
180
|
+
enforced by the runtime itself (spec 8.1 §15-25, full model in
|
|
181
|
+
[`INTEGRATIONS.md`](INTEGRATIONS.md#timeout)) — a non-cooperating adapter's promise that
|
|
182
|
+
never settles still causes the invocation, and therefore the tick, to fail within
|
|
183
|
+
`timeoutMs`, clearing `inFlight` so the next scheduled tick runs normally instead of being
|
|
184
|
+
skipped as an overlap forever.
|
|
185
|
+
|
|
145
186
|
## Validation
|
|
146
187
|
|
|
147
188
|
| Code | Raised when |
|
|
@@ -150,6 +191,8 @@ order they would fire on a real clock.
|
|
|
150
191
|
| `TRIGGER_INTERVAL_NOT_POSITIVE` | `everyMs`/`afterMs` is not a positive number. |
|
|
151
192
|
| `UNKNOWN_EVENT` | An `event` trigger's `eventId` does not resolve to an `EventDef`. |
|
|
152
193
|
| `TRIGGER_WRONG_AUTHORITY` | An authority mismatch between the trigger kind and its target action, described above. |
|
|
194
|
+
| `TRIGGER_TARGET_SOURCE_MISMATCH` | The target action's `invocation.allowedSources` excludes `'system'`, so this trigger could never invoke it. |
|
|
195
|
+
| `CLIENT_TRIGGER_UNSUPPORTED` | A client-authority trigger of a kind the named trigger runtime does not execute. |
|
|
153
196
|
|
|
154
197
|
Full table: [`VALIDATION.md`](VALIDATION.md#integrations-effects-triggers-and-events).
|
|
155
198
|
|
|
@@ -159,7 +202,12 @@ Full table: [`VALIDATION.md`](VALIDATION.md#integrations-effects-triggers-and-ev
|
|
|
159
202
|
agent.getTriggersForAction(actionId); // TriggerDef[]
|
|
160
203
|
agent.getTimedTriggers(); // TriggerDef[] — interval and delay only
|
|
161
204
|
agent.getActionsTriggeredByEvent(eventId); // ActionDef[]
|
|
205
|
+
agent.getSystemOnlyActions(); // ActionDef[] — invocation.allowedSources excludes 'client'
|
|
206
|
+
agent.getTriggersTargetingClientOnlyActions(); // TriggerDef[] — could never succeed
|
|
207
|
+
agent.isClientInvocable(actionId); // boolean
|
|
208
|
+
agent.isSystemOnly(actionId); // boolean
|
|
162
209
|
```
|
|
163
210
|
|
|
164
211
|
"What runs automatically" and "what happens every 5 seconds" (spec §78) are answerable
|
|
165
|
-
without reading source
|
|
212
|
+
without reading source, and so is "which actions are reachable only by a trigger, event or
|
|
213
|
+
effect outcome" (spec 8.1 §13).
|
package/docs/UI.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UI
|
|
2
2
|
|
|
3
|
-
Axiom 0.8.
|
|
3
|
+
Axiom 0.8.2-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.8.
|
|
3
|
+
Axiom 0.8.2-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
|
|
|
@@ -130,6 +130,9 @@ Full model: [`INTEGRATIONS.md`](INTEGRATIONS.md), [`EFFECTS.md`](EFFECTS.md),
|
|
|
130
130
|
| `TRIGGER_INTERVAL_NOT_POSITIVE` | An `interval` trigger's `everyMs`, or a `delay` trigger's `afterMs`, that is not a positive number. |
|
|
131
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
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
|
+
| `TRIGGER_TARGET_SOURCE_MISMATCH` | A trigger — which always invokes with `source: 'system'` — targets an action whose `invocation.allowedSources` excludes `'system'`; the trigger could never succeed (spec 8.1 §3-9). |
|
|
134
|
+
| `INVALID_INVOCATION_SOURCE` | `ActionDef.invocation.allowedSources` is present but empty — the action could never be invoked at all. |
|
|
135
|
+
| `CLIENT_TRIGGER_UNSUPPORTED` | A client-authority trigger of a kind the intended trigger runtime does not execute — before 8.1 this validated and compiled anyway, and simply never fired (spec 8.1 §31-36). The browser trigger runtime executes no kind today. Only raised when a `triggerRuntime` is actually named — `validateGraph(graph)` with no options is target-neutral and never raises it; `validateForBrowser(graph)`/`compileToIR(graph)` name the real browser capabilities and do (spec 8.2 §2-6). The message states the remediation: move the target action to server authority, or compile for a trigger runtime that publishes the kind. |
|
|
133
136
|
|
|
134
137
|
### UI and routing
|
|
135
138
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2-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.8.
|
|
36
|
-
"@cynodia/axiom-runtime": "0.8.
|
|
37
|
-
"@cynodia/axiom-compiler": "0.8.
|
|
38
|
-
"@cynodia/axiom-agent-api": "0.8.
|
|
35
|
+
"@cynodia/axiom-core": "0.8.2-alpha.1",
|
|
36
|
+
"@cynodia/axiom-runtime": "0.8.2-alpha.1",
|
|
37
|
+
"@cynodia/axiom-compiler": "0.8.2-alpha.1",
|
|
38
|
+
"@cynodia/axiom-agent-api": "0.8.2-alpha.1"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -b tsconfig.json"
|