@cynodia/axiom 0.8.2-alpha.1 → 0.9.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ACTIONS_TRANSACTIONS.md +35 -1
- package/docs/AGENT_API.md +28 -1
- package/docs/AGENT_REFERENCE.md +79 -2
- package/docs/ANTI_PATTERNS.md +95 -1
- package/docs/AUTHORITY.md +24 -4
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EFFECTS.md +4 -1
- package/docs/EVENTS.md +5 -2
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +1 -1
- package/docs/INTEGRATIONS.md +5 -1
- package/docs/LOCATIONS.md +1 -1
- package/docs/PRESENTATION.md +1 -1
- package/docs/RUNTIME.md +3 -1
- package/docs/SEMANTIC_CONTRACT.md +1 -1
- package/docs/STATE.md +1 -1
- package/docs/STORAGE.md +253 -0
- package/docs/SUBSCRIPTIONS.md +315 -0
- package/docs/TRIGGERS.md +7 -1
- package/docs/UI.md +1 -1
- package/docs/VALIDATION.md +14 -1
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Actions and transactions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. 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.
|
|
3
|
+
Axiom 0.9.0-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
|
|
@@ -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
|
package/docs/AGENT_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent reference
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
|
|
4
4
|
declarations before authoring or modifying an Axiom application.
|
|
5
5
|
|
|
6
6
|
Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
|
|
@@ -668,7 +668,8 @@ Portable artifacts, for a runtime written in another language:
|
|
|
668
668
|
@cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for axiom.server.v1 (frozen)
|
|
669
669
|
@cynodia/axiom-server/schema/server-ir.v2.schema.json JSON Schema for axiom.server.v2
|
|
670
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
|
|
671
|
+
@cynodia/axiom-server/schema/server-ir.v4.schema.json JSON Schema for axiom.server.v4
|
|
672
|
+
@cynodia/axiom-server/schema/server-ir.v5.schema.json JSON Schema for axiom.server.v5 (latest)
|
|
672
673
|
@cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
|
|
673
674
|
```
|
|
674
675
|
|
|
@@ -686,6 +687,82 @@ Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZ
|
|
|
686
687
|
`AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE` and `REMOTE_ACTION_UNAVAILABLE` on the
|
|
687
688
|
client.
|
|
688
689
|
|
|
690
|
+
## SUBSCRIPTIONS AND STORAGE
|
|
691
|
+
|
|
692
|
+
Full model: [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md), [`STORAGE.md`](STORAGE.md). The external
|
|
693
|
+
world reaching *in*, and binary data, which 0.8 had no vocabulary for.
|
|
694
|
+
|
|
695
|
+
The external-interaction model is exactly three directions. Anything else is one of them
|
|
696
|
+
wearing a different name.
|
|
697
|
+
|
|
698
|
+
| Direction | Shape | Vocabulary |
|
|
699
|
+
| --- | --- | --- |
|
|
700
|
+
| Query | Ask; wait for a finite answer. | `integration-query`, `blob-metadata` |
|
|
701
|
+
| Effect | Tell; no answer joins the transaction. | `integration-effect`, `blob-commit`, `blob-delete` |
|
|
702
|
+
| Subscription | The world tells you, while you are listening. | `SubscriptionDef` → `EventDef` |
|
|
703
|
+
|
|
704
|
+
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.
|
|
705
|
+
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.
|
|
706
|
+
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**.
|
|
707
|
+
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.
|
|
708
|
+
5. **SHUTDOWN INVARIANT** — after `server.stop()`, no delivery reaches application state. A stopped subscription answers `stopped` to everything, including deliveries already in flight.
|
|
709
|
+
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.
|
|
710
|
+
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.
|
|
711
|
+
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()`.
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
{ kind: 'subscription', id: SUB_STATUS, integrationId: INTEGRATION, source: 'device-status',
|
|
715
|
+
eventId: EVENT_STATUS_CHANGED,
|
|
716
|
+
lifecycle: { autoStart: true, required: false, reconnect: { policy: 'exponential', maxAttempts: 5, delayMs: 1000 } },
|
|
717
|
+
delivery: { maxQueued: 32, backpressure: 'block', deduplicateBy: F_DELIVERY_ID, maxAttempts: 1, onFailure: 'report' } }
|
|
718
|
+
|
|
719
|
+
{ kind: 'storage', id: STORAGE_LOGS, blobEntityId: ENTITY_BLOB,
|
|
720
|
+
readAuthorization: <Expression>, uploadAuthorization: <Expression>,
|
|
721
|
+
acceptedMediaTypes: ['text/plain'], maxSizeBytes: 8388608 }
|
|
722
|
+
|
|
723
|
+
{ kind: 'blob-metadata', storageId: STORAGE_LOGS, blobKey: <Expression>, bindAs: SCOPE_BLOB }
|
|
724
|
+
{ kind: 'blob-commit', storageId: STORAGE_LOGS, blobKey: <Expression> }
|
|
725
|
+
{ kind: 'blob-delete', storageId: STORAGE_LOGS, blobKey: <Expression> }
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Lifecycle: `inactive → starting → active → reconnecting → failed`, plus `stopped`. Startup
|
|
729
|
+
decides what activates; application code never calls `start()`. A failed source leaves the
|
|
730
|
+
application running unless `lifecycle.required`.
|
|
731
|
+
|
|
732
|
+
Subscription vs. webhook vs. polling — three different things, do not conflate them:
|
|
733
|
+
|
|
734
|
+
| | What it is | Vocabulary |
|
|
735
|
+
| --- | --- | --- |
|
|
736
|
+
| Webhook | Externally initiated finite request; each delivery enters independently. | host `webhooks` → `EventRequest` |
|
|
737
|
+
| Subscription | Standing semantic interest in a long-lived source. | `SubscriptionDef` |
|
|
738
|
+
| Polling | You ask, repeatedly, on a schedule. | `interval` `TriggerDef` → `integration-query` |
|
|
739
|
+
|
|
740
|
+
Upload is `POST /axiom/blob/<storageId>` and download is `GET /axiom/blob/<storageId>/<key>`
|
|
741
|
+
— one host transport for every Axiom application. Application-authored upload/download
|
|
742
|
+
routes: zero.
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
agent.listSubscriptions() / agent.getSubscriptionsForIntegration(id);
|
|
746
|
+
agent.getEventForSubscription(id) / agent.getActionsReachableFromSubscription(id);
|
|
747
|
+
agent.getExternalEventSources(); // { subscriptions, events, integrations }
|
|
748
|
+
agent.listStorages() / agent.getActionsUsingStorage(id) / agent.getStoragesWithoutAccessRules();
|
|
749
|
+
|
|
750
|
+
server.subscriptionLog() / server.subscriptionStatus(id); // state, counters, last delivery, last failure
|
|
751
|
+
server.blobLog(); // storage effects and their outcomes
|
|
752
|
+
server.stageBlob(storageId, principal, upload);
|
|
753
|
+
server.authorizeBlobRead(storageId, key, principal);
|
|
754
|
+
server.authorizeBlobUpload(storageId, principal, { mediaType, size });
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Adapters: `SubscriptionAdapter` (`createScriptedSubscriptionAdapter` is the deterministic
|
|
758
|
+
fake) and `BlobStorageAdapter` (`createMemoryBlobStore`). A declared subscription or store
|
|
759
|
+
with no registered adapter fails `start()` rather than staying silently inert.
|
|
760
|
+
|
|
761
|
+
Diagnostics: `SUBSCRIPTION_ADAPTER_MISSING` `SUBSCRIPTION_START_FAILED`
|
|
762
|
+
`SUBSCRIPTION_DELIVERY_DROPPED` `SUBSCRIPTION_DELIVERY_FAILED` `BLOB_STORE_MISSING`
|
|
763
|
+
`BLOB_NOT_FOUND` `BLOB_ACCESS_DENIED` `BLOB_TOO_LARGE` `BLOB_MEDIA_TYPE_REJECTED`
|
|
764
|
+
`BLOB_OPERATION_FAILED` `BLOB_STORAGE_UNAVAILABLE` `BLOB_METADATA_FAILED`.
|
|
765
|
+
|
|
689
766
|
## Metadata classes
|
|
690
767
|
|
|
691
768
|
```ts
|
package/docs/ANTI_PATTERNS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-patterns
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-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
|
|
@@ -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.
|
|
3
|
+
Axiom 0.9.0-alpha.1. How an application crosses the trust boundary.
|
|
4
4
|
|
|
5
5
|
Until 0.5.x an Axiom application executed locally. 0.6 adds an **authority**: a generic
|
|
6
6
|
runtime that owns state, decides mutations and persists them. The same semantic graph
|
|
@@ -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
|
|
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.
|
|
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
|
package/docs/CONSTRAINTS.md
CHANGED
package/docs/EFFECTS.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# Effects
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-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.
|
|
7
7
|
|
|
8
|
+
Storage effects — `blob-commit` and `blob-delete` — ride this same outbox rather than a
|
|
9
|
+
second durability system; see [`STORAGE.md`](STORAGE.md).
|
|
10
|
+
|
|
8
11
|
## The operation
|
|
9
12
|
|
|
10
13
|
```ts
|
package/docs/EVENTS.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Events
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-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
|
-
this file is the vocabulary and the webhook delivery mechanism.
|
|
5
|
+
this file is the vocabulary and the webhook delivery mechanism. A **subscription** is the
|
|
6
|
+
other way an external fact becomes an `EventDef` payload — see
|
|
7
|
+
[`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md), which also states the distinction between a webhook,
|
|
8
|
+
a subscription and polling.
|
|
6
9
|
|
|
7
10
|
## The model
|
|
8
11
|
|
package/docs/EXPRESSIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Expressions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. An expression describes **what value is computed**. It is a tree of
|
|
4
4
|
plain data, never source text and never a callback. Evaluation is pure: an expression MUST
|
|
5
5
|
NOT change state.
|
|
6
6
|
|
package/docs/GRAPH_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Graph model
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. The `ApplicationGraph` is the authoritative representation of an
|
|
4
4
|
application. Everything else — the IR, the page, the DOM — is derived from it and is never
|
|
5
5
|
edited.
|
|
6
6
|
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Integrations
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-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
|
|
|
7
|
+
This covers the two **outbound** directions — query and effect. The inbound one, a
|
|
8
|
+
long-lived source that delivers to you, is [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md); binary
|
|
9
|
+
object storage is [`STORAGE.md`](STORAGE.md).
|
|
10
|
+
|
|
7
11
|
## The model
|
|
8
12
|
|
|
9
13
|
```ts
|
package/docs/LOCATIONS.md
CHANGED
package/docs/PRESENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
|
|
4
4
|
node. It names roles, tokens and device classes. It never names a colour, a length, a media
|
|
5
5
|
query or a CSS property.
|
|
6
6
|
|
package/docs/RUNTIME.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Runtime
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
|
|
4
4
|
contains no knowledge of any application.
|
|
5
5
|
|
|
6
6
|
## Constructing
|
|
@@ -226,6 +226,8 @@ interface RuntimeDiagnostic {
|
|
|
226
226
|
| `INTEGRATION_TIMEOUT` | An integration query did not answer within its declared `timeoutMs`. | `integration-query` | `operationId` |
|
|
227
227
|
| `INTEGRATION_RESULT_INVALID` | A provider's response did not conform to the operation's declared `resultType`. | `integration-query` | `operationId` |
|
|
228
228
|
| `INTEGRATION_QUERY_FAILED` | An integration query failed for any other reason. Never carries a provider secret. | `integration-query` | `operationId`, `retryable` |
|
|
229
|
+
| `BLOB_STORAGE_UNAVAILABLE` | No host capable of reaching an object store is configured. Only the authoritative runtime ever reaches one. | `blob-metadata` | `storageId` |
|
|
230
|
+
| `BLOB_METADATA_FAILED` | A `blob-metadata` lookup failed: no such key, a still-staged object, or the store refused. The store's own code is in `details.code` rather than replacing the diagnostic code — a provider's vocabulary is not Axiom's. | `blob-metadata` | `storageId`, `code` |
|
|
229
231
|
| `UI_NODE_MISSING` | A child id that is not a UI node in the IR. | render | — |
|
|
230
232
|
| `UNSUPPORTED_UI_NODE` | An unknown UI node kind. | render | — |
|
|
231
233
|
| `PERSISTED_STATE_UNREADABLE` | **Warning.** A stored value could not be parsed; the initial value was used. | startup | — |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Semantic contract
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
|
|
4
4
|
does not teach. Where this file and any specification in `../specs/` disagree, this file
|
|
5
5
|
describes the implementation and is authoritative.
|
|
6
6
|
|
package/docs/STATE.md
CHANGED
package/docs/STORAGE.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Storage and blobs
|
|
2
|
+
|
|
3
|
+
Axiom 0.9.0-alpha.1. How an application stores, references, serves and deletes binary data —
|
|
4
|
+
an attachment, a document, a photograph, a diagnostic log — with no filesystem path, no
|
|
5
|
+
upload route and no download route anywhere in it.
|
|
6
|
+
|
|
7
|
+
[`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) is the other half of 0.9; [`EFFECTS.md`](EFFECTS.md)
|
|
8
|
+
is the outbox machinery this reuses rather than duplicating.
|
|
9
|
+
|
|
10
|
+
## The abstraction
|
|
11
|
+
|
|
12
|
+
A **stored object addressed by an opaque key**. Never a path, an inode or a descriptor.
|
|
13
|
+
|
|
14
|
+
That is what lets the same graph run against a local directory in development, an S3-like
|
|
15
|
+
store in production and an in-memory store in a test without a single edit — and it is why
|
|
16
|
+
`readFile(path)` is not, and will not become, graph vocabulary. See
|
|
17
|
+
[`ANTI_PATTERNS.md`](ANTI_PATTERNS.md#os-io-primitives-are-not-graph-vocabulary).
|
|
18
|
+
|
|
19
|
+
**Bytes never enter the graph, the Server IR or canonical state.** What state holds is a
|
|
20
|
+
`BlobRef`. Transfer of the bytes is out of band, through the host's own transport.
|
|
21
|
+
|
|
22
|
+
## BlobRef
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
graph.addNode<EntityDef>(blobRefEntity(ENTITY_BLOB));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
An ordinary entity, built once per graph, with reserved field ids:
|
|
29
|
+
|
|
30
|
+
| Field | Type | Required | Meaning |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `BLOB_KEY_FIELD` | string | yes | The opaque key. The identity field. |
|
|
33
|
+
| `BLOB_MEDIA_TYPE_FIELD` | string | yes | The object's media type. |
|
|
34
|
+
| `BLOB_SIZE_FIELD` | number | yes | Bytes. |
|
|
35
|
+
| `BLOB_FILENAME_FIELD` | string | no | The name it was uploaded under. |
|
|
36
|
+
| `BLOB_CHECKSUM_FIELD` | string | no | A digest, when the store computes one. |
|
|
37
|
+
|
|
38
|
+
Those five fields are the **whole** public contract. A `BlobRef` crossing to a client carries
|
|
39
|
+
no bucket, container, region, account, path, provider name or lifecycle bookkeeping. Field ids
|
|
40
|
+
are graph-global, so one graph declares one blob entity and every store and every attachment
|
|
41
|
+
field references it.
|
|
42
|
+
|
|
43
|
+
Store it wherever an attachment belongs:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Document { id, title, attachment: BlobRef }
|
|
47
|
+
{ id: F_DOCUMENT_ATTACHMENT, valueType: optionalType(entityType(ENTITY_BLOB)) }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A `BlobRef` is ordinary authoritative state: it persists, it survives a restart, it is
|
|
51
|
+
observable, and its identity is stable enough to hold in a record.
|
|
52
|
+
|
|
53
|
+
**Checksums are offered, never required.** Content addressing is a legitimate storage model
|
|
54
|
+
and a poor universal one, so a store that computes a digest publishes it and one that does
|
|
55
|
+
not omits it.
|
|
56
|
+
|
|
57
|
+
## StorageDef
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
interface StorageDef {
|
|
61
|
+
id: NodeId;
|
|
62
|
+
kind: 'storage';
|
|
63
|
+
blobEntityId: NodeId;
|
|
64
|
+
readAuthorization?: Expression;
|
|
65
|
+
uploadAuthorization?: Expression;
|
|
66
|
+
acceptedMediaTypes?: string[];
|
|
67
|
+
maxSizeBytes?: number;
|
|
68
|
+
retry?: { policy: 'none' | 'fixed' | 'exponential'; maxAttempts?: number; delayMs?: number };
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`StorageDef` is to blobs what `IntegrationDef` is to operations: the semantic capability,
|
|
73
|
+
never the provider. No bucket, endpoint, region, credential or directory appears here or
|
|
74
|
+
anywhere else in a graph — a `BlobStorageAdapter` supplies all of it. A local filesystem
|
|
75
|
+
adapter is perfectly legitimate; its paths are host configuration.
|
|
76
|
+
|
|
77
|
+
## Authorization
|
|
78
|
+
|
|
79
|
+
**Possession of a key is not permission.** `readAuthorization` is evaluated by the authority
|
|
80
|
+
before a single byte is served, with the caller bound to `PRINCIPAL` and the requested
|
|
81
|
+
`BlobRef` bound to `ref(<this storage's id>)`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
graph.addNode<StorageDef>({
|
|
85
|
+
id: STORAGE_DIAGNOSTICS,
|
|
86
|
+
kind: 'storage',
|
|
87
|
+
blobEntityId: ENTITY_BLOB,
|
|
88
|
+
// Readable only while some device actually references it.
|
|
89
|
+
readAuthorization: some(ref(STATE_DEVICES), SCOPE, binary('eq',
|
|
90
|
+
field(field(ref(SCOPE), F_DEVICE_LOG), BLOB_KEY_FIELD),
|
|
91
|
+
field(ref(STORAGE_DIAGNOSTICS), BLOB_KEY_FIELD))),
|
|
92
|
+
uploadAuthorization: binary('eq', field(ref(PRINCIPAL), F_ROLE), literal('operator')),
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
MUST: a store with **no** rule serves nothing and accepts nothing. The safe default for a
|
|
97
|
+
missing access rule is refusal, so an author who forgets one gets a closed door.
|
|
98
|
+
|
|
99
|
+
A key that names nothing and a key the caller may not read are answered **identically**
|
|
100
|
+
(`BLOB_ACCESS_DENIED`), deliberately: distinguishing them would turn the endpoint into an
|
|
101
|
+
oracle a hostile client could enumerate keys with.
|
|
102
|
+
|
|
103
|
+
Delete goes through an action, so an action's ordinary `authorization` governs it.
|
|
104
|
+
|
|
105
|
+
## Operations
|
|
106
|
+
|
|
107
|
+
| Operation | Shape | When it runs |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `blob-metadata` | query-like | Resolved **before** the transaction opens, binds the `BlobRef` into scope via `bindAs`. |
|
|
110
|
+
| `blob-commit` | effect-like | Recorded as intent; dispatched **after** the transaction commits. |
|
|
111
|
+
| `blob-delete` | effect-like | The same. |
|
|
112
|
+
|
|
113
|
+
None is legal inside a `for-each`. All three make their action server-authority, exactly as an
|
|
114
|
+
integration operation does.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
{ kind: 'blob-metadata', storageId: STORAGE, blobKey: <Expression>, bindAs: SCOPE }
|
|
118
|
+
{ kind: 'blob-commit', storageId: STORAGE, blobKey: <Expression>, succeededEventId?, failedEventId? }
|
|
119
|
+
{ kind: 'blob-delete', storageId: STORAGE, blobKey: <Expression>, succeededEventId?, failedEventId? }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`blob-metadata` returns the `BlobRef` and never the bytes. A key that names nothing, or names
|
|
123
|
+
a still-staged object, **fails the invocation** rather than binding a plausible empty record —
|
|
124
|
+
`BLOB_METADATA_FAILED`, with the store's own code in `details.code`.
|
|
125
|
+
|
|
126
|
+
There is no `list`. Add one only when a real application scenario requires it.
|
|
127
|
+
|
|
128
|
+
## Lifecycle: staged, then committed
|
|
129
|
+
|
|
130
|
+
An object has two lifecycle values:
|
|
131
|
+
|
|
132
|
+
| | Meaning |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `staged` | It exists and has a key. **Nothing references it.** |
|
|
135
|
+
| `stored` | A committed transaction claimed it, through `blob-commit`. |
|
|
136
|
+
|
|
137
|
+
This exists because **external object storage does not participate in an Axiom transaction**,
|
|
138
|
+
and pretending otherwise would be a lie. The consequences are stated rather than hidden:
|
|
139
|
+
|
|
140
|
+
**Storage succeeds, state commit fails.** The upload is already staged. The transaction that
|
|
141
|
+
meant to reference it rolls back, so the `blob-commit` intent is discarded with every other
|
|
142
|
+
effect of that transaction and the object stays `staged`. `BlobStorageAdapter.listStaged()`
|
|
143
|
+
enumerates them, so an orphan is a sweepable staged object rather than a committed object
|
|
144
|
+
nothing points at.
|
|
145
|
+
|
|
146
|
+
**State deletion succeeds, blob deletion fails.** The transaction committed: the record no
|
|
147
|
+
longer references the object, and that is correct. The `blob-delete` effect then failed, and
|
|
148
|
+
it is visible in `server.blobLog()` with its status and `lastError`. State correctness and
|
|
149
|
+
external cleanup remain **separately observable** rather than falsely coupled — the alternative
|
|
150
|
+
would be rolling back correct state because a remote store was briefly unreachable.
|
|
151
|
+
|
|
152
|
+
Orphan policy, therefore: **staged upload + commit, plus a host-side sweep of staged objects.**
|
|
153
|
+
Nothing pretends the store joined the transaction.
|
|
154
|
+
|
|
155
|
+
## Upload
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
user selects a file
|
|
159
|
+
↓
|
|
160
|
+
POST /axiom/blob/<storageId> ← the host's transport, identical for every application
|
|
161
|
+
↓ (uploadAuthorization, acceptedMediaTypes, maxSizeBytes all checked here)
|
|
162
|
+
BlobRef ← staged
|
|
163
|
+
↓
|
|
164
|
+
action argument
|
|
165
|
+
↓
|
|
166
|
+
authoritative state + blob-commit
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Application-authored upload routes: **zero**. `POST /axiom/blob/<storageId>` is the endpoint
|
|
170
|
+
for every Axiom application there will ever be, exactly as `POST /axiom` is the semantic one.
|
|
171
|
+
The request body is the bytes; `content-type` is the media type; the optional
|
|
172
|
+
`x-axiom-filename` header carries the filename. The response is `{ kind: 'blob', ref }`.
|
|
173
|
+
|
|
174
|
+
Programmatically, the same path is `server.stageBlob(storageId, principal, upload)`.
|
|
175
|
+
|
|
176
|
+
## Download
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
GET /axiom/blob/<storageId>/<key>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Application-authored download routes: **zero**. The authority evaluates the store's
|
|
183
|
+
`readAuthorization` against the caller, then the host streams the bytes with the object's
|
|
184
|
+
media type and, when it has one, a `content-disposition` naming the stored filename. The
|
|
185
|
+
opaque key is never offered as a filename.
|
|
186
|
+
|
|
187
|
+
Programmatically: `server.authorizeBlobRead(storageId, key, principal)`.
|
|
188
|
+
|
|
189
|
+
## Blobs are never base64 in state
|
|
190
|
+
|
|
191
|
+
MUST NOT: encode an object's contents into a `LiteralValue`, a state value, an action
|
|
192
|
+
argument or the Server IR. A 5MB attachment leaves the record it is attached to exactly as
|
|
193
|
+
large as a 5-byte one, because what the record holds is five scalars.
|
|
194
|
+
|
|
195
|
+
## Adapters
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
interface BlobStorageAdapter {
|
|
199
|
+
stage(upload): Promise<BlobStoreResult<StoredBlob>>;
|
|
200
|
+
commit(key): Promise<BlobStoreResult<StoredBlob>>;
|
|
201
|
+
metadata(key): Promise<BlobStoreResult<StoredBlob>>;
|
|
202
|
+
read(key): Promise<BlobStoreResult<{ blob: StoredBlob; data: Uint8Array }>>;
|
|
203
|
+
delete(key): Promise<BlobStoreResult<null>>;
|
|
204
|
+
listStaged?(olderThanMs?): Promise<StoredBlob[]>;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Registered per `StorageDef.id` on `createAxiomServer({ blobStores })` or
|
|
209
|
+
`serveAxiomApplication({ blobStores })`. A declared store with no adapter fails `start()`.
|
|
210
|
+
|
|
211
|
+
`stage` is the only write path, deliberately: there is no point at which a store could be
|
|
212
|
+
told to write an object that the transaction referencing it might still refuse.
|
|
213
|
+
|
|
214
|
+
`createMemoryBlobStore()` implements the complete lifecycle deterministically, with seeding
|
|
215
|
+
and per-operation failure injection for tests and conformance fixtures.
|
|
216
|
+
|
|
217
|
+
## Idempotency and retries
|
|
218
|
+
|
|
219
|
+
Storage effects ride the **same** transactional outbox integration effects do: committed
|
|
220
|
+
atomically with the state that references the object, dispatched post-commit, retried under
|
|
221
|
+
`StorageDef.retry`, and reported through `server.blobLog()` (a filtered view of
|
|
222
|
+
`effectLog()`). There is no second durability system.
|
|
223
|
+
|
|
224
|
+
## Observability
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
server.blobLog(); // EffectRecord[] whose `storage` is set
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Each carries `storage.operation` (`'commit'` / `'delete'`), `storage.key`, `status`,
|
|
231
|
+
`attempts` and `lastError`. An uncommitted upload and a failed external deletion are both
|
|
232
|
+
visible here; neither is a silent orphan.
|
|
233
|
+
|
|
234
|
+
## Contract
|
|
235
|
+
|
|
236
|
+
Storage vocabulary requires `axiom.server.v5`, computed from the document. See
|
|
237
|
+
[`AUTHORITY.md`](AUTHORITY.md#contract-identifiers).
|
|
238
|
+
|
|
239
|
+
## Diagnostic codes
|
|
240
|
+
|
|
241
|
+
| Code | Meaning |
|
|
242
|
+
| --- | --- |
|
|
243
|
+
| `UNKNOWN_STORAGE` | (validation) A `storageId` or `blobEntityId` that does not resolve. |
|
|
244
|
+
| `INVALID_BLOB_ENTITY` | (validation) The named entity is not the `blobRefEntity()` shape. |
|
|
245
|
+
| `INVALID_BLOB_OPERATION` | (validation) A blob operation that cannot execute as written. |
|
|
246
|
+
| `BLOB_STORE_MISSING` | No adapter is registered for the store. |
|
|
247
|
+
| `BLOB_NOT_FOUND` | No object with that key, or the key names a still-staged upload. |
|
|
248
|
+
| `BLOB_ACCESS_DENIED` | The caller may not read, download or upload. Also the answer for a key that names nothing. |
|
|
249
|
+
| `BLOB_TOO_LARGE` | The upload exceeds `maxSizeBytes`. |
|
|
250
|
+
| `BLOB_MEDIA_TYPE_REJECTED` | The media type is not in `acceptedMediaTypes`. |
|
|
251
|
+
| `BLOB_OPERATION_FAILED` | A `blob-commit` or `blob-delete` failed at the store after its retry policy. |
|
|
252
|
+
| `BLOB_STORAGE_UNAVAILABLE` | (runtime) The host provides no object storage at all. |
|
|
253
|
+
| `BLOB_METADATA_FAILED` | (runtime) A `blob-metadata` lookup failed; the store's own code is in `details.code`. |
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Subscriptions
|
|
2
|
+
|
|
3
|
+
Axiom 0.9.0-alpha.1. How an application receives a stream of external events — an MQTT
|
|
4
|
+
topic, a WebSocket feed, a queue consumer, a filesystem watcher, a serial port — without a
|
|
5
|
+
client, a socket or a callback anywhere in the graph.
|
|
6
|
+
|
|
7
|
+
[`INTEGRATIONS.md`](INTEGRATIONS.md) is the outbound half (query and effect); this is the
|
|
8
|
+
inbound one. [`AUTHORITY.md`](AUTHORITY.md) is the trust boundary all three sit behind.
|
|
9
|
+
|
|
10
|
+
## The three directions
|
|
11
|
+
|
|
12
|
+
Axiom's whole external-interaction model is three directions and no more:
|
|
13
|
+
|
|
14
|
+
| | Shape | Vocabulary |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| **Query** | Ask, and wait for a finite answer. | `integration-query` on an `IntegrationOperationDef{mode:'query'}` |
|
|
17
|
+
| **Effect** | Tell the world to do something. No answer joins the transaction. | `integration-effect` on an `IntegrationOperationDef{mode:'effect'}` |
|
|
18
|
+
| **Subscription** | The world tells *you*, for as long as you are listening. | `SubscriptionDef` → `EventDef` |
|
|
19
|
+
|
|
20
|
+
Storage is not a fourth direction: a metadata lookup is a query, a commit or a delete is an
|
|
21
|
+
effect. See [`STORAGE.md`](STORAGE.md).
|
|
22
|
+
|
|
23
|
+
## The model
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
interface SubscriptionDef {
|
|
27
|
+
id: NodeId;
|
|
28
|
+
kind: 'subscription';
|
|
29
|
+
integrationId: NodeId;
|
|
30
|
+
source?: string;
|
|
31
|
+
arguments?: Record<string, Expression>;
|
|
32
|
+
eventId: NodeId;
|
|
33
|
+
lifecycle?: {
|
|
34
|
+
autoStart?: boolean;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
reconnect?: { policy: 'none' | 'fixed' | 'exponential'; maxAttempts?: number; delayMs?: number };
|
|
37
|
+
};
|
|
38
|
+
delivery?: {
|
|
39
|
+
maxQueued?: number;
|
|
40
|
+
backpressure?: 'block' | 'reject' | 'drop-oldest' | 'drop-newest';
|
|
41
|
+
deduplicateBy?: FieldId;
|
|
42
|
+
deduplicationWindow?: number;
|
|
43
|
+
maxAttempts?: number;
|
|
44
|
+
onFailure?: 'report' | 'pause';
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`integrationId` names the capability domain whose adapter maintains the source. `source` is
|
|
50
|
+
a **semantic** name — `'device-status'`, `'inbound-orders'` — that the adapter maps to a
|
|
51
|
+
topic, a URL, a queue or a device; the graph never learns which. Absent, the subscription's
|
|
52
|
+
own id is the name.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
graph.addNode<SubscriptionDef>({
|
|
56
|
+
id: SUBSCRIPTION_DEVICE_STATUS,
|
|
57
|
+
kind: 'subscription',
|
|
58
|
+
name: 'live device status',
|
|
59
|
+
integrationId: INTEGRATION_DEVICE_PROVIDER,
|
|
60
|
+
source: 'device-status',
|
|
61
|
+
eventId: EVENT_DEVICE_STATUS_CHANGED,
|
|
62
|
+
delivery: { deduplicateBy: F_CHANGE_DELIVERY_ID, maxQueued: 32 },
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## The pipeline
|
|
67
|
+
|
|
68
|
+
There is no second event system. A delivery enters the one that already exists:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
external source → SubscriptionAdapter → SubscriptionDef → EventDef → TriggerDef → ActionDef
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
MUST NOT: invoke an application callback, register a handler, or mutate state from the
|
|
75
|
+
adapter. An adapter's only semantic act is calling `deliver`.
|
|
76
|
+
|
|
77
|
+
## Rules
|
|
78
|
+
|
|
79
|
+
- A `SubscriptionDef` MUST name an `EventDef` that at least one `TriggerDef{when:{kind:'event'}}`
|
|
80
|
+
is bound to. A live source feeding nothing is `SUBSCRIPTION_EVENT_UNREACHABLE`.
|
|
81
|
+
- A `SubscriptionDef` MUST appear in a graph with server-authoritative state, or it is
|
|
82
|
+
`SUBSCRIPTION_WITHOUT_AUTHORITY` — nothing would activate it.
|
|
83
|
+
- Subscriptions are **server-only**. `compileToIR` strips them from the client IR exactly as
|
|
84
|
+
it strips integrations. There is no client-side subscription and none is compiled inert;
|
|
85
|
+
`validateForBrowser` and `compileToIR` therefore agree by construction.
|
|
86
|
+
- `arguments` are evaluated **once, at activation**, in the root scope. A live source is not
|
|
87
|
+
re-negotiated per message, so nothing per-delivery is in scope there.
|
|
88
|
+
- A subscription-originated action runs with `source: 'system'`. Declare
|
|
89
|
+
`invocation: { allowedSources: ['system'] }` on it — see **Security** below.
|
|
90
|
+
|
|
91
|
+
## Lifecycle
|
|
92
|
+
|
|
93
|
+
Six states, and only these transitions:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
inactive ──start──▶ starting ──ok──▶ active ──stop──▶ stopped
|
|
97
|
+
│ │
|
|
98
|
+
│ fail │ transport lost
|
|
99
|
+
▼ ▼
|
|
100
|
+
failed ◀─exhausted─ reconnecting ──ok──▶ active
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| State | Meaning |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `inactive` | Declared, but `lifecycle.autoStart: false` — startup did not activate it. |
|
|
106
|
+
| `starting` | The first activation attempt is in flight. |
|
|
107
|
+
| `active` | The source is established and deliveries are being accepted. |
|
|
108
|
+
| `reconnecting` | The transport was lost, or an attempt failed; the reconnect policy is running. |
|
|
109
|
+
| `failed` | The reconnect policy is spent, or `onFailure: 'pause'` stopped it. Terminal for this process. |
|
|
110
|
+
| `stopped` | `server.stop()`. Accepts nothing, ever again. |
|
|
111
|
+
|
|
112
|
+
**Startup owns activation.** `AxiomServer.start()` activates every `autoStart` subscription,
|
|
113
|
+
after persistence has loaded and pending effects have resumed. Application code MUST NOT —
|
|
114
|
+
and cannot — call `subscription.start()`: there is no such method.
|
|
115
|
+
|
|
116
|
+
**A failed source does not stop the application.** `lifecycle.required` is false by default,
|
|
117
|
+
so an unreachable feed leaves the application serving requests with that subscription
|
|
118
|
+
observably `reconnecting` then `failed`. Set `required: true` only when the application is
|
|
119
|
+
not meaningfully running without the source; `start()` then rejects.
|
|
120
|
+
|
|
121
|
+
**Restart recreates from the graph.** A live connection is not persisted. The graph is the
|
|
122
|
+
declaration of desired subscription state, so a restarted process reactivates every
|
|
123
|
+
`autoStart` subscription from the Server IR alone.
|
|
124
|
+
|
|
125
|
+
**Reconnect policy is Axiom's; reconnect mechanics are the adapter's.** An adapter reports a
|
|
126
|
+
lost transport through `connectionLost()`; how many attempts, how far apart and when to give
|
|
127
|
+
up come from `lifecycle.reconnect`, so the answer does not change with the provider. Default:
|
|
128
|
+
exponential, 5 attempts, 1000ms base.
|
|
129
|
+
|
|
130
|
+
**A transport failure is not a domain event.** It moves the lifecycle state and reports a
|
|
131
|
+
diagnostic. It becomes an `EventDef` only if an author deliberately declares one for it.
|
|
132
|
+
|
|
133
|
+
## Delivery guarantees
|
|
134
|
+
|
|
135
|
+
**Delivery is at-least-once.** A provider that redelivers, a reconnect that replays, and a
|
|
136
|
+
crash between dispatch and commit can each present the same external event twice. Axiom does
|
|
137
|
+
not claim exactly-once, because it cannot deliver it across all three.
|
|
138
|
+
|
|
139
|
+
**Deduplication makes it effectively-once where an external identity exists.**
|
|
140
|
+
`delivery.deduplicateBy` names a field of the event's payload entity carrying the provider's
|
|
141
|
+
own delivery identity — a message id, an offset, a delivery tag. A repeated value is
|
|
142
|
+
acknowledged and never dispatched. It is deliberately a payload field and not an Axiom
|
|
143
|
+
transaction id: the provider decides what identifies a delivery, and Axiom cannot invent one
|
|
144
|
+
that survives a redelivery it did not cause.
|
|
145
|
+
|
|
146
|
+
**Deduplication survives a restart** when the persistence adapter implements `hasDelivery`
|
|
147
|
+
and `recordDelivery` — `createMemoryPersistence` does. Without them the window is
|
|
148
|
+
in-process only and does **not** survive a restart; that is a real limitation, not a
|
|
149
|
+
guarantee to build on.
|
|
150
|
+
|
|
151
|
+
Deduplication is bounded by `deduplicationWindow` (default 512). A redelivery older than the
|
|
152
|
+
window is dispatched again.
|
|
153
|
+
|
|
154
|
+
## Ordering
|
|
155
|
+
|
|
156
|
+
| | Guaranteed? |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| Deliveries of **one** subscription | **Yes** — dispatched one at a time, in accepted order, each in its own transaction. |
|
|
159
|
+
| Deliveries of **two** subscriptions | **No.** None. They are independent sources. |
|
|
160
|
+
| A delivery against a client request or a trigger tick | Only that each takes its own serialized turn. No relative order. |
|
|
161
|
+
|
|
162
|
+
Two subscriptions do enter the same serialized authority queue, so their *commits* never
|
|
163
|
+
interleave — but nothing orders their *arrival*, and a consumer MUST NOT build on an
|
|
164
|
+
accident of interleaving.
|
|
165
|
+
|
|
166
|
+
## Transactions
|
|
167
|
+
|
|
168
|
+
Each accepted delivery enters authoritative execution through **one** transaction boundary.
|
|
169
|
+
Multiple deliveries never share a transaction: the runtime awaits each dispatch before taking
|
|
170
|
+
the next off the queue.
|
|
171
|
+
|
|
172
|
+
If the triggered action fails, its transaction rolls back like any other — but the external
|
|
173
|
+
provider cannot un-send the event. What happens next is `delivery.maxAttempts` (default 1,
|
|
174
|
+
i.e. no retry) and then `delivery.onFailure`.
|
|
175
|
+
|
|
176
|
+
## Backpressure
|
|
177
|
+
|
|
178
|
+
The queue is **bounded**, always. `maxQueued` defaults to 64.
|
|
179
|
+
|
|
180
|
+
| `backpressure` | When the queue is full | Loses events? |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| `block` (default) | The adapter's `deliver` call does not resolve until there is room. | **No** |
|
|
183
|
+
| `reject` | The delivery is refused; the source still holds it. | **No** |
|
|
184
|
+
| `drop-oldest` | The oldest queued delivery is discarded. | **Yes** |
|
|
185
|
+
| `drop-newest` | The arriving delivery is discarded. | **Yes** |
|
|
186
|
+
|
|
187
|
+
The default cannot lose an authoritative event: an adapter that awaits `deliver` applies real
|
|
188
|
+
backpressure to its own transport instead of buffering without limit. The two dropping modes
|
|
189
|
+
are legitimate for a genuinely lossy source — a sensor feed where only the newest reading
|
|
190
|
+
matters — and both report `SUBSCRIPTION_DELIVERY_DROPPED` and increment
|
|
191
|
+
`SubscriptionRecord.dropped`. **Loss is always declared and never silent.**
|
|
192
|
+
|
|
193
|
+
## Poison deliveries
|
|
194
|
+
|
|
195
|
+
A delivery whose action keeps failing is bounded by `delivery.maxAttempts` and then handled by
|
|
196
|
+
`delivery.onFailure`:
|
|
197
|
+
|
|
198
|
+
| `onFailure` | Behaviour |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `report` (default) | Count it as `failed`, answer the adapter `failed`, take the next delivery. |
|
|
201
|
+
| `pause` | The above, then stop the subscription — state `failed`. |
|
|
202
|
+
|
|
203
|
+
Neither ever retries without bound. There is no dead-letter queue: Axiom does not invent a
|
|
204
|
+
workflow system, and an adapter that needs one translates a `failed` outcome into its
|
|
205
|
+
provider's own.
|
|
206
|
+
|
|
207
|
+
## Acknowledgement
|
|
208
|
+
|
|
209
|
+
`deliver` resolves with a `DeliveryOutcome` describing what became of the delivery, and the
|
|
210
|
+
adapter translates it into whatever its provider needs — an `ack`, a `nack`, an offset
|
|
211
|
+
commit, or nothing:
|
|
212
|
+
|
|
213
|
+
| Status | Meaning | Typical translation |
|
|
214
|
+
| --- | --- | --- |
|
|
215
|
+
| `applied` | Dispatched; its action committed. | ack / commit offset |
|
|
216
|
+
| `duplicate` | Already seen; not dispatched again. | ack |
|
|
217
|
+
| `rejected` | The payload did not conform to the `EventDef`. | ack, or dead-letter |
|
|
218
|
+
| `failed` | Dispatched; the action failed after every permitted attempt. | nack, or dead-letter |
|
|
219
|
+
| `dropped` | Discarded under a declared lossy policy. | ack |
|
|
220
|
+
| `refused` | Refused by backpressure; the source still holds it. | nack |
|
|
221
|
+
| `stopped` | The subscription is no longer running. | nack |
|
|
222
|
+
|
|
223
|
+
Ack vocabulary lives in the adapter contract and MUST NOT appear in an `ApplicationGraph`:
|
|
224
|
+
every provider spells it differently, and a graph that named one would stop being portable.
|
|
225
|
+
|
|
226
|
+
## Payload validation
|
|
227
|
+
|
|
228
|
+
A delivered payload is **untrusted data**, even though the adapter is trusted infrastructure.
|
|
229
|
+
It is validated against the target `EventDef.payloadType` **before** anything else. An invalid
|
|
230
|
+
payload:
|
|
231
|
+
|
|
232
|
+
- does not dispatch the event,
|
|
233
|
+
- does not invoke any action,
|
|
234
|
+
- does not mutate authoritative state,
|
|
235
|
+
- reports `EVENT_PAYLOAD_INVALID` and increments `SubscriptionRecord.rejected`.
|
|
236
|
+
|
|
237
|
+
## Security
|
|
238
|
+
|
|
239
|
+
- A client **cannot** forge a delivery. There is no protocol message that delivers into a
|
|
240
|
+
subscription; a delivery exists only inside the authority, from an adapter.
|
|
241
|
+
- A client **cannot** invoke a subscription-only action. Declare
|
|
242
|
+
`invocation: { allowedSources: ['system'] }` and `INVOCATION_SOURCE_NOT_ALLOWED` refuses a
|
|
243
|
+
client-sourced call before identity is consulted.
|
|
244
|
+
- An external source **cannot** bypass authorization, preconditions, constraints, transition
|
|
245
|
+
constraints or rollback. A subscription-originated action runs through exactly the same
|
|
246
|
+
`invokeCore` a client request does, under `principal: null`.
|
|
247
|
+
- After shutdown, **no** delivery reaches state. `stop()` moves every subscription to
|
|
248
|
+
`stopped`, and a stopped subscription answers `stopped` to everything.
|
|
249
|
+
|
|
250
|
+
## Observability
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
server.subscriptionLog(); // SubscriptionRecord[]
|
|
254
|
+
server.subscriptionStatus(SUBSCRIPTION_ID); // SubscriptionRecord | undefined
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`SubscriptionRecord` carries `state`, `source`, `attempts`, `received`, `applied`,
|
|
258
|
+
`rejected`, `failed`, `dropped`, `queued`, `lastDeliveryAt` and `lastFailure`. That is the
|
|
259
|
+
whole operational picture — configured, active, reconnecting, failed, arriving, being
|
|
260
|
+
refused — and it is host observability, not an application-authored health route.
|
|
261
|
+
|
|
262
|
+
## Adapters
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
interface SubscriptionAdapter {
|
|
266
|
+
start(context: SubscriptionContext): Promise<SubscriptionHandle>;
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Registered per integration id on `createAxiomServer({ subscriptions })` or
|
|
271
|
+
`serveAxiomApplication({ subscriptions })`. A declared subscription with no registered
|
|
272
|
+
adapter fails `start()`, rather than staying silently inactive.
|
|
273
|
+
|
|
274
|
+
Sockets, `EventEmitter`s, Node streams, subprocesses, serial ports and provider SDKs are all
|
|
275
|
+
permitted **inside** an adapter and nowhere above it. One adapter may multiplex many
|
|
276
|
+
subscriptions over one connection: nothing in Axiom assumes `SubscriptionDef` and TCP session
|
|
277
|
+
correspond.
|
|
278
|
+
|
|
279
|
+
`createScriptedSubscriptionAdapter(scripts, host)` is the deterministic fake — connect
|
|
280
|
+
success and failure, deliveries, duplicates, delayed deliveries, disconnection and
|
|
281
|
+
reconnection, all as data against a virtual clock, with no network and no wall-clock wait.
|
|
282
|
+
The same script shape appears in the portable conformance fixtures.
|
|
283
|
+
|
|
284
|
+
## Subscription vs. webhook vs. polling
|
|
285
|
+
|
|
286
|
+
| | What it is | How it reaches Axiom |
|
|
287
|
+
| --- | --- | --- |
|
|
288
|
+
| **Webhook** | An externally initiated **finite request**. Each delivery independently enters Axiom. | The host verifies it, then sends one `EventRequest`. See [`EVENTS.md`](EVENTS.md). |
|
|
289
|
+
| **Subscription** | Axiom maintains a standing **semantic interest** in a long-lived source. Deliveries occur while it is active. | The adapter calls `deliver` while the subscription is `active`. |
|
|
290
|
+
| **Polling** | Axiom asks, repeatedly, on a schedule. | `TriggerDef{when:{kind:'interval'}}` → `integration-query`. See [`TRIGGERS.md`](TRIGGERS.md). |
|
|
291
|
+
|
|
292
|
+
Do not hide polling behind subscription syntax: a source you have to ask is a query on a
|
|
293
|
+
timer, and saying so keeps the graph honest about what it does to the provider.
|
|
294
|
+
|
|
295
|
+
`TriggerDef` keeps timer and lifecycle semantics; `SubscriptionDef` keeps external long-lived
|
|
296
|
+
sources. Neither replaces the other.
|
|
297
|
+
|
|
298
|
+
## Contract
|
|
299
|
+
|
|
300
|
+
Subscription vocabulary requires `axiom.server.v5`, computed from the document — a graph that
|
|
301
|
+
uses none of it compiles to the byte-identical older document it always did. See
|
|
302
|
+
[`AUTHORITY.md`](AUTHORITY.md#contract-identifiers).
|
|
303
|
+
|
|
304
|
+
## Diagnostic codes
|
|
305
|
+
|
|
306
|
+
| Code | Meaning |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
| `SUBSCRIPTION_EVENT_UNREACHABLE` | (validation) The event has no trigger bound to it. |
|
|
309
|
+
| `SUBSCRIPTION_WITHOUT_AUTHORITY` | (validation) The graph has no server-authoritative state. |
|
|
310
|
+
| `SUBSCRIPTION_INVALID_POLICY` | (validation) A queue below one, no attempts, an unknown backpressure policy, or a `deduplicateBy` that is not a field of the payload entity. |
|
|
311
|
+
| `SUBSCRIPTION_ADAPTER_MISSING` | No adapter is registered for the integration. |
|
|
312
|
+
| `SUBSCRIPTION_START_FAILED` | The source could not be established after every reconnect attempt. |
|
|
313
|
+
| `SUBSCRIPTION_DELIVERY_DROPPED` | A delivery was discarded under a declared lossy policy. |
|
|
314
|
+
| `SUBSCRIPTION_DELIVERY_FAILED` | A delivery's action failed after every permitted attempt. |
|
|
315
|
+
| `EVENT_PAYLOAD_INVALID` | The payload did not conform to the `EventDef.payloadType`. |
|
package/docs/TRIGGERS.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Triggers
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-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.
|
|
7
7
|
|
|
8
|
+
A trigger covers time (`interval`, `delay`), the application lifecycle, and a dispatched
|
|
9
|
+
event. It does **not** cover a long-lived external source: that is a
|
|
10
|
+
[`SubscriptionDef`](SUBSCRIPTIONS.md), whose deliveries become `EventDef` payloads that
|
|
11
|
+
`event` triggers then react to. Keep the two separate — an interval trigger driving an
|
|
12
|
+
`integration-query` is polling, and polling is not a subscription.
|
|
13
|
+
|
|
8
14
|
## The model
|
|
9
15
|
|
|
10
16
|
```ts
|
package/docs/UI.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UI
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. Eleven semantic UI node kinds describe **what exists and what it does**.
|
|
4
4
|
How it looks is [presentation](PRESENTATION.md).
|
|
5
5
|
|
|
6
6
|
All eleven share `UIBase`:
|
package/docs/VALIDATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Validation
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.9.0-alpha.1. Validation is authoring-time structural checking. It is not the same
|
|
4
4
|
as runtime constraint evaluation — see [`CONSTRAINTS.md`](CONSTRAINTS.md) for the four
|
|
5
5
|
layers of correctness.
|
|
6
6
|
|
|
@@ -134,6 +134,19 @@ Full model: [`INTEGRATIONS.md`](INTEGRATIONS.md), [`EFFECTS.md`](EFFECTS.md),
|
|
|
134
134
|
| `INVALID_INVOCATION_SOURCE` | `ActionDef.invocation.allowedSources` is present but empty — the action could never be invoked at all. |
|
|
135
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. |
|
|
136
136
|
|
|
137
|
+
### Subscriptions and object storage
|
|
138
|
+
|
|
139
|
+
Full model: [`SUBSCRIPTIONS.md`](SUBSCRIPTIONS.md) and [`STORAGE.md`](STORAGE.md).
|
|
140
|
+
|
|
141
|
+
| Code | Raised when |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `SUBSCRIPTION_EVENT_UNREACHABLE` | A `SubscriptionDef` whose `eventId` no `TriggerDef{when:{kind:'event'}}` is bound to. Every delivery would be validated and then discarded — a live source feeding nothing. |
|
|
144
|
+
| `SUBSCRIPTION_WITHOUT_AUTHORITY` | A `SubscriptionDef` in a graph with no server-authoritative state. Subscriptions are authority-side; nothing would ever activate it. |
|
|
145
|
+
| `SUBSCRIPTION_INVALID_POLICY` | A delivery or lifecycle policy that cannot be executed as written: `maxQueued` below one, `maxAttempts` below one, an unknown backpressure policy, or a `deduplicateBy` that is not a field of the event's payload entity. |
|
|
146
|
+
| `UNKNOWN_STORAGE` | A blob operation's `storageId`, or a `StorageDef.blobEntityId`, that does not resolve. |
|
|
147
|
+
| `INVALID_BLOB_ENTITY` | A `StorageDef.blobEntityId` naming an entity that is not the canonical `blobRefEntity()` shape. The message names the missing fields. |
|
|
148
|
+
| `INVALID_BLOB_OPERATION` | A blob operation that cannot execute as written. |
|
|
149
|
+
|
|
137
150
|
### UI and routing
|
|
138
151
|
|
|
139
152
|
| Code | Raised when | Severity |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-alpha.1",
|
|
4
4
|
"description": "AI-native semantic web application framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@cynodia/axiom-core": "0.
|
|
36
|
-
"@cynodia/axiom-runtime": "0.
|
|
37
|
-
"@cynodia/axiom-compiler": "0.
|
|
38
|
-
"@cynodia/axiom-agent-api": "0.
|
|
35
|
+
"@cynodia/axiom-core": "0.9.0-alpha.1",
|
|
36
|
+
"@cynodia/axiom-runtime": "0.9.0-alpha.1",
|
|
37
|
+
"@cynodia/axiom-compiler": "0.9.0-alpha.1",
|
|
38
|
+
"@cynodia/axiom-agent-api": "0.9.0-alpha.1"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -b tsconfig.json"
|