@cynodia/axiom 0.8.2-alpha.1 → 0.9.0-alpha.2

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/AGENTS.md ADDED
@@ -0,0 +1,68 @@
1
+ # Instructions for AI coding agents
2
+
3
+ You are consuming **Axiom** as an application framework. This file is routing only; the
4
+ contract lives in `docs/`.
5
+
6
+ ## Read this first
7
+
8
+ **Read [`docs/AGENT_REFERENCE.md`](docs/AGENT_REFERENCE.md), in full, before writing any
9
+ application code.** It is the compressed operational contract for application authors and it
10
+ ships inside this package.
11
+
12
+ Then escalate in this order, and only as far as the question requires:
13
+
14
+ 1. **`docs/AGENT_REFERENCE.md`** — the contract. Start here, every time.
15
+ 2. **The `.d.ts` declarations in `dist/`** — the API contract. Signatures, unions and
16
+ branded types are authoritative there.
17
+ 3. **The focused document in `docs/`** for the topic — the full contract for one area. See
18
+ the map in [`README.md`](README.md#documentation-map).
19
+ 4. **A minimal public-API probe** — build the smallest graph that isolates the question,
20
+ call `validateGraph` and read the returned codes. Diagnostics are structured and name
21
+ what is wrong.
22
+
23
+ Only if all four leave the question open is reading framework implementation source
24
+ justified. That is framework debugging, not application authoring.
25
+
26
+ ## Do not
27
+
28
+ - **Do not clone or reverse-engineer the Axiom repository to learn normal usage.** Everything
29
+ needed to author an application is in this package. There is no published Axiom CLI.
30
+ - **Do not search the web or scrape npm for documentation.** This package is the primary
31
+ source, and it describes this exact version.
32
+ - **Do not read, edit or patch generated output.** The emitted JavaScript, HTML and CSS are
33
+ build products. Nothing is authored there.
34
+ - **Do not guess the API from React, Vue, Angular, Svelte or Express conventions.** Axiom has
35
+ no component, no hook, no JSX, no route handler, no ORM and no callback anywhere in the
36
+ graph. Guessing from those conventions produces graphs `validateGraph` rejects.
37
+ - **Do not reach for an escape hatch.** There is no `formatter: fn`, no `validator: fn`, no
38
+ raw-CSS channel and no stored closure. If a capability seems missing, it is expressed as a
39
+ graph node; look it up rather than working around it.
40
+
41
+ ## Prefer canonical Axiom semantics
42
+
43
+ Each row is a pointer, not the rule. The rule itself lives in
44
+ [`docs/AGENT_REFERENCE.md`](docs/AGENT_REFERENCE.md) and in the topic document; read it
45
+ there rather than treating this table as a specification.
46
+
47
+ | Instead of | Use |
48
+ | --- | --- |
49
+ | Mutating an object an expression returned | A `Location`, addressing the writable position |
50
+ | A field name key in a record | The `FieldId` — runtime records are keyed by field id |
51
+ | A hand-written validation function | A `ConstraintDef` or `TransitionConstraintDef` |
52
+ | A hidden control to forbid an operation | A guard or a transition constraint. `hidden` is not `forbidden` |
53
+ | CSS, a colour or a length | A presentation role or token, and a `Theme` |
54
+ | A route handler, controller or SQL statement | `StateDef.authority` plus an `ActionDef` |
55
+ | A callback for new capability | The graph node that expresses it |
56
+
57
+ ## When something fails
58
+
59
+ `validateGraph` and the runtime both report structured diagnostics with a code and a path.
60
+ Match on the code, never on the message, and look the code up:
61
+ [`docs/VALIDATION.md`](docs/VALIDATION.md) for authoring-time codes,
62
+ [`docs/RUNTIME.md`](docs/RUNTIME.md) for runtime codes. A construct that validates and then
63
+ does nothing is a framework defect, not something to work around — report it.
64
+
65
+ Mistakes that compile but are wrong are collected in
66
+ [`docs/ANTI_PATTERNS.md`](docs/ANTI_PATTERNS.md). Read it before the **first** attempt, not the
67
+ second: collection nulls, how a `repeat` binds its current item, and addressing a collection
68
+ member by identity rather than index all shape a first draft rather than repairing it.
package/README.md CHANGED
@@ -1,13 +1,39 @@
1
1
  # Axiom
2
2
 
3
- AI-native semantic application framework.
3
+ AI-native semantic application framework. An application is a typed semantic graph, not
4
+ source files.
4
5
 
5
- Axiom represents application behavior, state, UI structure and presentation as structured
6
- semantic data executed by generic runtimes. An application is a typed graph, not source
7
- files: the JavaScript and HTML that reach the browser are output, and are never edited.
6
+ ## AI agents: read this first
7
+
8
+ **Read [`docs/AGENT_REFERENCE.md`](docs/AGENT_REFERENCE.md) before writing any Axiom code.**
9
+ It ships inside this package, so no repository access, web search or framework source
10
+ inspection is needed to obtain the contract.
11
+
12
+ | | |
13
+ | --- | --- |
14
+ | **What** | A semantic application framework. State, behavior, constraints, UI structure, presentation and authority are structured data executed by generic runtimes. |
15
+ | **Who for** | AI-authored applications. The primary author of an Axiom application is a coding agent; human readability is explicitly not the optimization target. |
16
+ | **Start** | [`docs/AGENT_REFERENCE.md`](docs/AGENT_REFERENCE.md) — the compressed operational contract for application authors. |
17
+ | **API contract** | The published `.d.ts` declarations in `dist/`. Authoritative for signatures, unions and branded types. |
18
+ | **Deeper semantics** | The focused documents in `docs/` — [map below](#documentation-map). |
19
+ | **Escalation** | `docs/AGENT_REFERENCE.md` → `.d.ts` → the focused `docs/` document for the topic → a minimal public-API probe. |
20
+
21
+ Axiom's vocabulary is deliberately unlike React, Vue, Angular, Svelte or Express: there is no
22
+ component, no hook, no JSX, no route handler, no ORM and no callback anywhere in the graph.
23
+ Guessing the API from those conventions produces graphs `validateGraph` rejects, so reading
24
+ the reference first is cheaper than any number of attempts.
25
+
26
+ **Reading the framework's implementation source should not be necessary to author an
27
+ application.** `docs/` plus the `.d.ts` declarations are intended to be sufficient on their
28
+ own. Source inspection is a legitimate tool for debugging Axiom itself; it is not the way to
29
+ discover normal consumer usage, and cloning the repository for that purpose is a sign the
30
+ documentation above was missed.
31
+
32
+ Shorter forms of the same routing: [`AGENTS.md`](AGENTS.md) and [`llms.txt`](llms.txt), both
33
+ at this package's root.
8
34
 
9
35
  **Status: experimental / alpha.** The API may change between alpha releases. The
10
- documentation in `docs/` describes this exact version.
36
+ documentation in `docs/` describes this exact version, `0.9.0-alpha.2`.
11
37
 
12
38
  ## Installation
13
39
 
@@ -17,6 +43,17 @@ npm install @cynodia/axiom-ui # semantic UI authoring patterns (build ti
17
43
  npm install @cynodia/axiom-server # only if the application has an authority
18
44
  ```
19
45
 
46
+ Every release of this project is a pre-release and npm's `latest` tag points at it, so the
47
+ plain command above installs the current version. **There is no `alpha` dist-tag** — the tag
48
+ was removed once it stopped tracking releases, and `npm install @cynodia/axiom@alpha` now
49
+ fails with a 404. Pin the exact version instead when one is needed:
50
+ `npm install @cynodia/axiom@0.9.0-alpha.2`.
51
+
52
+ These are ES modules compiled to ES2022; import them with `import`, not `require`. There is
53
+ no published Axiom CLI. `@cynodia/axiom-server`'s SQLite persistence adapter additionally
54
+ needs a Node build that provides `node:sqlite` (Node 22 or newer); `isSqliteAvailable()`
55
+ reports its absence rather than failing at import.
56
+
20
57
  ## Canonical mental model
21
58
 
22
59
  | Concept | Is |
@@ -124,30 +161,39 @@ console.log(app.getState(COUNT)); // 1
124
161
 
125
162
  `compileToHtml(graph)` emits the same application as one self-contained page.
126
163
 
127
- ## Documentation
164
+ ## Documentation map
128
165
 
129
- The complete operational contract ships with this package, in `docs/`.
166
+ The complete operational contract ships with this package, in `docs/`. Every path below
167
+ resolves inside the installed package. Read `docs/AGENT_REFERENCE.md` first; reach for a
168
+ focused document when the reference is not specific enough for the question at hand.
130
169
 
131
170
  | Need to understand | Read |
132
171
  | --- | --- |
133
- | Compressed reference for authoring or modifying an app | `docs/AGENT_REFERENCE.md` |
134
- | Exact runtime guarantees | `docs/SEMANTIC_CONTRACT.md` |
135
- | Graph, ids, types, entity value representation | `docs/GRAPH_MODEL.md` |
136
- | Every expression kind, builtin and scope rule | `docs/EXPRESSIONS.md` |
137
- | Addressing writable positions | `docs/LOCATIONS.md` |
138
- | Stored, derived, draft and ephemeral state | `docs/STATE.md` |
139
- | Actions, operations, transactions, iteration | `docs/ACTIONS_TRANSACTIONS.md` |
140
- | Constraints and transition constraints | `docs/CONSTRAINTS.md` |
141
- | Semantic UI nodes and bindings | `docs/UI.md` |
142
- | Presentation, UX intent, themes, formatting | `docs/PRESENTATION.md` |
143
- | Runtime API and diagnostic codes | `docs/RUNTIME.md` |
144
- | Server authority, Server IR, the protocol and persistence | `docs/AUTHORITY.md` |
145
- | Machine queries and graph transformations | `docs/AGENT_API.md` |
146
- | Validation codes | `docs/VALIDATION.md` |
147
- | Mistakes that compile but are wrong | `docs/ANTI_PATTERNS.md` |
148
-
149
- Start with `docs/AGENT_REFERENCE.md`. It plus the `.d.ts` declarations are intended to be
150
- sufficient on their own.
172
+ | **Compressed contract for authoring or modifying an app — start here** | [`docs/AGENT_REFERENCE.md`](docs/AGENT_REFERENCE.md) |
173
+ | Exact runtime guarantees, stated formally | [`docs/SEMANTIC_CONTRACT.md`](docs/SEMANTIC_CONTRACT.md) |
174
+ | Mistakes that compile but are wrong | [`docs/ANTI_PATTERNS.md`](docs/ANTI_PATTERNS.md) |
175
+ | Graph, node kinds, ids, types, entity value representation | [`docs/GRAPH_MODEL.md`](docs/GRAPH_MODEL.md) |
176
+ | Every expression kind, builtin, scope, presence and null rule | [`docs/EXPRESSIONS.md`](docs/EXPRESSIONS.md) |
177
+ | Addressing writable positions | [`docs/LOCATIONS.md`](docs/LOCATIONS.md) |
178
+ | Stored, derived, draft and ephemeral state | [`docs/STATE.md`](docs/STATE.md) |
179
+ | Actions, operations, guards, transactions, iteration | [`docs/ACTIONS_TRANSACTIONS.md`](docs/ACTIONS_TRANSACTIONS.md) |
180
+ | Constraints and transition constraints | [`docs/CONSTRAINTS.md`](docs/CONSTRAINTS.md) |
181
+ | Semantic UI nodes, interaction primitives and bindings | [`docs/UI.md`](docs/UI.md) |
182
+ | Presentation, UX intent, themes, value formatting | [`docs/PRESENTATION.md`](docs/PRESENTATION.md) |
183
+ | Runtime API, startup lifecycle and diagnostic codes | [`docs/RUNTIME.md`](docs/RUNTIME.md) |
184
+ | Validation codes and what rejects a graph | [`docs/VALIDATION.md`](docs/VALIDATION.md) |
185
+ | Server authority, the trust boundary, Server IR, the protocol, persistence, deployment | [`docs/AUTHORITY.md`](docs/AUTHORITY.md) |
186
+ | External systems: integration definitions, query operations, adapters, secrets | [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md) |
187
+ | External effects: the outbox, retries, delivery guarantees, outcomes | [`docs/EFFECTS.md`](docs/EFFECTS.md) |
188
+ | Typed events, webhooks, the dispatch pipeline | [`docs/EVENTS.md`](docs/EVENTS.md) |
189
+ | Timed and lifecycle execution, event-invoked actions | [`docs/TRIGGERS.md`](docs/TRIGGERS.md) |
190
+ | Live inbound streams: lifecycle, delivery, deduplication, backpressure | [`docs/SUBSCRIPTIONS.md`](docs/SUBSCRIPTIONS.md) |
191
+ | Binary data: `BlobRef`, upload, download, authorization, orphans | [`docs/STORAGE.md`](docs/STORAGE.md) |
192
+ | Machine queries, mutation impact and graph transformations | [`docs/AGENT_API.md`](docs/AGENT_API.md) |
193
+
194
+ `docs/AGENT_REFERENCE.md` plus the `.d.ts` declarations are intended to be sufficient on
195
+ their own. If they are not, that is a documentation defect worth reporting rather than a
196
+ reason to read framework source.
151
197
 
152
198
  ## What is in the box
153
199
 
@@ -1,6 +1,6 @@
1
1
  # Actions and transactions
2
2
 
3
- Axiom 0.8.2-alpha.1. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.9.0-alpha.2. An action is behavior expressed as data, executed as a transaction.
4
4
 
5
5
  ```ts
6
6
  {
@@ -210,6 +210,40 @@ like a mutation is. The adapter runs only after the transaction commits, and the
210
210
  this action's caller gets back never waits for it — "committed, effect pending." Never legal
211
211
  inside `for-each`. Full model: [`EFFECTS.md`](EFFECTS.md).
212
212
 
213
+ ### `blob-metadata`
214
+
215
+ ```ts
216
+ { kind: 'blob-metadata', storageId: NodeId, blobKey: Expression, bindAs: NodeId }
217
+ ```
218
+
219
+ Reads a stored object's metadata from a `StorageDef` and binds the `BlobRef` into scope —
220
+ query-like in exactly the sense `integration-query` is, and resolved in the same
221
+ pre-transaction phase. It returns the reference, never the bytes. A key that names nothing,
222
+ or names a still-staged upload, **fails the invocation** rather than binding a plausible
223
+ empty record. Never legal inside `for-each`. Full model: [`STORAGE.md`](STORAGE.md).
224
+
225
+ ### `blob-commit`
226
+
227
+ ```ts
228
+ { kind: 'blob-commit', storageId: NodeId, blobKey: Expression, succeededEventId?: NodeId, failedEventId?: NodeId }
229
+ ```
230
+
231
+ Promotes a staged upload to a stored object. Effect-like, and for the same reason every
232
+ effect is: an object store cannot join an Axiom transaction. Reaching it records intent,
233
+ committed atomically with the state that references the object and dispatched only once that
234
+ state is durable. A rolled-back transaction dispatches nothing and leaves the upload staged.
235
+ Never legal inside `for-each`.
236
+
237
+ ### `blob-delete`
238
+
239
+ ```ts
240
+ { kind: 'blob-delete', storageId: NodeId, blobKey: Expression, succeededEventId?: NodeId, failedEventId?: NodeId }
241
+ ```
242
+
243
+ Removes a stored object, post-commit. The state that stopped referencing it commits first; if
244
+ the external deletion then fails, state is still correct and the orphan is visible in
245
+ `server.blobLog()`. Never legal inside `for-each`.
246
+
213
247
  ## Authorization
214
248
 
215
249
  ```ts
package/docs/AGENT_API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent API
2
2
 
3
- Axiom 0.8.2-alpha.1. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.9.0-alpha.2. The machine-facing interface. Agents query semantics and apply
4
4
  structural transformations; they never edit generated code.
5
5
 
6
6
  ```ts
@@ -160,6 +160,33 @@ agent.getPersistenceForState(stateId)
160
160
  Authority is derived from what an action writes, so these answers cannot disagree with what
161
161
  the graph does. See [`AUTHORITY.md`](AUTHORITY.md).
162
162
 
163
+ ## External-world queries
164
+
165
+ ```ts
166
+ agent.listIntegrations() / agent.listIntegrationOperations(integrationId?)
167
+ agent.getActionsUsingIntegration(integrationId) / agent.getEffectsForAction(actionId)
168
+ agent.getTriggersForAction(actionId) / agent.getTimedTriggers()
169
+ agent.getActionsTriggeredByEvent(eventId) / agent.getTriggeredEvents()
170
+ agent.getExternalDependencies() // { integrations, operations }
171
+
172
+ agent.listSubscriptions()
173
+ agent.getSubscriptionsForIntegration(integrationId)
174
+ agent.getEventForSubscription(subscriptionId)
175
+ agent.getActionsReachableFromSubscription(subscriptionId)
176
+ agent.getExternalEventSources() // { subscriptions, events, integrations }
177
+
178
+ agent.listStorages()
179
+ agent.getActionsUsingStorage(storageId)
180
+ agent.getStoragesWithoutAccessRules() // stores that serve and accept nothing
181
+ ```
182
+
183
+ All of these are **graph-static**: they answer what the application *can* reach, not what a
184
+ running authority has done. `getActionsReachableFromSubscription` is the one to reach for
185
+ before changing a live feed — it follows the subscription's event through every bound
186
+ trigger, so "what can this feed actually change" needs no traversal by the consumer. Runtime
187
+ answers come from `AxiomServer.subscriptionLog()` and `blobLog()` instead; see
188
+ [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) and [`STORAGE.md`](STORAGE.md).
189
+
163
190
  ## Transactions
164
191
 
165
192
  Every change is staged on a private copy. The graph an agent or a runtime can observe is
@@ -1,11 +1,45 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.8.2-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.9.0-alpha.2. 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:
7
7
  [`ANTI_PATTERNS.md`](ANTI_PATTERNS.md).
8
8
 
9
+ ## Start here
10
+
11
+ ```bash
12
+ npm install @cynodia/axiom # graph, compiler, runtime, agent API
13
+ npm install @cynodia/axiom-ui # semantic UI authoring patterns, build time only
14
+ npm install @cynodia/axiom-server # only if a StateDef declares server authority
15
+ ```
16
+
17
+ Everything is imported from `@cynodia/axiom`; the four re-exported packages need not be
18
+ installed individually. There is no published CLI.
19
+
20
+ A complete runnable skeleton — graph, state, action, UI, route, compile, run — is the
21
+ minimal application in [`../README.md`](../README.md). Read this document for the rules;
22
+ copy that for the shape.
23
+
24
+ **Escalation order.** This document → the `.d.ts` declarations → the focused document for the
25
+ topic → a minimal public-API probe: build the smallest graph that isolates the question, call
26
+ `validateGraph`, read the returned codes. Reading Axiom's own implementation source is for
27
+ debugging the framework, not for authoring an application.
28
+
29
+ **How much of this to read.** Everything up to and including
30
+ [Agent API](#agent-api) applies to every Axiom application. If no `StateDef` declares
31
+ `authority: 'server'`, the application is client-only and
32
+ [SERVER AUTHORITY](#server-authority) onwards — authority, integrations, effects, triggers,
33
+ subscriptions, storage — describes capability it does not use; skim the headings and stop.
34
+ Read [`ANTI_PATTERNS.md`](ANTI_PATTERNS.md) **before** the first attempt, not the second:
35
+ collection nulls, repeat scope binding and identity-over-index selectors all shape a first
36
+ draft.
37
+
38
+ **Do not guess from React, Vue, Angular, Svelte or Express.** Axiom has no component, no hook,
39
+ no JSX, no route handler, no ORM and no callback in the graph. There is no `formatter: fn`, no
40
+ `validator: fn`, no raw-CSS channel and no stored closure anywhere; new capability arrives as
41
+ an inspectable node, never as a function you supply.
42
+
9
43
  ## Glossary
10
44
 
11
45
  One canonical term per concept. These are not interchangeable.
@@ -31,7 +65,7 @@ One canonical term per concept. These are not interchangeable.
31
65
  ## Graph construction
32
66
 
33
67
  ```ts
34
- const graph = new ApplicationGraph(id, name); // version defaults to '0.8.2'
68
+ const graph = new ApplicationGraph(id, name); // version defaults to '0.9.0'
35
69
  graph.addNode<StateDef>({ id, kind: 'state', ... }); // returns NodeId; throws if id exists
36
70
  graph.getNode<StateDef>(id); // deep clone, or undefined
37
71
  graph.updateNode(node); // write a modified node back
@@ -323,6 +357,13 @@ boolean types (`TYPE_MISMATCH`).
323
357
  Every kind is in `UI_NODE_KINDS`: `view` `container` `text` `repeat` `field-display` `form`
324
358
  `input` `button` `conditional` `diagnostic` `dialog`. Detail: [`UI.md`](UI.md).
325
359
 
360
+ Every kind carries the same base, and `visibleWhen` lives here rather than in
361
+ `presentation`:
362
+
363
+ ```ts
364
+ { id, kind, name?, visibleWhen?: Expression, presentation?: Presentation, metadata? }
365
+ ```
366
+
326
367
  - `RepeatNode` binds the current item to **the repeat node's own id**; the template refers to it as `ref(repeatNodeId)`.
327
368
  - `InputNode.binding` is `{ location }` — no expression, no field id. An input write goes through the same mutation engine and transaction as an action.
328
369
  - `ButtonNode.arguments` is keyed by **action parameter id**.
@@ -412,6 +453,22 @@ presentation: {
412
453
  }
413
454
  ```
414
455
 
456
+ Every value is a closed vocabulary, exported as an array; a token outside it is a validation
457
+ **error**, never a silently ignored value. The four an application reaches for constantly:
458
+
459
+ | Property | Vocabulary | Array |
460
+ | --- | --- | --- |
461
+ | `layout` | `vertical` `horizontal` `grid` `stack` | `LAYOUT_KINDS` |
462
+ | `gap`, `padding` | `none` `xsmall` `small` `medium` `large` `xlarge` | `SPACING_TOKENS` |
463
+ | `textRole` | `body` `caption` `label` `heading` `title` `display` | `TEXT_ROLES` |
464
+ | `density` | `compact` `comfortable` `spacious` | `DENSITIES` |
465
+
466
+ `layout: 'horizontal'` is shorthand for `layout: { kind: 'horizontal', gap?, align?, justify?,
467
+ wrap?, columns? }` — a bare token or the object, never a third form. `stack` is a tight
468
+ vertical column, not overlapping children. Every other vocabulary — roles, UX roles, surfaces,
469
+ treatments, icons, control variants, sizing, value formats, device classes —
470
+ is in [`PRESENTATION.md`](PRESENTATION.md); read it before guessing a token.
471
+
415
472
  Resolution precedence, lowest first:
416
473
 
417
474
  ```text
@@ -668,7 +725,8 @@ Portable artifacts, for a runtime written in another language:
668
725
  @cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for axiom.server.v1 (frozen)
669
726
  @cynodia/axiom-server/schema/server-ir.v2.schema.json JSON Schema for axiom.server.v2
670
727
  @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)
728
+ @cynodia/axiom-server/schema/server-ir.v4.schema.json JSON Schema for axiom.server.v4
729
+ @cynodia/axiom-server/schema/server-ir.v5.schema.json JSON Schema for axiom.server.v5 (latest)
672
730
  @cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
673
731
  ```
674
732
 
@@ -686,6 +744,82 @@ Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZ
686
744
  `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE` and `REMOTE_ACTION_UNAVAILABLE` on the
687
745
  client.
688
746
 
747
+ ## SUBSCRIPTIONS AND STORAGE
748
+
749
+ Full model: [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md), [`STORAGE.md`](STORAGE.md). The external
750
+ world reaching *in*, and binary data, which 0.8 had no vocabulary for.
751
+
752
+ The external-interaction model is exactly three directions. Anything else is one of them
753
+ wearing a different name.
754
+
755
+ | Direction | Shape | Vocabulary |
756
+ | --- | --- | --- |
757
+ | Query | Ask; wait for a finite answer. | `integration-query`, `blob-metadata` |
758
+ | Effect | Tell; no answer joins the transaction. | `integration-effect`, `blob-commit`, `blob-delete` |
759
+ | Subscription | The world tells you, while you are listening. | `SubscriptionDef` → `EventDef` |
760
+
761
+ 1. **SUBSCRIPTION INVARIANT** — a long-lived external source is a `SubscriptionDef`, never a client, a socket or a callback in the graph. A delivery becomes an `EventDef` payload and enters the existing `EventDef → TriggerDef → ActionDef` pipeline; there is no second event system.
762
+ 2. **RAW-I/O INVARIANT** — OS I/O primitives are not graph vocabulary. No `readFile(path)`, `openSocket(host, port)`, `exec(command)`, `spawn(process)`, file descriptor, Node stream or POSIX path exists or will. They live inside an adapter, which is exactly what lets a Rust runtime implement the same graph with different primitives.
763
+ 3. **DELIVERY INVARIANT** — at-least-once; effectively-once where `delivery.deduplicateBy` names an external identity, and that deduplication survives a restart when the persistence adapter is durable. Per-subscription ordering is guaranteed; cross-subscription ordering is guaranteed to be **nothing**.
764
+ 4. **BACKPRESSURE INVARIANT** — the queue is always bounded (`maxQueued`, default 64) and the default policy (`block`) cannot lose an event. A policy that may discard one (`drop-oldest`/`drop-newest`) is declared in the graph and reports `SUBSCRIPTION_DELIVERY_DROPPED` every time. Loss is never silent and never a default.
765
+ 5. **SHUTDOWN INVARIANT** — after `server.stop()`, no delivery reaches application state. A stopped subscription answers `stopped` to everything, including deliveries already in flight.
766
+ 6. **BLOB INVARIANT** — bytes never enter the graph, the Server IR or canonical state. A `BlobRef` (`blobRefEntity()` — key, media type, size, filename?, checksum?) is what state holds, and it discloses nothing about the provider.
767
+ 7. **BLOB AUTHORIZATION INVARIANT** — possession of a key is not permission. `StorageDef.readAuthorization` is evaluated with the caller bound to `PRINCIPAL` and the `BlobRef` bound to `ref(<storageId>)`; a store with no rule serves nothing. A key that names nothing is refused identically to one the caller may not read.
768
+ 8. **STAGED-COMMIT INVARIANT** — an object store does not join an Axiom transaction, and nothing pretends it does. An upload lands `staged`; `blob-commit` promotes it post-commit. A refused transaction leaves a sweepable staged object, never a state referencing bytes that were never claimed. A failed `blob-delete` leaves state correct and the orphan visible in `blobLog()`.
769
+
770
+ ```ts
771
+ { kind: 'subscription', id: SUB_STATUS, integrationId: INTEGRATION, source: 'device-status',
772
+ eventId: EVENT_STATUS_CHANGED,
773
+ lifecycle: { autoStart: true, required: false, reconnect: { policy: 'exponential', maxAttempts: 5, delayMs: 1000 } },
774
+ delivery: { maxQueued: 32, backpressure: 'block', deduplicateBy: F_DELIVERY_ID, maxAttempts: 1, onFailure: 'report' } }
775
+
776
+ { kind: 'storage', id: STORAGE_LOGS, blobEntityId: ENTITY_BLOB,
777
+ readAuthorization: <Expression>, uploadAuthorization: <Expression>,
778
+ acceptedMediaTypes: ['text/plain'], maxSizeBytes: 8388608 }
779
+
780
+ { kind: 'blob-metadata', storageId: STORAGE_LOGS, blobKey: <Expression>, bindAs: SCOPE_BLOB }
781
+ { kind: 'blob-commit', storageId: STORAGE_LOGS, blobKey: <Expression> }
782
+ { kind: 'blob-delete', storageId: STORAGE_LOGS, blobKey: <Expression> }
783
+ ```
784
+
785
+ Lifecycle: `inactive → starting → active → reconnecting → failed`, plus `stopped`. Startup
786
+ decides what activates; application code never calls `start()`. A failed source leaves the
787
+ application running unless `lifecycle.required`.
788
+
789
+ Subscription vs. webhook vs. polling — three different things, do not conflate them:
790
+
791
+ | | What it is | Vocabulary |
792
+ | --- | --- | --- |
793
+ | Webhook | Externally initiated finite request; each delivery enters independently. | host `webhooks` → `EventRequest` |
794
+ | Subscription | Standing semantic interest in a long-lived source. | `SubscriptionDef` |
795
+ | Polling | You ask, repeatedly, on a schedule. | `interval` `TriggerDef` → `integration-query` |
796
+
797
+ Upload is `POST /axiom/blob/<storageId>` and download is `GET /axiom/blob/<storageId>/<key>`
798
+ — one host transport for every Axiom application. Application-authored upload/download
799
+ routes: zero.
800
+
801
+ ```ts
802
+ agent.listSubscriptions() / agent.getSubscriptionsForIntegration(id);
803
+ agent.getEventForSubscription(id) / agent.getActionsReachableFromSubscription(id);
804
+ agent.getExternalEventSources(); // { subscriptions, events, integrations }
805
+ agent.listStorages() / agent.getActionsUsingStorage(id) / agent.getStoragesWithoutAccessRules();
806
+
807
+ server.subscriptionLog() / server.subscriptionStatus(id); // state, counters, last delivery, last failure
808
+ server.blobLog(); // storage effects and their outcomes
809
+ server.stageBlob(storageId, principal, upload);
810
+ server.authorizeBlobRead(storageId, key, principal);
811
+ server.authorizeBlobUpload(storageId, principal, { mediaType, size });
812
+ ```
813
+
814
+ Adapters: `SubscriptionAdapter` (`createScriptedSubscriptionAdapter` is the deterministic
815
+ fake) and `BlobStorageAdapter` (`createMemoryBlobStore`). A declared subscription or store
816
+ with no registered adapter fails `start()` rather than staying silently inert.
817
+
818
+ Diagnostics: `SUBSCRIPTION_ADAPTER_MISSING` `SUBSCRIPTION_START_FAILED`
819
+ `SUBSCRIPTION_DELIVERY_DROPPED` `SUBSCRIPTION_DELIVERY_FAILED` `BLOB_STORE_MISSING`
820
+ `BLOB_NOT_FOUND` `BLOB_ACCESS_DENIED` `BLOB_TOO_LARGE` `BLOB_MEDIA_TYPE_REJECTED`
821
+ `BLOB_OPERATION_FAILED` `BLOB_STORAGE_UNAVAILABLE` `BLOB_METADATA_FAILED`.
822
+
689
823
  ## Metadata classes
690
824
 
691
825
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.8.2-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.9.0-alpha.2. 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
@@ -405,3 +405,97 @@ a side effect nothing can roll back. See [`INTEGRATIONS.md`](INTEGRATIONS.md) an
405
405
 
406
406
  // RIGHT — declare `required: true` on the FieldDef and let the renderer mark it.
407
407
  ```
408
+
409
+ ## 32. OS I/O primitives are not graph vocabulary
410
+
411
+ ```ts
412
+ // WRONG — every one of these. None exists, and none will.
413
+ readFile(path); writeFile(path); openSocket(host, port); exec(command); spawn(process);
414
+ openSerialPort(device);
415
+ { kind: 'native', implementationId: 'app.readAttachment', inputs: { path: literal('/var/uploads/x') } }
416
+ ```
417
+
418
+ An `ApplicationGraph` exposes no filesystem path, no socket, no stream, no file descriptor
419
+ and no subprocess. That is not squeamishness about I/O — the Node host uses all of them
420
+ freely. It is that a graph naming one stops being:
421
+
422
+ | | Why |
423
+ | --- | --- |
424
+ | **portable** | A Rust runtime, or a browser, has different primitives — or none. |
425
+ | **analyzable for authority** | "What can this action reach?" has no answer once `exec` is in the vocabulary. |
426
+ | **secure** | A path is a capability the graph hands out; a key checked against a declared rule is not. |
427
+ | **deterministically testable** | A conformance fixture cannot script a real socket. |
428
+ | **introspectable** | `getExternalDependencies()` can enumerate typed operations. It cannot enumerate what a shell command does. |
429
+
430
+ Low-level I/O is permitted, and expected, **inside an adapter**:
431
+
432
+ ```
433
+ PrinterIntegration.print() → adapter implementation → TCP
434
+ VideoIntegration.transcode() → adapter implementation → ffmpeg subprocess
435
+ DeviceStream subscription → adapter implementation → serial port, MQTT, WebSocket
436
+ DiagnosticLogs storage → adapter implementation → local directory, or S3
437
+ ```
438
+
439
+ The graph says *what the interaction means*; the adapter decides *how*. Replace Node with
440
+ Rust, MQTT with a WebSocket, or a local directory with S3, and the graph does not change —
441
+ which is the test that the abstraction is at the right level.
442
+
443
+ Use [`INTEGRATIONS.md`](INTEGRATIONS.md), [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) or
444
+ [`STORAGE.md`](STORAGE.md) instead.
445
+
446
+ ## 33. `setInterval` + `fetch` for polling, or a client in application code
447
+
448
+ ```ts
449
+ // WRONG — all four, in application code.
450
+ setInterval(() => fetch('/api/devices').then(apply), 5000);
451
+ const socket = new WebSocket('wss://…');
452
+ const client = mqtt.connect('mqtt://…');
453
+ socket.onmessage = (event) => applyStatus(JSON.parse(event.data));
454
+ ```
455
+
456
+ Each of these puts scheduling, transport and a callback-driven mutation path into the
457
+ application, where nothing can analyze, test or authorize them.
458
+
459
+ | Instead of | Declare |
460
+ | --- | --- |
461
+ | `setInterval` + `fetch` | `TriggerDef{when:{kind:'interval'}}` → `integration-query` |
462
+ | `new WebSocket` / an MQTT client | `SubscriptionDef` → `EventDef` → `TriggerDef` |
463
+ | `socket.onmessage = handler` | The trigger's target action. Deliveries never invoke a callback. |
464
+ | A hand-rolled webhook route | The host's `webhooks` option → `EventRequest` |
465
+
466
+ ## 34. A client-authored subscription event
467
+
468
+ ```ts
469
+ // WRONG — an action a subscription's trigger invokes, left open to clients.
470
+ { kind: 'action', id: ACTION_APPLY_STATUS, /* no `invocation` */ operations: [ … ] }
471
+ ```
472
+
473
+ Any anonymous client that guesses the id can then assert whatever the live feed asserts.
474
+ Declare `invocation: { allowedSources: ['system'] }`: a client-sourced call is refused with
475
+ `INVOCATION_SOURCE_NOT_ALLOWED` before identity is even consulted.
476
+
477
+ ## 35. base64 bytes in canonical state
478
+
479
+ ```ts
480
+ // WRONG — the attachment's contents in the record, in the Server IR, in every snapshot.
481
+ { id: F_DOCUMENT_ATTACHMENT, valueType: primitiveType('string') } // "data:application/pdf;base64,…"
482
+ ```
483
+
484
+ Every read, every snapshot, every persisted write and every `changes` map then carries the
485
+ whole object. Store a `BlobRef` (`blobRefEntity()`) and let the bytes move through the
486
+ host's own upload and download transport — see [`STORAGE.md`](STORAGE.md). A 5MB attachment
487
+ leaves the record exactly as large as a 5-byte one.
488
+
489
+ ## 36. An application-authored upload or download route
490
+
491
+ ```ts
492
+ // WRONG — a route the graph does not know about, guarding data the graph does own.
493
+ express.post('/upload', (request, response) => { /* … */ });
494
+ express.get('/files/:key', (request, response) => response.sendFile(`/var/uploads/${request.params.key}`));
495
+ ```
496
+
497
+ The second is also a path traversal waiting to happen, and neither can be reached by
498
+ `validateGraph`, by `AgentAPI`, or by a conformance fixture. `POST /axiom/blob/<storageId>`
499
+ and `GET /axiom/blob/<storageId>/<key>` already exist, for every Axiom application, with
500
+ `StorageDef.uploadAuthorization` and `StorageDef.readAuthorization` enforced by the
501
+ authority. Application-authored upload/download routes: zero.
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.8.2-alpha.1. How an application crosses the trust boundary.
3
+ Axiom 0.9.0-alpha.2. 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
@@ -477,6 +477,16 @@ does for a local failure.
477
477
  | `INTEGRATION_ADAPTER_MISSING` | The Server IR requires an integration with no registered adapter — refused at `start()`, never deferred to first invocation. |
478
478
  | `EVENT_DISPATCH_DEPTH_EXCEEDED` | An event → action → effect → event chain was stopped before it could recurse unboundedly. |
479
479
  | `WEBHOOK_VERIFICATION_FAILED` | A webhook delivery failed provider signature verification and was refused before an event was ever constructed. |
480
+ | `SUBSCRIPTION_ADAPTER_MISSING` | The Server IR declares a subscription whose integration has no registered `SubscriptionAdapter`. Startup refuses, rather than leaving a declared live source permanently inactive. |
481
+ | `SUBSCRIPTION_START_FAILED` | A subscription's source could not be established, after every attempt its declared reconnect policy allows. |
482
+ | `SUBSCRIPTION_DELIVERY_DROPPED` | A delivery was discarded by a declared lossy backpressure policy. Loss is always declared and never silent. |
483
+ | `SUBSCRIPTION_DELIVERY_FAILED` | A delivery's triggered action failed after every permitted attempt. |
484
+ | `BLOB_STORE_MISSING` | The Server IR declares a `StorageDef` with no registered `BlobStorageAdapter`. |
485
+ | `BLOB_NOT_FOUND` | No object with that key, or the key names a still-staged upload. |
486
+ | `BLOB_ACCESS_DENIED` | The caller may not read, download or upload this object. Also the answer for a key that names nothing at all, so the endpoint is not an oracle for enumerating keys. |
487
+ | `BLOB_TOO_LARGE` | An upload exceeded the store's declared `maxSizeBytes`. |
488
+ | `BLOB_MEDIA_TYPE_REJECTED` | An upload's media type is not in the store's declared `acceptedMediaTypes`. |
489
+ | `BLOB_OPERATION_FAILED` | A `blob-commit` or `blob-delete` failed at the store, after its retry policy. |
480
490
  | `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. |
481
491
 
482
492
  Two client-side codes belong to the boundary as well:
@@ -626,8 +636,9 @@ page plus the conformance fixtures.
626
636
  | `axiom.server.v2` | the expression kinds `group` and `expression-ref`, and the `expressionDefs` they resolve against | 0.7.0 |
627
637
  | `axiom.server.v3` | integrations, integration operations, events, triggers, and the `integration-query`/`integration-effect` operation kinds | 0.8.0 |
628
638
  | `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 |
639
+ | `axiom.server.v5` | `SubscriptionDef` and `StorageDef`, and the `blob-metadata`/`blob-commit`/`blob-delete` operation kinds — the inbound external-I/O direction and binary object storage | 0.9.0 |
629
640
 
630
- `SERVER_IR_CONTRACTS` enumerates all four, and is the single source of truth this table is
641
+ `SERVER_IR_CONTRACTS` enumerates all five, and is the single source of truth this table is
631
642
  tested against — `packages/demo/test/documentation.test.ts` fails if a contract in
632
643
  `SERVER_IR_CONTRACTS` has no row here, or a row here names a contract the code does not
633
644
  declare (spec 8.2 §7-8). The rules:
@@ -635,11 +646,20 @@ declare (spec 8.2 §7-8). The rules:
635
646
  - **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
647
  - **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
648
  - **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-onlydocumentation, 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).
649
+ - **`axiom.server.v5` is the latest contract as of 0.9.0** (`SERVER_IR_LATEST_CONTRACT`). `usesExternalIOVocabulary` computes it from the document: any `subscriptions`, any `storages`, or any of the three blob operation kinds. The reason it is an incompatible change rather than an additive one is exact a v4 runtime that ignored `subscriptions` would start an application whose declared live event source never activates, and one that ignored a `blob-commit` would leave state referencing an object that stays staged forever. Both are silent divergence, which is what a contract label exists to prevent.
639
650
 
640
651
  There is one JSON Schema per contract, each generated from the runtime's own vocabulary and
641
652
  each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json`,
642
- `server-ir.v4.schema.json`.
653
+ `server-ir.v4.schema.json`, `server-ir.v5.schema.json`.
654
+
655
+ **`SubscriptionDef`, `StorageDef` and the blob operations.** Their normative semantics —
656
+ lifecycle states and transitions, at-least-once delivery, per-subscription ordering and the
657
+ explicit absence of cross-subscription ordering, bounded queues and every backpressure policy,
658
+ deduplication and its restart durability, poison-delivery bounds, staged-then-committed object
659
+ lifecycle, and the authorization rule a store evaluates before serving a byte — are documented
660
+ in [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) and [`STORAGE.md`](STORAGE.md), and are executable in
661
+ the `subscription-*` and `blob-*` conformance fixtures. An independent implementer needs those
662
+ two documents, the v5 schema and those fixtures, and no TypeScript.
643
663
 
644
664
  **`group`.** Partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`. Groups appear
645
665
  in the order their key was **first seen** in the source; members keep source order; two keys are