@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.
@@ -1,6 +1,6 @@
1
1
  # Actions and transactions
2
2
 
3
- Axiom 0.8.0-alpha.1. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.8.2-alpha.1. An action is behavior expressed as data, executed as a transaction.
4
4
 
5
5
  ```ts
6
6
  {
package/docs/AGENT_API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent API
2
2
 
3
- Axiom 0.8.0-alpha.1. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.8.2-alpha.1. The machine-facing interface. Agents query semantics and apply
4
4
  structural transformations; they never edit generated code.
5
5
 
6
6
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.8.0-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
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.0'
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.getWebhookEvents();
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 the IR
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
- `CONCURRENCY_CONFLICT` `MALFORMED_REQUEST` `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE`
655
- and `REMOTE_ACTION_UNAVAILABLE` on the client.
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
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.8.0-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
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
- External effects are deliberately unsolved in 0.6. A design involving commands, a
394
- transactional outbox and idempotency is future work; do not smuggle one in meanwhile.
393
+ Use `IntegrationDef` + an `integration-effect` operation instead — a typed semantic
394
+ operation, dispatched post-commit through the durable outbox, with retry and an
395
+ idempotency key, whose outcome reaches a follow-up action as a structured event rather than
396
+ a side effect nothing can roll back. See [`INTEGRATIONS.md`](INTEGRATIONS.md) and
397
+ [`EFFECTS.md`](EFFECTS.md).
395
398
 
396
399
  ## 31. Restating what the model already knows
397
400
 
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.8.0-alpha.1. How an application crosses the trust boundary.
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 names the contracts the fixtures are written against (`axiom.conformance.v1`,
517
- `axiom.server.v1`, `axiom.protocol.v1`), the release, every area covered, and every fixture
518
- with its file. A runtime that does not implement a contract the manifest names should refuse
519
- the suite rather than discover the mismatch one assertion at a time. The files are plain JSON
520
- in a documented package directory, so a non-JavaScript consumer can read them straight out of
521
- the tarball without Node's module resolver.
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 three. The rules:
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, 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.
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.** `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.
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.
@@ -1,6 +1,6 @@
1
1
  # Constraints
2
2
 
3
- Axiom 0.8.0-alpha.1. Two constructs, answering different questions. They are not
3
+ Axiom 0.8.2-alpha.1. Two constructs, answering different questions. They are not
4
4
  interchangeable.
5
5
 
6
6
  | | Question | Sees |
package/docs/EFFECTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Effects
2
2
 
3
- Axiom 0.8.0-alpha.1. External effects are not rollback-capable state mutations. This file
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
- - **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.
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.0-alpha.1. An event is a typed fact — something that happened — never work
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.getWebhookEvents(); // EventDef[] — events at least one trigger reacts to
113
+ agent.getTriggeredEvents(); // EventDef[] — events at least one trigger reacts to
110
114
  agent.getActionsTriggeredByEvent(id); // ActionDef[]
111
115
  ```
112
116
 
113
- "Which webhook can mutate `Order`?" (spec §78) is answerable by following
114
- `getWebhookEvents()` through `getActionsTriggeredByEvent()` to the actions it can reach,
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.
@@ -1,6 +1,6 @@
1
1
  # Expressions
2
2
 
3
- Axiom 0.8.0-alpha.1. An expression describes **what value is computed**. It is a tree of
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
 
@@ -1,6 +1,6 @@
1
1
  # Graph model
2
2
 
3
- Axiom 0.8.0-alpha.1. The `ApplicationGraph` is the authoritative representation of an
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.0'
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
@@ -1,6 +1,6 @@
1
1
  # Integrations
2
2
 
3
- Axiom 0.8.0-alpha.1. How an application declares and calls an external system, without
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
- effect? })` returns deterministic, caller-supplied results: what conformance fixtures and
118
- tests use, since semantics must never depend on a real network call.
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
@@ -1,6 +1,6 @@
1
1
  # Locations
2
2
 
3
- Axiom 0.8.0-alpha.1.
3
+ Axiom 0.8.2-alpha.1.
4
4
 
5
5
  ```text
6
6
  Expression = a value
@@ -1,6 +1,6 @@
1
1
  # Presentation
2
2
 
3
- Axiom 0.8.0-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
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
  # Runtime
2
2
 
3
- Axiom 0.8.0-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
3
+ Axiom 0.8.2-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
@@ -1,6 +1,6 @@
1
1
  # Semantic contract
2
2
 
3
- Axiom 0.8.0-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
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
@@ -1,6 +1,6 @@
1
1
  # State
2
2
 
3
- Axiom 0.8.0-alpha.1. A `StateDef` is a named application value: stored, or computed from
3
+ Axiom 0.8.2-alpha.1. A `StateDef` is a named application value: stored, or computed from
4
4
  other state.
5
5
 
6
6
  ```ts
package/docs/TRIGGERS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Triggers
2
2
 
3
- Axiom 0.8.0-alpha.1. A `TriggerDef` says **when** an action should be invoked, without
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
- **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).
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.0-alpha.1. Eleven semantic UI node kinds describe **what exists and what it does**.
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`:
@@ -1,6 +1,6 @@
1
1
  # Validation
2
2
 
3
- Axiom 0.8.0-alpha.1. Validation is authoring-time structural checking. It is not the same
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.0-alpha.1",
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.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"
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"