@cynodia/axiom 0.6.0-alpha.1 → 0.6.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/ACTIONS_TRANSACTIONS.md +17 -1
- package/docs/AGENT_API.md +3 -2
- package/docs/AGENT_REFERENCE.md +60 -14
- package/docs/ANTI_PATTERNS.md +1 -1
- package/docs/AUTHORITY.md +276 -23
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +1 -1
- package/docs/LOCATIONS.md +1 -1
- package/docs/PRESENTATION.md +1 -1
- package/docs/RUNTIME.md +35 -7
- package/docs/SEMANTIC_CONTRACT.md +18 -8
- package/docs/STATE.md +1 -1
- package/docs/UI.md +67 -7
- package/docs/VALIDATION.md +3 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Actions and transactions
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. An action is behavior expressed as data, executed as a transaction.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
{
|
|
@@ -40,6 +40,22 @@ result.diagnostics[0].details // { preconditionIndex: 2, failureMode: 'insuffic
|
|
|
40
40
|
|
|
41
41
|
`actionGuards(action)` returns the conditions however they were written.
|
|
42
42
|
|
|
43
|
+
### Guards evaluate in order, and the first failure stops
|
|
44
|
+
|
|
45
|
+
Declaration order is evaluation order. The first guard that does not hold refuses the
|
|
46
|
+
invocation, and evaluation stops there: later guards are not evaluated, and exactly one
|
|
47
|
+
`PRECONDITION_FAILED` is reported, naming that guard by position and by failure mode.
|
|
48
|
+
|
|
49
|
+
This is the contract, not an implementation detail, and it has three consequences worth
|
|
50
|
+
writing an action around:
|
|
51
|
+
|
|
52
|
+
- **A guard may rely on the guards before it.** Ordering `required(x)` ahead of a guard that reads a field of `x` is how the second one is kept evaluable.
|
|
53
|
+
- **A refusal names one cause.** Failures are not aggregated. An interface that wants to show every problem at once should express them as constraints, which are evaluated over the whole proposed state, rather than as guards.
|
|
54
|
+
- **A guard that cannot be evaluated fails.** It refuses, exactly as an unevaluable constraint counts as violated — never passes.
|
|
55
|
+
|
|
56
|
+
Aggregating guard failures would be a different feature with a different diagnostic shape,
|
|
57
|
+
and is deliberately not this one.
|
|
58
|
+
|
|
43
59
|
## Lifecycle
|
|
44
60
|
|
|
45
61
|
```text
|
package/docs/AGENT_API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent API
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. The machine-facing interface. Agents query semantics and apply
|
|
4
4
|
structural transformations; they never edit generated code.
|
|
5
5
|
|
|
6
6
|
```ts
|
|
@@ -121,7 +121,8 @@ reported.
|
|
|
121
121
|
```ts
|
|
122
122
|
{
|
|
123
123
|
formId, density,
|
|
124
|
-
submitActionId?, // submitActionId, or the declared submit button's own action
|
|
124
|
+
submitActionId?, // submitActionId, or the declared submit button's own action —
|
|
125
|
+
// the same resolution execution, validation and presentation use
|
|
125
126
|
submitButtonId?, // set when the form declares its submit control
|
|
126
127
|
sections: [{ nodeId, name?, headings: string[], inputIds }],
|
|
127
128
|
ungroupedInputIds, // controls belonging to no section
|
package/docs/AGENT_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent reference
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
|
|
4
4
|
declarations before authoring or modifying an Axiom application.
|
|
5
5
|
|
|
6
6
|
Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
|
|
@@ -408,13 +408,22 @@ not a constraint. Enforcement belongs to guards, constraints and transition cons
|
|
|
408
408
|
|
|
409
409
|
```ts
|
|
410
410
|
const ir = compileToIR(graph); // throws GraphValidationError if invalid
|
|
411
|
-
const html = compileToHtml(graph, { title?, appearance? });
|
|
411
|
+
const html = compileToHtml(graph, { title?, appearance?, remote? });
|
|
412
412
|
const css = createThemeStylesheet(ir.theme);
|
|
413
413
|
|
|
414
|
-
const app = createAxiomRuntime({ ir, rootElement, host, nativeOperations?, inputValidation?, recordMutationValues? });
|
|
415
|
-
app.start();
|
|
414
|
+
const app = createAxiomRuntime({ ir, rootElement, host, remote?, nativeOperations?, inputValidation?, recordMutationValues? });
|
|
415
|
+
await app.start(); // render → restore → load authoritative state
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
+
`start()` renders synchronously and then loads authoritative state when a gateway is
|
|
419
|
+
configured; awaiting it means that has happened. **The gateway must be passed to
|
|
420
|
+
`createAxiomRuntime`, before `start()`.** A failed load reports `AUTHORITY_UNREACHABLE` and
|
|
421
|
+
leaves `authoritativeStateLoaded()` false — it never throws, and never looks like empty data.
|
|
422
|
+
|
|
423
|
+
`compileToHtml` wires the browser-safe gateway into the generated page whenever the IR
|
|
424
|
+
contains a remote action, so a server-authoritative application needs no client JavaScript
|
|
425
|
+
of its own. `remote: { endpoint }` points it elsewhere; `remote: false` switches it off.
|
|
426
|
+
|
|
418
427
|
`compileToIR` refuses an invalid graph. Pass `{ validate: false }` only for diagnostics.
|
|
419
428
|
|
|
420
429
|
Hosts: `createBrowserHost()` for a page, `createMemoryHost()` for headless use. The runtime
|
|
@@ -431,7 +440,11 @@ reads nothing from globals.
|
|
|
431
440
|
| `diagnostics()` / `clearDiagnostics()` | — | Running log. |
|
|
432
441
|
| `getMutationLog()` | — | Every attempted mutation with source, path and `outcome`. |
|
|
433
442
|
| `registerNativeOperation(id, fn)` | — | |
|
|
434
|
-
| `start()` / `render()` | — | Rendering is a full re-render; focus and caret are restored by node id. |
|
|
443
|
+
| `start()` / `render()` | — | `start()` is render → restore → synchronize, and returns a promise. Rendering is a full re-render; focus and caret are restored by node id. |
|
|
444
|
+
| `invokeActionAsync(id, args?)` | yes | Awaits the outcome, an authority's answer included. |
|
|
445
|
+
| `syncAuthoritativeState()` | — | Loads and applies the authoritative snapshot. Idempotent. |
|
|
446
|
+
| `authoritativeStateLoaded()` | — | Whether a snapshot has been applied. Not the same question as "is this collection empty". |
|
|
447
|
+
| `settled()` | — | Resolves when no remote invocation is outstanding — how to await an action a click or a form submit started. |
|
|
435
448
|
|
|
436
449
|
## Diagnostics
|
|
437
450
|
|
|
@@ -481,6 +494,12 @@ authoring an application that crosses the trust boundary.
|
|
|
481
494
|
5. **CONCURRENCY** — two actions cannot both commit from incompatible snapshots.
|
|
482
495
|
6. **PROTOCOL** — a client requests semantic actions, never mutation programs.
|
|
483
496
|
7. **SERIALIZATION** — authoritative behavior is data. No closure, no arbitrary code.
|
|
497
|
+
8. **BOOTSTRAP** — a remote client is given its gateway before it starts.
|
|
498
|
+
9. **STARTUP** — `start()` renders, then synchronizes; a failed load is a diagnostic.
|
|
499
|
+
10. **FORM SUBMIT** — a declared submit button invokes with its own arguments, clicked or submitted.
|
|
500
|
+
11. **IDEMPOTENCY** — a generated request id is unique across runtime instances; records are scoped by principal.
|
|
501
|
+
12. **CHANGES** — `changes` names every observable state whose value moved, and no others.
|
|
502
|
+
13. **PORTABILITY** — `axiom.server.v1` is frozen and language-independent.
|
|
484
503
|
|
|
485
504
|
```ts
|
|
486
505
|
{ id: STATE_PRODUCTS, kind: 'state', authority: 'server' } // the authority owns it
|
|
@@ -506,23 +525,50 @@ graph.setPrincipalEntity(ENTITY_USER);
|
|
|
506
525
|
```
|
|
507
526
|
|
|
508
527
|
`PRINCIPAL` is bound to a record keyed by the principal entity's field ids, and **only where
|
|
509
|
-
an authority evaluates
|
|
510
|
-
|
|
528
|
+
an authority evaluates** — which is everywhere on the authority, not only in `authorization`:
|
|
529
|
+
guards, operation values, postconditions and constraints alike. That is how a record says who
|
|
530
|
+
caused it without a client being asked to claim an identity:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
{ fieldId: F_ORDER_PLACED_BY, value: field(ref(PRINCIPAL), F_USER_ID) }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Reading it anywhere a client evaluates is `PRINCIPAL_REFERENCE_ON_CLIENT`. A rule that cannot
|
|
537
|
+
be evaluated denies. An anonymous caller has no principal: attributes read from it are absent,
|
|
538
|
+
so any rule naming one is false. `requiresConfirmation` is interaction, not authorization.
|
|
511
539
|
|
|
512
540
|
### Running one
|
|
513
541
|
|
|
514
|
-
|
|
515
|
-
const server = createAxiomServer({ ir: compileToServerIR(graph), persistence, host });
|
|
516
|
-
await server.start();
|
|
517
|
-
await serveOverHttp({ server, port: 3000 });
|
|
542
|
+
One graph, one process — the generated page and the authority that answers it:
|
|
518
543
|
|
|
519
|
-
|
|
520
|
-
await
|
|
544
|
+
```ts
|
|
545
|
+
await serveAxiomApplication({
|
|
546
|
+
serverIR: compileToServerIR(graph),
|
|
547
|
+
page: compileToHtml(graph),
|
|
548
|
+
persistence: await createSqlitePersistence({ location: 'app.db' }),
|
|
549
|
+
authenticate: (credential) => resolveUser(credential),
|
|
550
|
+
port: 3000,
|
|
551
|
+
});
|
|
521
552
|
```
|
|
522
553
|
|
|
554
|
+
`GET /` is the page, `POST /axiom` the semantic endpoint. No route, controller, handler, SQL
|
|
555
|
+
or client JavaScript is authored. **There is no published Axiom CLI.** The halves also run
|
|
556
|
+
separately: `serveOverHttp({ server, port })` is the bare authority, and
|
|
557
|
+
`createDirectTransport(server)` drives one in-process for tests.
|
|
558
|
+
|
|
523
559
|
A remote invocation returns `{ ok: false, pending: true }` and its outcome arrives later,
|
|
524
560
|
through the same action-outcome lifecycle a local refusal uses — so a `diagnostic` node
|
|
525
|
-
presents a server refusal exactly as it presents a local one
|
|
561
|
+
presents a server refusal exactly as it presents a local one, and the control that started it
|
|
562
|
+
renders `aria-busy` and refuses a second press until it settles.
|
|
563
|
+
|
|
564
|
+
Portable artifacts, for a runtime written in another language:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
@cynodia/axiom-server/conformance the fixture manifest
|
|
568
|
+
@cynodia/axiom-server/conformance/<name>.json one fixture, pure data
|
|
569
|
+
@cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for the IR
|
|
570
|
+
@cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
|
|
571
|
+
```
|
|
526
572
|
|
|
527
573
|
Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZATION_DENIED`
|
|
528
574
|
`CONCURRENCY_CONFLICT` `MALFORMED_REQUEST` `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE`
|
package/docs/ANTI_PATTERNS.md
CHANGED
package/docs/AUTHORITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Authority
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. How an application crosses the trust boundary.
|
|
4
4
|
|
|
5
5
|
Until 0.5.x an Axiom application executed locally. 0.6 adds an **authority**: a generic
|
|
6
6
|
runtime that owns state, decides mutations and persists them. The same semantic graph
|
|
@@ -26,6 +26,12 @@ describes both halves, so there is no backend to write.
|
|
|
26
26
|
5. **CONCURRENCY** — two actions cannot both commit from incompatible snapshots.
|
|
27
27
|
6. **PROTOCOL** — a client requests semantic actions, never mutation programs.
|
|
28
28
|
7. **SERIALIZATION** — authoritative behavior is data. No closure, no arbitrary code.
|
|
29
|
+
8. **REMOTE CLIENT BOOTSTRAP** — a client of a server-authoritative application is configured with a gateway *before* it starts. A generated page does this for itself; see [Running one](#running-one).
|
|
30
|
+
9. **STARTUP** — `start()` renders synchronously, then loads authoritative state. See [Startup](#startup).
|
|
31
|
+
10. **FORM SUBMIT** — a declared submit button invokes its action with exactly the arguments it declares, whether it is clicked or the form is submitted. See [UI](./UI.md#forms).
|
|
32
|
+
11. **IDEMPOTENCY** — an automatically generated request id is unique across runtime instances, whatever the host's uuid provider does. See [Idempotency](#idempotency).
|
|
33
|
+
12. **CHANGES** — `InvokeResponse.changes` names every observable state whose value moved, and no others. See [Observing authoritative state](#observing-authoritative-state).
|
|
34
|
+
13. **PORTABILITY** — `axiom.server.v1` semantics are language-independent, defined normatively by this document, the [schemas](#machine-readable-contracts) and the [conformance fixtures](#conformance). See [Server IR v1](#server-ir-v1-is-frozen).
|
|
29
35
|
|
|
30
36
|
## Authority and persistence are different questions
|
|
31
37
|
|
|
@@ -44,7 +50,7 @@ persisted to local storage. Do not conflate them.
|
|
|
44
50
|
```
|
|
45
51
|
|
|
46
52
|
- `authority` defaults to `'client'`, so **every 0.5.x graph is unchanged** and still runs with no server at all.
|
|
47
|
-
- `serverOnly: true` excludes the state from the client IR entirely. It is not merely unwritable; it is absent.
|
|
53
|
+
- `serverOnly: true` excludes the state from the client IR entirely. It is not merely unwritable; it is absent. What that does and does not hide is spelled out under [What `serverOnly` hides](#what-serveronly-hides).
|
|
48
54
|
|
|
49
55
|
## Where an action executes
|
|
50
56
|
|
|
@@ -109,6 +115,45 @@ client IR, so an authority that read one and not the other cannot silently skip
|
|
|
109
115
|
|
|
110
116
|
Entity type declarations are shared: they are the schema, not the rules.
|
|
111
117
|
|
|
118
|
+
### What `serverOnly` hides
|
|
119
|
+
|
|
120
|
+
`serverOnly` is a statement about **values and behaviour**, not about the existence of a
|
|
121
|
+
vocabulary. Stated exactly, so nothing has to be inferred from the word "absent":
|
|
122
|
+
|
|
123
|
+
| | Hidden from the client? |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| The state's value | **yes** — not in the client IR, not in a snapshot, not in any `changes` map |
|
|
126
|
+
| The state's id | **yes** — the state does not appear in the client IR at all |
|
|
127
|
+
| The state's declared type, initial value, derivation, persistence | **yes**, with the state |
|
|
128
|
+
| A constraint or transition rule that reads the state | **yes** — stripped, and enforced only on the authority |
|
|
129
|
+
| An edge naming the state | **yes** |
|
|
130
|
+
| The **entity type** the state holds | **no**, when a client-visible state or action also uses that type |
|
|
131
|
+
| That entity's **field ids, names and types** | **no**, under the same condition |
|
|
132
|
+
| The **values** a rule was judging when it refused | **yes** — see below |
|
|
133
|
+
|
|
134
|
+
The exception is not an oversight: an entity type is a *schema*, shared by everything that
|
|
135
|
+
stores instances of it, and a client that renders one legitimately needs its fields. Stripping
|
|
136
|
+
the type would require proving no client-visible state or action reaches it, and a shared type
|
|
137
|
+
makes that proof fail. **A client can therefore learn that an entity has a field, and never
|
|
138
|
+
learn any value of it.** If the *shape* of a record is itself confidential, model it as a
|
|
139
|
+
separate entity that no client-visible state or action references — then it is stripped with
|
|
140
|
+
its state, and nothing is shared to keep.
|
|
141
|
+
|
|
142
|
+
`validateGraph` enforces the boundary at authoring time with `SERVER_ONLY_STATE_OBSERVED`:
|
|
143
|
+
anything a client evaluates that reads a `serverOnly` state is a validation error rather than
|
|
144
|
+
a runtime surprise.
|
|
145
|
+
|
|
146
|
+
**A diagnostic is the other way state could cross, and it does not.** A refusal is returned to
|
|
147
|
+
the caller, and a locally-evaluated one carries the record it was judging — a transition rule's
|
|
148
|
+
`previousValue` and `proposedValue`, for instance. An authority strips those: a diagnostic
|
|
149
|
+
leaving an authority keeps its code, its authored message and its *structural* details — which
|
|
150
|
+
rule, which record's identity, which guard by position — and carries **no state value**. The
|
|
151
|
+
list is a whitelist (`DISCLOSABLE_DETAIL_KEYS`), so a detail added later is withheld until
|
|
152
|
+
somebody decides it may cross, rather than disclosed until somebody notices.
|
|
153
|
+
|
|
154
|
+
An authored `message` is returned verbatim, because a refusal that cannot say why is not a
|
|
155
|
+
refusal a person can act on. Do not put a secret in one.
|
|
156
|
+
|
|
112
157
|
## Executing an action
|
|
113
158
|
|
|
114
159
|
The authority runs **the same semantic engine** the client runs. Transaction boundaries,
|
|
@@ -153,11 +198,37 @@ graph.setPrincipalEntity(ENTITY_USER);
|
|
|
153
198
|
```
|
|
154
199
|
|
|
155
200
|
- `PRINCIPAL` is bound to a record keyed by the **principal entity's field ids**, so a rule is written with the ordinary `field`/`ref` vocabulary. There is no separate policy language.
|
|
156
|
-
- It is bound **only where an authority evaluates**. Reading it in a derivation or a UI expression is `PRINCIPAL_REFERENCE_ON_CLIENT`.
|
|
157
201
|
- A rule that cannot be evaluated **denies**, exactly as an unevaluable constraint counts as violated.
|
|
158
202
|
- No `authorization` means every caller may invoke the action. An application with server state and no authorization anywhere gets a warning.
|
|
159
203
|
- `requiresConfirmation` is interaction, asked by the client. It is **not** an authorization mechanism and the authority never treats it as one.
|
|
160
204
|
|
|
205
|
+
### Where `PRINCIPAL` resolves
|
|
206
|
+
|
|
207
|
+
`PRINCIPAL` is not an authorization-only construct. It resolves **wherever an authority
|
|
208
|
+
evaluates an expression**, which is load-bearing: it is how an authoritative record records
|
|
209
|
+
who caused it without a client ever being asked to say so.
|
|
210
|
+
|
|
211
|
+
| Scope | `PRINCIPAL` |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| An action's `authorization` | resolves |
|
|
214
|
+
| A guard on a server action | resolves |
|
|
215
|
+
| An operation's value expression, including inside `for-each` | resolves |
|
|
216
|
+
| A postcondition of a server action | resolves |
|
|
217
|
+
| A constraint or transition constraint evaluated on the authority | resolves |
|
|
218
|
+
| A derivation, a UI expression, a client-evaluated guard | **prohibited** — `PRINCIPAL_REFERENCE_ON_CLIENT` |
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// An order records its own author. The client passes no user id, so it cannot claim one.
|
|
222
|
+
{ kind: 'insert', target: stateLocation(STATE_ORDERS), value: object(ENTITY_ORDER, [
|
|
223
|
+
{ fieldId: F_ORDER_PRODUCT, value: ref(PARAM_PRODUCT) },
|
|
224
|
+
{ fieldId: F_ORDER_PLACED_BY, value: field(ref(PRINCIPAL), F_USER_ID) },
|
|
225
|
+
]) }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- **Client validation.** `validateGraph` rejects a `PRINCIPAL` reference anywhere a client evaluates, and rejects an `authorization` on an action no authority executes. The rule is caught at authoring time, not discovered at run time.
|
|
229
|
+
- **Anonymous callers.** When `authenticate` returns `null` there is no principal. `ref(PRINCIPAL)` resolves to nothing: `field(ref(PRINCIPAL), …)` is absent, `required(…)` over it is false, and an authorization rule comparing it to anything is false — so an anonymous caller is denied by any rule naming a principal attribute rather than passing by accident. An operation that writes an absent principal attribute into a `required` field refuses the transaction with `REQUIRED_FIELD_MISSING`; nothing half-attributed is ever committed.
|
|
230
|
+
- **Never disclosed.** `PRINCIPAL` is not in the client IR, not in a snapshot and not in any answer. Observability reports the caller's **identity field only**.
|
|
231
|
+
|
|
161
232
|
**Read authorization is not solved in 0.6.** Observability is per state
|
|
162
233
|
(`serverOnly` or not), not per caller and not per record. An application whose users must
|
|
163
234
|
see different rows of the same collection needs a mechanism this release does not provide.
|
|
@@ -212,24 +283,58 @@ A network retry must not run an action twice.
|
|
|
212
283
|
```
|
|
213
284
|
|
|
214
285
|
The authority remembers recent request ids and answers a repeat from the record, marking it
|
|
215
|
-
`replayed: true`.
|
|
216
|
-
|
|
286
|
+
`replayed: true`. Exactly-once delivery is not assumed.
|
|
287
|
+
|
|
288
|
+
**Records are scoped by principal.** The key is the caller's identity together with the
|
|
289
|
+
request id, not the request id alone. A request id is chosen by a client and is therefore not
|
|
290
|
+
a secret: were it the whole key, a caller who guessed another caller's id would be handed
|
|
291
|
+
that caller's answer. Anonymous callers share one scope, because there is nothing to tell
|
|
292
|
+
them apart — an application that needs replay isolation between anonymous callers has to
|
|
293
|
+
authenticate them.
|
|
294
|
+
|
|
295
|
+
**Generated ids are unique across clients.** The client runtime generates one per invocation,
|
|
296
|
+
combining a per-runtime session identity with the action, an ordinal and the host's uuid. The
|
|
297
|
+
session identity matters: a deterministic host — a memory host, a conformance host, a test
|
|
298
|
+
double — hands every runtime it constructs the same uuid sequence, and without something
|
|
299
|
+
above the host two clients would generate the same key for their first invocation and the
|
|
300
|
+
second would be answered as a replay of the first.
|
|
301
|
+
|
|
302
|
+
A client writing its own `requestId` is choosing its own retry identity and must make it
|
|
303
|
+
unique per intended transaction.
|
|
217
304
|
|
|
218
305
|
## The protocol
|
|
219
306
|
|
|
220
307
|
Transport-independent by construction. One endpoint, semantic requests:
|
|
221
308
|
|
|
222
309
|
```ts
|
|
223
|
-
{ kind: 'snapshot', protocol: 'axiom.protocol.v1', credential? }
|
|
310
|
+
{ kind: 'snapshot', protocol: 'axiom.protocol.v1', credential?, sinceRevision? }
|
|
224
311
|
{ kind: 'invoke', protocol: 'axiom.protocol.v1', actionId, arguments?, credential?, requestId? }
|
|
225
312
|
```
|
|
226
313
|
|
|
227
314
|
```ts
|
|
228
315
|
{ kind: 'result', ok, diagnostics, changes, revision, requestId?, replayed? }
|
|
229
|
-
{ kind: 'snapshot', snapshot: { revision, states } }
|
|
316
|
+
{ kind: 'snapshot', snapshot: { revision, states, partial? } }
|
|
230
317
|
{ kind: 'error', diagnostics }
|
|
231
318
|
```
|
|
232
319
|
|
|
320
|
+
### `sinceRevision`
|
|
321
|
+
|
|
322
|
+
A snapshot request may name a revision the caller already holds. The answer is then
|
|
323
|
+
**partial**, and what it may leave out is a contract of its own:
|
|
324
|
+
|
|
325
|
+
| | |
|
|
326
|
+
| --- | --- |
|
|
327
|
+
| Omitted | Nothing. Every observable state is named. |
|
|
328
|
+
| Given | Every observable state the authority cannot **prove** unchanged since that revision: each stored state whose last committed revision is later, and every derived state. `partial: true`. |
|
|
329
|
+
| Ahead of the authority's own revision | The complete snapshot. A revision it never issued tells it nothing, and "nothing changed" would be a lie. |
|
|
330
|
+
| Not a non-negative integer | `MALFORMED_REQUEST`. |
|
|
331
|
+
|
|
332
|
+
A state absent from a partial snapshot is unchanged since the revision asked for. The reverse
|
|
333
|
+
does not hold: a state may be named without having moved. Derived states are always named,
|
|
334
|
+
because a derived value follows states the response may not even be permitted to disclose and
|
|
335
|
+
the authority will not guess. `snapshot.revision` is the authority's current revision either
|
|
336
|
+
way, so it is what the caller passes as the next `sinceRevision`.
|
|
337
|
+
|
|
233
338
|
No graph declares `POST /orders`. Transports:
|
|
234
339
|
|
|
235
340
|
| Adapter | Use |
|
|
@@ -239,22 +344,59 @@ No graph declares `POST /orders`. Transports:
|
|
|
239
344
|
|
|
240
345
|
A later WebSocket, worker or IPC transport changes nothing in a graph.
|
|
241
346
|
|
|
242
|
-
##
|
|
347
|
+
## Startup
|
|
243
348
|
|
|
244
|
-
|
|
349
|
+
`start()` is the whole startup sequence, and it is the same three steps whether or not there
|
|
350
|
+
is an authority:
|
|
351
|
+
|
|
352
|
+
1. **Render**, synchronously, from the client IR's initial values. A page is on screen before any network call.
|
|
353
|
+
2. **Restore** persisted client state, where a state declares persistence.
|
|
354
|
+
3. **Load authoritative state**, when a gateway is configured — one snapshot request, applied through the ordinary write path, then one re-render.
|
|
355
|
+
|
|
356
|
+
`start()` returns a promise that settles when step 3 has settled. Awaiting it means "the page
|
|
357
|
+
is showing authoritative state"; not awaiting it means "the page is showing something", which
|
|
358
|
+
is a legitimate choice for a page that renders progressively.
|
|
245
359
|
|
|
246
360
|
```ts
|
|
247
|
-
const app = createAxiomRuntime({ ir, rootElement, host, remote
|
|
248
|
-
app.start();
|
|
249
|
-
|
|
361
|
+
const app = createAxiomRuntime({ ir, rootElement, host, remote });
|
|
362
|
+
await app.start(); // rendered, restored, synchronized
|
|
363
|
+
app.authoritativeStateLoaded(); // true
|
|
250
364
|
```
|
|
251
365
|
|
|
252
|
-
|
|
366
|
+
Three things follow, and they are contract rather than implementation detail:
|
|
367
|
+
|
|
368
|
+
- **A gateway must be configured before `start()`.** Adding one afterwards does not retroactively synchronize; call `syncAuthoritativeState()` yourself.
|
|
369
|
+
- **A failed load is a diagnostic, not an exception.** The authority being unreachable reports `AUTHORITY_UNREACHABLE`, `start()` still resolves, the page still renders, and `authoritativeStateLoaded()` stays `false`. An empty authoritative collection and an unreachable authority are different situations and the runtime distinguishes them.
|
|
370
|
+
- **`syncAuthoritativeState()` is idempotent and may be called at any time.** It is what a refresh button does.
|
|
371
|
+
|
|
372
|
+
## Observing authoritative state
|
|
373
|
+
|
|
374
|
+
The model is the simplest correct one: **request, decide, apply**.
|
|
375
|
+
|
|
376
|
+
- A remote invocation returns `{ ok: false, pending: true }` immediately, so a click never blocks on the network. The outcome arrives later and is recorded through the ordinary action-outcome lifecycle — which is how a `diagnostic` node presents a *server* refusal exactly as it presents a local one. While it is outstanding, the control that started it renders `aria-busy` and refuses a second press; see [UI](./UI.md#pending-actions).
|
|
253
377
|
- `invokeActionAsync(id, args)` awaits the answer, for tests and programmatic callers.
|
|
254
378
|
- The answer's `changes` are applied through the client's single write path, under the one flag that permits writing server-owned state.
|
|
255
379
|
- **Optimistic updates are not implemented.** There is no client-side rollback to get wrong.
|
|
256
380
|
- A derivation that depends only on observed state is recomputed locally rather than transferred.
|
|
257
381
|
|
|
382
|
+
### What `changes` contains
|
|
383
|
+
|
|
384
|
+
**Every observable state whose value differs from what it was when the transaction opened,
|
|
385
|
+
and no others.** Difference is the criterion, not provenance:
|
|
386
|
+
|
|
387
|
+
| Situation | In `changes`? |
|
|
388
|
+
| --- | --- |
|
|
389
|
+
| A stored state the transaction wrote to a new value | yes |
|
|
390
|
+
| A stored state written back to the value it already held | no |
|
|
391
|
+
| A derived state whose recomputed value moved | yes, though no mutation named it |
|
|
392
|
+
| A derived state recomputed to the same value | no |
|
|
393
|
+
| A `serverOnly` state, however it changed | never — it is not observable |
|
|
394
|
+
| Any state, when the invocation was refused or rolled back | no: `changes` is empty |
|
|
395
|
+
| Any state, when the action committed nothing | no: `changes` is empty |
|
|
396
|
+
|
|
397
|
+
A replayed response carries the `changes` that were recorded for the original request, not a
|
|
398
|
+
freshly computed set.
|
|
399
|
+
|
|
258
400
|
## Diagnostics
|
|
259
401
|
|
|
260
402
|
Server codes join the same structured vocabulary; a client matches on `code` exactly as it
|
|
@@ -290,21 +432,43 @@ implements logging of its own.
|
|
|
290
432
|
|
|
291
433
|
## Running one
|
|
292
434
|
|
|
293
|
-
|
|
294
|
-
|
|
435
|
+
One graph, one process: the generated page and the authority that answers it.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { compileToHtml, compileToServerIR } from '@cynodia/axiom-compiler';
|
|
439
|
+
import { createSqlitePersistence, serveAxiomApplication } from '@cynodia/axiom-server';
|
|
440
|
+
|
|
441
|
+
const graph = createApplicationGraph();
|
|
442
|
+
const running = await serveAxiomApplication({
|
|
443
|
+
serverIR: compileToServerIR(graph),
|
|
444
|
+
page: compileToHtml(graph),
|
|
445
|
+
persistence: await createSqlitePersistence({ location: 'app.db' }),
|
|
446
|
+
authenticate: (credential) => resolveUser(credential),
|
|
447
|
+
port: 3000,
|
|
448
|
+
});
|
|
449
|
+
// running.pageUrl → http://127.0.0.1:3000/
|
|
295
450
|
```
|
|
296
451
|
|
|
297
|
-
|
|
298
|
-
application
|
|
299
|
-
|
|
300
|
-
|
|
452
|
+
`GET /` is the page and `POST /axiom` is the semantic endpoint, for every Axiom application
|
|
453
|
+
there will ever be. No application defines a route, a verb, a controller, a handler or a line
|
|
454
|
+
of SQL, and the page needs no JavaScript of its own: `compileToHtml` wires the browser-safe
|
|
455
|
+
gateway into the generated bootstrap whenever the IR contains a remote action.
|
|
456
|
+
|
|
457
|
+
To point a page somewhere else, or to switch the gateway off for an application that has no
|
|
458
|
+
authority:
|
|
301
459
|
|
|
302
460
|
```ts
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
await serveOverHttp({ server, port: 3000 });
|
|
461
|
+
compileToHtml(graph, { remote: { endpoint: 'https://api.example.com/axiom' } });
|
|
462
|
+
compileToHtml(graph, { remote: false });
|
|
306
463
|
```
|
|
307
464
|
|
|
465
|
+
The two halves also run separately. `serveOverHttp({ server, port })` is the bare authority,
|
|
466
|
+
and `createAxiomServer({ ir, persistence, host })` is the authority with no transport at all —
|
|
467
|
+
which is what `createDirectTransport` drives in tests.
|
|
468
|
+
|
|
469
|
+
**There is no published Axiom CLI.** `packages/cli` is a private development tool of this
|
|
470
|
+
repository and is not on npm; the API above is the supported way to run an application.
|
|
471
|
+
|
|
308
472
|
## Conformance
|
|
309
473
|
|
|
310
474
|
`@cynodia/axiom-server` ships `conformance/*.json`. Each fixture is pure data — a Server IR,
|
|
@@ -317,11 +481,100 @@ Running them requires no part of this implementation. That is the point: the Ser
|
|
|
317
481
|
specification plus these fixtures are the whole contract, so an independent runtime in
|
|
318
482
|
another language can be held to exactly the same standard.
|
|
319
483
|
|
|
320
|
-
|
|
484
|
+
Enumerate the suite from its manifest rather than by listing a directory:
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
@cynodia/axiom-server/conformance → the manifest
|
|
488
|
+
@cynodia/axiom-server/conformance/<name>.json → one fixture
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The manifest names the contracts the fixtures are written against (`axiom.conformance.v1`,
|
|
492
|
+
`axiom.server.v1`, `axiom.protocol.v1`), the release, every area covered, and every fixture
|
|
493
|
+
with its file. A runtime that does not implement a contract the manifest names should refuse
|
|
494
|
+
the suite rather than discover the mismatch one assertion at a time. The files are plain JSON
|
|
495
|
+
in a documented package directory, so a non-JavaScript consumer can read them straight out of
|
|
496
|
+
the tarball without Node's module resolver.
|
|
497
|
+
|
|
498
|
+
Every fixture is executed against the reference runtime by this repository's own test suite,
|
|
499
|
+
and its expectations are exhaustive — a fixture that says which states changed must name all
|
|
500
|
+
of them and no others. No fixture is permitted to disagree with the shipped runtime.
|
|
501
|
+
|
|
502
|
+
## Machine-readable contracts
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
@cynodia/axiom-server/schema/server-ir.v1.schema.json
|
|
506
|
+
@cynodia/axiom-server/schema/protocol.v1.schema.json
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
JSON Schema for the Server IR and for the wire protocol, so an implementer is not reading
|
|
510
|
+
TypeScript declarations to find out what a document may contain. They are generated from the
|
|
511
|
+
runtime's own vocabulary — expression kinds, built-in functions, operation kinds — so they
|
|
512
|
+
cannot drift from what the runtime implements, and every shipped fixture is validated against
|
|
513
|
+
them.
|
|
514
|
+
|
|
515
|
+
They describe **structure**. What a conforming runtime must *do* with a valid document is this
|
|
516
|
+
page plus the conformance fixtures.
|
|
517
|
+
|
|
518
|
+
## Server IR v1 is frozen
|
|
519
|
+
|
|
520
|
+
`axiom.server.v1` is a stable semantic contract as of 0.6.1. A runtime may depend on the
|
|
521
|
+
semantics below exactly as written; a change that breaks them requires a new contract
|
|
522
|
+
identifier, not a new version number.
|
|
523
|
+
|
|
524
|
+
**Numbers.** Every numeric value is an IEEE-754 binary64 (double-precision) value, and every
|
|
525
|
+
arithmetic operator (`add`, `subtract`, `multiply`, `divide`) is the corresponding IEEE-754
|
|
526
|
+
operation. Ordered comparisons against a value that is not a number are **false**, both ways —
|
|
527
|
+
so a guard over a computation that failed refuses rather than passing on a value it could not
|
|
528
|
+
compute. `sum` over an empty collection is `0`. Integer-valued doubles are not a separate
|
|
529
|
+
type; there is no integer type and no decimal type in this contract.
|
|
530
|
+
|
|
531
|
+
**Text.** `to-string` of a number is its shortest round-tripping decimal form, without digit
|
|
532
|
+
grouping and without a locale. `lowercase` is the Unicode default case conversion, unconditional
|
|
533
|
+
and locale-independent. Text ordering — in `sort`, and in `compareValues` wherever two
|
|
534
|
+
non-numeric values are ordered — is **lexicographic by Unicode code point**. Not locale
|
|
535
|
+
collation, and not UTF-16 code-unit order: the two disagree whenever a string mixes astral
|
|
536
|
+
characters with U+E000–U+FFFF, and code points are the ordering every language can reproduce.
|
|
537
|
+
|
|
538
|
+
**Host values.** `now()` and `uuid()` are the only two places semantics may depend on something
|
|
539
|
+
outside the graph, and their production values are the host's. For conformance the host is
|
|
540
|
+
pinned: one counter shared by both, starting at zero and incremented before each value, so the
|
|
541
|
+
nth host call in an execution is `id-<n>` or `2026-01-01T00:00:<n>.000Z` whichever it was. A
|
|
542
|
+
runtime in another language reproduces this by counting host calls in execution order. The
|
|
543
|
+
counter is per host instance; a restart does not rewind it.
|
|
544
|
+
|
|
545
|
+
**Serialization.** A Server IR document is JSON and nothing else. It MUST NOT contain
|
|
546
|
+
`undefined`, a function, a closure, `NaN`, `Infinity`, a `BigInt`, a host object, a `Date`, a
|
|
547
|
+
`RegExp`, or an instance of a class that needs its prototype to be understood. It must survive
|
|
548
|
+
a parse/serialize round trip unchanged. Record key order is not significant and MUST NOT be
|
|
549
|
+
relied upon.
|
|
550
|
+
|
|
551
|
+
**Diagnostics.** The codes in the table above are public vocabulary and are frozen: a
|
|
552
|
+
conforming runtime reports these codes, with these meanings, and the fixtures assert on them.
|
|
553
|
+
The details a diagnostic may carry across the boundary are the whitelist described under
|
|
554
|
+
[What `serverOnly` hides](#what-serveronly-hides).
|
|
555
|
+
|
|
556
|
+
**Authorization.** An `authorization` expression is an ordinary expression evaluated on the
|
|
557
|
+
authority with `PRINCIPAL` bound, before any guard and before any transaction opens. There is
|
|
558
|
+
no policy language, no rule engine and no evaluation order beyond that.
|
|
559
|
+
|
|
560
|
+
**Persistence is not part of the contract.** `PersistenceAdapter` is how an authority keeps
|
|
561
|
+
what it decided; nothing about *which* adapter is in use changes what a graph means. A
|
|
562
|
+
conforming runtime may store state any way it likes, provided a semantic transaction persists
|
|
563
|
+
as one unit and a stale revision refuses the commit.
|
|
564
|
+
|
|
565
|
+
**Concurrency.** An authority serializes execution: one action at a time, its commit complete
|
|
566
|
+
before the next begins. A conforming runtime may execute concurrently only if the observable
|
|
567
|
+
result is identical to some serial order. Across processes, correctness rests on the
|
|
568
|
+
persistence adapter's revision check — the contract guarantees that a commit from a stale
|
|
569
|
+
snapshot is refused, not that two processes coordinate.
|
|
570
|
+
|
|
571
|
+
## Not in 0.6.2
|
|
321
572
|
|
|
322
573
|
Stated plainly rather than left to discovery:
|
|
323
574
|
|
|
575
|
+
- **Generated values cannot be bound within an action.** An operation cannot name a value an earlier operation produced: `uuid()` evaluated in one `insert` cannot be referred to by a later `insert` in the same action. Give the record an identity the action already has — a parameter, or a field of something it read — or perform the second write in a second action. A semantic binding for this (`bindAs` on an operation, `ref` to it later) needs a lexical lifetime, a type, `for-each` and nested-invoke semantics, serialization and dependency analysis all decided together; doing that hastily would weaken a contract that is now frozen, so it is deferred to 0.7.
|
|
324
576
|
- **Read authorization per caller or per record.** Visibility is per state.
|
|
577
|
+
|
|
325
578
|
- **External effects.** A database write rolls back; an email does not. Nothing here makes an external side effect participate in a transaction, and `NativeOperation` MUST NOT be used to smuggle one in. A deliberate effect model — commands, a transactional outbox, idempotency — is future work.
|
|
326
579
|
- **Realtime synchronization**, subscriptions and collaboration. Request/response only.
|
|
327
580
|
- **Query semantics.** Authoritative collections are loaded into runtime state; large-data querying needs its own design.
|
package/docs/CONSTRAINTS.md
CHANGED
package/docs/EXPRESSIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Expressions
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. An expression describes **what value is computed**. It is a tree of
|
|
4
4
|
plain data, never source text and never a callback. Evaluation is pure: an expression MUST
|
|
5
5
|
NOT change state.
|
|
6
6
|
|
package/docs/GRAPH_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Graph model
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. The `ApplicationGraph` is the authoritative representation of an
|
|
4
4
|
application. Everything else — the IR, the page, the DOM — is derived from it and is never
|
|
5
5
|
edited.
|
|
6
6
|
|
package/docs/LOCATIONS.md
CHANGED
package/docs/PRESENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
|
|
4
4
|
node. It names roles, tokens and device classes. It never names a colour, a length, a media
|
|
5
5
|
query or a CSS property.
|
|
6
6
|
|
package/docs/RUNTIME.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Runtime
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
|
|
4
4
|
contains no knowledge of any application.
|
|
5
5
|
|
|
6
6
|
## Constructing
|
|
@@ -14,7 +14,8 @@ const app = createAxiomRuntime({
|
|
|
14
14
|
rootElement, // a DomElement
|
|
15
15
|
host, // a HostEnvironment
|
|
16
16
|
nativeOperations?: Record<string, (inputs) => unknown>,
|
|
17
|
-
remote?: RemoteGateway, // required for server-authoritative state
|
|
17
|
+
remote?: RemoteGateway, // required for server-authoritative state,
|
|
18
|
+
// and required *before* start()
|
|
18
19
|
inputValidation?: 'immediate' | 'deferred', // DEFAULT: 'immediate'
|
|
19
20
|
recordMutationValues?: boolean, // default true
|
|
20
21
|
});
|
|
@@ -60,7 +61,7 @@ carries `data-node="<node id>"`.
|
|
|
60
61
|
|
|
61
62
|
| Member | Governed | Semantics |
|
|
62
63
|
| --- | --- | --- |
|
|
63
|
-
| `start()` | — |
|
|
64
|
+
| `start()` | — | The whole startup sequence: render, restore, then load authoritative state. Returns a promise; awaiting it means authoritative state has been applied. Idempotent. See [Startup](#startup). |
|
|
64
65
|
| `render()` | — | Re-renders from current state. |
|
|
65
66
|
| `getState(id)` | — | A **deep clone** of the value. Derived state is recomputed. |
|
|
66
67
|
| `invokeAction(id, args?)` | **yes** | Runs the action as a transaction. Returns `{ ok, diagnostics }` for that invocation. |
|
|
@@ -73,7 +74,9 @@ carries `data-node="<node id>"`.
|
|
|
73
74
|
| `getMutationLog()` | — | Every attempted mutation, with source, path and outcome. |
|
|
74
75
|
| `registerNativeOperation(id, fn)` | — | Registers an implementation for a `native` operation. |
|
|
75
76
|
| `invokeActionAsync(id, args?)` | **yes** | Awaits the outcome, including an authority's answer. |
|
|
76
|
-
| `syncAuthoritativeState()` | — | Loads the authoritative snapshot and applies it. See [`AUTHORITY.md`](AUTHORITY.md). |
|
|
77
|
+
| `syncAuthoritativeState()` | — | Loads the authoritative snapshot and applies it. Idempotent; may be called at any time. See [`AUTHORITY.md`](AUTHORITY.md). |
|
|
78
|
+
| `authoritativeStateLoaded()` | — | Whether a snapshot has been applied. `false` also after a failed load, which is why it is not the same question as "is this collection empty". |
|
|
79
|
+
| `settled()` | — | Resolves when no remote invocation is outstanding. An action started by a click or a form submit leaves no promise for a caller to hold; this is how to wait for it without guessing a delay. |
|
|
77
80
|
| `evaluate(expression)` | — | Evaluates in the root scope, reporting rather than throwing. A pure read. |
|
|
78
81
|
|
|
79
82
|
### `hydrateState` bypasses semantic enforcement
|
|
@@ -210,6 +213,7 @@ interface RuntimeDiagnostic {
|
|
|
210
213
|
| `ROUTE_NOT_FOUND` | A `navigate` operation naming an unresolvable route. | action | — |
|
|
211
214
|
| `NATIVE_OPERATION_MISSING` | No implementation registered for an `implementationId`. | `native` operation | — |
|
|
212
215
|
| `REMOTE_ACTION_UNAVAILABLE` | An action belonging to the authority was invoked with no gateway configured, or the transport failed. | remote invocation | — |
|
|
216
|
+
| `AUTHORITY_UNREACHABLE` | **Warning.** `start()` could not load authoritative state: no answer from the authority. The page renders with what it has, and `authoritativeStateLoaded()` stays false. | startup | — |
|
|
213
217
|
| `UI_NODE_MISSING` | A child id that is not a UI node in the IR. | render | — |
|
|
214
218
|
| `UNSUPPORTED_UI_NODE` | An unknown UI node kind. | render | — |
|
|
215
219
|
| `PERSISTED_STATE_UNREADABLE` | **Warning.** A stored value could not be parsed; the initial value was used. | startup | — |
|
|
@@ -250,19 +254,43 @@ app.getMutationLog();
|
|
|
250
254
|
- Only the outermost transaction decides an outcome, so the log never suggests that early iterations of a failed loop committed.
|
|
251
255
|
- `recordMutationValues: false` omits `oldValue` / `newValue`.
|
|
252
256
|
|
|
257
|
+
## Startup
|
|
258
|
+
|
|
259
|
+
`start()` is the whole startup sequence, and it is the same three steps whether or not there
|
|
260
|
+
is an authority:
|
|
261
|
+
|
|
262
|
+
1. **Render**, synchronously, from the client IR's initial values. A page is on screen before any network call.
|
|
263
|
+
2. **Restore** persisted client state, where a state declares persistence.
|
|
264
|
+
3. **Load authoritative state**, when a gateway is configured — one snapshot request, applied through the ordinary write path, then one re-render.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const app = createAxiomRuntime({ ir, rootElement, host, remote });
|
|
268
|
+
await app.start(); // rendered, restored, synchronized
|
|
269
|
+
app.authoritativeStateLoaded(); // true
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- `start()` returns a promise that settles when step 3 has settled. Not awaiting it is a legitimate choice for a page that renders progressively; the first two steps have already happened synchronously by the time it returns.
|
|
273
|
+
- **A gateway must be configured before `start()`.** Adding one afterwards does not retroactively synchronize — call `syncAuthoritativeState()`.
|
|
274
|
+
- **A failed load is a diagnostic, not an exception.** `AUTHORITY_UNREACHABLE` is reported, `start()` still resolves, the page still renders, and `authoritativeStateLoaded()` stays `false`. An empty authoritative collection and an unreachable authority are different situations, and the runtime does not conflate them.
|
|
275
|
+
- `start()` is idempotent: calling it twice does not re-render twice or fetch twice.
|
|
276
|
+
|
|
253
277
|
## Reaching an authority
|
|
254
278
|
|
|
255
279
|
An application with server-authoritative state gives the runtime a gateway:
|
|
256
280
|
|
|
257
281
|
```ts
|
|
258
282
|
const app = createAxiomRuntime({ ir, rootElement, host, remote: createRemoteGateway(transport) });
|
|
259
|
-
app.start();
|
|
260
|
-
await app.syncAuthoritativeState();
|
|
283
|
+
await app.start();
|
|
261
284
|
```
|
|
262
285
|
|
|
286
|
+
A generated page does this for itself — `compileToHtml` wires the browser-safe gateway into
|
|
287
|
+
the bootstrap whenever the IR contains a remote action, so an application author writes no
|
|
288
|
+
JavaScript at all. See [`AUTHORITY.md`](AUTHORITY.md#running-one).
|
|
289
|
+
|
|
263
290
|
`invokeAction` on a remote action returns `{ ok: false, pending: true }` immediately — a
|
|
264
291
|
click never blocks on the network — and the outcome arrives through the ordinary
|
|
265
|
-
action-outcome lifecycle.
|
|
292
|
+
action-outcome lifecycle. While it is outstanding the control that started it renders
|
|
293
|
+
`aria-busy` and refuses a second press. Full model: [`AUTHORITY.md`](AUTHORITY.md).
|
|
266
294
|
|
|
267
295
|
## The browser bundle
|
|
268
296
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Semantic contract
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
4
|
-
does not teach. Where this file and any specification in
|
|
3
|
+
Axiom 0.6.2-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
|
|
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
|
|
|
7
7
|
`MUST` / `MUST NOT` describe guaranteed behavior. `MAY` describes a documented option.
|
|
@@ -196,22 +196,30 @@ Full model: [`AUTHORITY.md`](AUTHORITY.md).
|
|
|
196
196
|
- The transaction guarantees above hold unchanged on the authority: the same semantic engine executes both halves.
|
|
197
197
|
- A semantic transaction MUST persist atomically. An adapter MUST NOT apply a subset of a transaction's writes, and MUST refuse a commit whose expected revisions no longer hold.
|
|
198
198
|
- Two actions MUST NOT both commit from incompatible snapshots. The authority serializes execution, and a stale commit is refused with `CONCURRENCY_CONFLICT`.
|
|
199
|
-
- A repeated `requestId` MUST be answered from the record rather than executed again.
|
|
199
|
+
- A repeated `requestId` MUST be answered from the record rather than executed again. Records MUST be scoped by principal as well as by request id: a request id is client-chosen, and one caller MUST NOT be able to receive another's recorded answer.
|
|
200
|
+
- An automatically generated request id MUST be unique across independent runtime instances, whatever the host's uuid provider does.
|
|
201
|
+
- `InvokeResponse.changes` MUST contain every observable state whose value differs from what it was when the transaction opened, and no others. A refused or no-op invocation MUST return an empty `changes`.
|
|
202
|
+
- A `SnapshotRequest.sinceRevision` MUST affect the answer as documented: the response omits only states the authority can prove unchanged, and MUST mark itself `partial`. A revision the authority never issued MUST be answered completely.
|
|
203
|
+
- A diagnostic crossing the trust boundary MUST NOT carry a state value. Its code, authored message and structural details cross; the record a rule was judging does not.
|
|
204
|
+
- `PRINCIPAL` MUST resolve wherever an authority evaluates an expression — authorization, guards, operation values, postconditions and constraints alike — and MUST NOT resolve anywhere a client evaluates.
|
|
200
205
|
- Server IR MUST be serializable, deterministic and free of closures, and MUST declare a contract version a runtime can refuse.
|
|
206
|
+
- `axiom.server.v1` is **frozen**. Its semantics — IEEE-754 binary64 arithmetic, Unicode code-point text ordering, the deterministic host model, and the JSON serialization constraints — are normative and language-independent. An incompatible semantic change requires a new contract identifier.
|
|
201
207
|
|
|
202
|
-
**Not guaranteed in 0.6**, and stated so rather than implied: read authorization per caller
|
|
203
|
-
or per record,
|
|
204
|
-
synchronization, query semantics, and
|
|
208
|
+
**Not guaranteed in 0.6.2**, and stated so rather than implied: read authorization per caller
|
|
209
|
+
or per record, binding a value generated by one operation in a later one, external side
|
|
210
|
+
effects participating in a transaction, realtime synchronization, query semantics, and
|
|
211
|
+
multi-node execution.
|
|
205
212
|
|
|
206
213
|
## Diagnostics as semantic UI
|
|
207
214
|
|
|
208
|
-
- The runtime MUST record the outcome of each action's most recent invocation: `'ok'`, `'failed'` or `'
|
|
215
|
+
- The runtime MUST record the outcome of each action's most recent invocation: `'ok'`, `'failed'`, `'cancelled'` or `'pending'`, with that invocation's diagnostics.
|
|
209
216
|
- The record MUST be replaced by the next invocation of the same action, whatever its outcome. It does not accumulate.
|
|
210
217
|
- `'ok'` and `'cancelled'` MUST carry no diagnostics.
|
|
211
218
|
- The record MUST be cleared by `clearDiagnostics()` and by navigating to another route.
|
|
212
219
|
- A `diagnostic` UI node MUST present the diagnostics of its action's record at or above its own severity, and nothing else.
|
|
213
220
|
- Diagnostics are ephemeral runtime state. They are not application state, are never persisted, and cannot be written.
|
|
214
221
|
- The runtime MUST re-render after every top-level action invocation, including one refused before a transaction was opened, so a refusal reaches the interface without the application arranging it.
|
|
222
|
+
- Action guards MUST be evaluated in declaration order, and the first that does not hold MUST stop evaluation and be the only failure reported. Guard failures MUST NOT be aggregated.
|
|
215
223
|
- An application MUST NOT need to duplicate an action's guards, read console output, copy an `ActionResult` into its own state, or install a renderer-specific handler in order to present a refusal.
|
|
216
224
|
|
|
217
225
|
## Render identity
|
|
@@ -219,6 +227,7 @@ synchronization, query semantics, and multi-node execution.
|
|
|
219
227
|
- A UI node inside a `repeat` is rendered once per member. `NodeId` MUST NOT be used alone to identify a rendered element.
|
|
220
228
|
- Every renderer-generated identity and relationship — element id, label association, described-by relationships, error-region ids, control lookup, focus restoration — MUST be keyed by the render instance.
|
|
221
229
|
- A rendered identity MUST be unique within the document, deterministic, and stable while the member's identity is stable. Nested repeats MUST compose rather than collide.
|
|
230
|
+
- Where a semantic node renders as more than one element, every one of them MUST carry the node's identity, and the single element a person operates MUST additionally be distinguishable. Tooling MUST NOT have to infer which element is the control from its tag name.
|
|
222
231
|
- Where the collection's member type carries an identity field, that identity MUST be preferred, so the identity follows a member through reordering. Otherwise a deterministic index is used.
|
|
223
232
|
- Refusing a write in one rendered instance MUST NOT affect the accessibility state of another.
|
|
224
233
|
- The graph still contains one semantic node. `AgentAPI` reasons about the node, never about instances.
|
|
@@ -262,5 +271,6 @@ These are current implementation limits, not design intentions.
|
|
|
262
271
|
- Type inference is deliberately partial: it rejects obvious mismatches and stays silent where a type depends on an iteration scope.
|
|
263
272
|
- Iteration scopes are ordinary `NodeId`s; misuse is caught by validation rather than by the type system.
|
|
264
273
|
- Remote persistence is declared but not executed.
|
|
265
|
-
-
|
|
274
|
+
- Asynchronous action semantics extend no further than a remote invocation: the outcome is `pending` until the authority answers, and the control that started it is marked busy and refuses a second press. There is no general async workflow model, no progress reporting and no cancellation.
|
|
275
|
+
- An action cannot bind a value it generated earlier in the same transaction; `uuid()` in one operation is not addressable by a later one.
|
|
266
276
|
- Change sets are in memory and per `AgentAPI` instance. There is no semantic version control and no on-disk graph format.
|
package/docs/STATE.md
CHANGED
package/docs/UI.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UI
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. Ten 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 nine share `UIBase`:
|
|
@@ -110,10 +110,26 @@ graph.addNode<FormNode>({
|
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
- `submitButtonId` MUST name a `ButtonNode` among the form's descendants.
|
|
113
|
-
- The submit action is `submitActionId ?? <that button>.actionId`. If both are given they MUST agree.
|
|
113
|
+
- The submit action is `submitActionId ?? <that button>.actionId`. If both are given they MUST agree. **That one resolution is used everywhere** — execution, validation, presentation inference, `AgentAPI` form structure and UX warnings — so a form with a declared submit button is a form with a primary action, and nothing has to infer it a second way.
|
|
114
114
|
- The declared control is rendered with `type="submit"` and no click handler of its own, so a click runs the action exactly once.
|
|
115
115
|
- `submitLabel` is ignored when `submitButtonId` is given.
|
|
116
116
|
|
|
117
|
+
**A declared submit button keeps its arguments.** Submitting the form and clicking the button
|
|
118
|
+
are the same invocation: the same `arguments`, evaluated in the same scope, at the moment the
|
|
119
|
+
control is used. This matters most inside a `repeat`, where the button's arguments are what
|
|
120
|
+
say *which row* — a submit path that ignored them would invoke a parameterized action with
|
|
121
|
+
nothing bound and be refused.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
graph.addNode<ButtonNode>({
|
|
125
|
+
id: UI_CONFIRM, kind: 'button', label: 'Confirm', actionId: ACTION_CONFIRM,
|
|
126
|
+
arguments: { [PARAM_ORDER]: field(ref(UI_ORDER_ROW), F_ORDER_ID) }, // survives form submit
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
An action parameter that is `required` and never supplied is `MISSING_ACTION_ARGUMENT` at
|
|
131
|
+
authoring time — from a `button`, and from a `form` that submits an action without one.
|
|
132
|
+
|
|
117
133
|
### `input`
|
|
118
134
|
|
|
119
135
|
```ts
|
|
@@ -162,8 +178,26 @@ the two. All three are keyed by render instance, so only the refused row is affe
|
|
|
162
178
|
```
|
|
163
179
|
|
|
164
180
|
Invokes an action. `arguments` is keyed by the **action parameter id**; passing an unknown
|
|
165
|
-
parameter is a validation error
|
|
166
|
-
|
|
181
|
+
parameter is a validation error, and omitting a required one is `MISSING_ACTION_ARGUMENT`.
|
|
182
|
+
`destructive` may be declared here, but declaring it on the action is enough — presentation
|
|
183
|
+
is inferred from the action.
|
|
184
|
+
|
|
185
|
+
#### Pending actions
|
|
186
|
+
|
|
187
|
+
An action the authority executes is not finished when the button is released. While its
|
|
188
|
+
answer is outstanding the control renders:
|
|
189
|
+
|
|
190
|
+
```html
|
|
191
|
+
<button data-node="ui_place" data-control="ui_place"
|
|
192
|
+
data-pending="true" aria-busy="true" disabled>Place order</button>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
and a second press does nothing — a second press is a second transaction, not a retry of the
|
|
196
|
+
first, and by the time it reached the authority it would be a legitimately different request.
|
|
197
|
+
The outcome is the ordinary `ActionOutcome` lifecycle (`pending` → `ok` / `failed`), so a
|
|
198
|
+
`diagnostic` node presents a server refusal exactly as it presents a local one. There is no
|
|
199
|
+
async vocabulary in the graph: `pending` is a runtime outcome, and this is the whole of its
|
|
200
|
+
presentation.
|
|
167
201
|
|
|
168
202
|
### `diagnostic`
|
|
169
203
|
|
|
@@ -238,6 +272,7 @@ identify a rendered element, and two concepts are kept distinct:
|
|
|
238
272
|
```text
|
|
239
273
|
NodeId = semantic graph identity → data-node
|
|
240
274
|
RenderInstance = runtime presentation identity → data-instance
|
|
275
|
+
Control = the element a person operates → data-control
|
|
241
276
|
```
|
|
242
277
|
|
|
243
278
|
Every renderer-generated identity and relationship is keyed by the render instance:
|
|
@@ -256,14 +291,39 @@ Instance identity is:
|
|
|
256
291
|
- **composing** for nested repeats, rather than colliding.
|
|
257
292
|
|
|
258
293
|
```html
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
294
|
+
<label data-node="ui_line_quantity" data-instance="ui_line_quantity--line-7f3a"
|
|
295
|
+
for="axiom-control-ui_line_quantity--line-7f3a">
|
|
296
|
+
<span class="axiom-input-label">Quantity</span>
|
|
297
|
+
<input data-node="ui_line_quantity" data-control="ui_line_quantity"
|
|
298
|
+
data-instance="ui_line_quantity--line-7f3a"
|
|
299
|
+
id="axiom-control-ui_line_quantity--line-7f3a" type="number">
|
|
300
|
+
</label>
|
|
262
301
|
```
|
|
263
302
|
|
|
264
303
|
The exact encoding is an implementation detail; the properties above are the contract. The
|
|
265
304
|
graph still holds one node, and `AgentAPI` reasons about that node.
|
|
266
305
|
|
|
306
|
+
### `data-node` and `data-control`
|
|
307
|
+
|
|
308
|
+
One semantic node can render as more than one element: an input is a label wrapping a control,
|
|
309
|
+
and **both carry `data-node`, because both are that node**. Only one of them can be typed into,
|
|
310
|
+
and `data-node` cannot say which.
|
|
311
|
+
|
|
312
|
+
| Attribute | Selects | Cardinality |
|
|
313
|
+
| --- | --- | --- |
|
|
314
|
+
| `data-node` | every element that is this semantic node | one or more per rendering |
|
|
315
|
+
| `data-control` | the single element a person operates | exactly one, on nodes that have one |
|
|
316
|
+
| `data-instance` | this rendering of the node | one per element, inside a `repeat` |
|
|
317
|
+
| `data-variant` | the control variant chosen from presentation intent — `switch`, `stepper`, `radio-group` | on the control |
|
|
318
|
+
|
|
319
|
+
So `[data-control="ui_line_quantity"]` is the input, `[data-node="ui_line_quantity"]` is the
|
|
320
|
+
input and its label, and inside a repeat `[data-control="…"][data-instance="…"]` is one row's.
|
|
321
|
+
A `button` is its own control, so both attributes name the same element. A radio group has no
|
|
322
|
+
single element to operate, so the group itself carries `data-control`.
|
|
323
|
+
|
|
324
|
+
Semantic identity stays on the wrappers: `AgentAPI` and any tooling that reasons about the
|
|
325
|
+
graph needs to find every element a node produced, not only the interactive one.
|
|
326
|
+
|
|
267
327
|
## Visibility is not authorization
|
|
268
328
|
|
|
269
329
|
`visibleWhen` and `ConditionalNode` decide what is **rendered**. They decide nothing about
|
package/docs/VALIDATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Validation
|
|
2
2
|
|
|
3
|
-
Axiom 0.6.
|
|
3
|
+
Axiom 0.6.2-alpha.1. Validation is authoring-time structural checking. It is not the same
|
|
4
4
|
as runtime constraint evaluation — see [`CONSTRAINTS.md`](CONSTRAINTS.md) for the four
|
|
5
5
|
layers of correctness.
|
|
6
6
|
|
|
@@ -79,6 +79,7 @@ non-numeric collections, and obviously incompatible assignments.
|
|
|
79
79
|
| `INVALID_SELECTOR_TYPE` | An index selector that is statically not a number. |
|
|
80
80
|
| `EPHEMERAL_STATE_PERSISTED` | `ephemeral: true` together with `persistence`. |
|
|
81
81
|
| `CLIENT_WRITE_TO_SERVER_STATE` | An input bound into server-authoritative state. See [Authority](#authority). |
|
|
82
|
+
| `MISSING_ACTION_ARGUMENT` | A control invokes an action without supplying a required parameter. |
|
|
82
83
|
|
|
83
84
|
### Initial values
|
|
84
85
|
|
|
@@ -148,6 +149,7 @@ client commit authoritative state does not compile. Full model:
|
|
|
148
149
|
| `SERVER_ONLY_STATE_OBSERVED` | Something the client receives reads a `serverOnly` state — an input, a derivation, a UI expression. | error |
|
|
149
150
|
| `AUTHORIZATION_WITHOUT_PRINCIPAL` | An `authorization` expression with no principal entity declared. **Also raised as a warning** when an application has server state but no action declares authorization, so every caller may invoke everything. | error / warning |
|
|
150
151
|
| `PRINCIPAL_REFERENCE_ON_CLIENT` | `PRINCIPAL` is read where a client evaluates, or an `authorization` sits on an action no authority executes. | error |
|
|
152
|
+
| `MISSING_ACTION_ARGUMENT` | A `button`, or a `form` submitting without one, invokes an action with a required parameter it never supplies. The invocation would always be refused for a missing argument, so it is refused here instead. | error |
|
|
151
153
|
| `INVALID_PRINCIPAL_ENTITY` | `principalEntityId` names something that is not an entity. | error |
|
|
152
154
|
|
|
153
155
|
### Accessibility
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2-alpha.1",
|
|
4
4
|
"description": "AI-native semantic web application framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@cynodia/axiom-core": "0.6.
|
|
36
|
-
"@cynodia/axiom-runtime": "0.6.
|
|
37
|
-
"@cynodia/axiom-compiler": "0.6.
|
|
38
|
-
"@cynodia/axiom-agent-api": "0.6.
|
|
35
|
+
"@cynodia/axiom-core": "0.6.2-alpha.1",
|
|
36
|
+
"@cynodia/axiom-runtime": "0.6.2-alpha.1",
|
|
37
|
+
"@cynodia/axiom-compiler": "0.6.2-alpha.1",
|
|
38
|
+
"@cynodia/axiom-agent-api": "0.6.2-alpha.1"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -b tsconfig.json"
|