@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 CHANGED
@@ -12,7 +12,7 @@ releases. The documentation in `docs/` describes this exact version.
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- npm install @cynodia/axiom@alpha
15
+ npm install @cynodia/axiom
16
16
  ```
17
17
 
18
18
  ## Canonical mental model
@@ -1,6 +1,6 @@
1
1
  # Actions and transactions
2
2
 
3
- Axiom 0.6.0-alpha.1. An action is behavior expressed as data, executed as a transaction.
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.0-alpha.1. The machine-facing interface. Agents query semantics and apply
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
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.6.0-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
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**. A rule that cannot be evaluated denies. `requiresConfirmation` is
510
- interaction, not authorization.
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
- ```ts
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
- const app = createAxiomRuntime({ ir, rootElement, host, remote: createRemoteGateway(transport) });
520
- await app.syncAuthoritativeState();
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`
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.6.0-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.6.2-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
4
4
  alternative.
5
5
 
6
6
  ## 1. Field names as entity runtime keys
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.6.0-alpha.1. How an application crosses the trust boundary.
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`. The client runtime generates one per invocation automatically. Exactly-once
216
- delivery is not assumed.
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
- ## Observing authoritative state
347
+ ## Startup
243
348
 
244
- The model is the simplest correct one: **request, decide, apply**.
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: createRemoteGateway(transport) });
248
- app.start();
249
- await app.syncAuthoritativeState(); // load the authoritative snapshot
361
+ const app = createAxiomRuntime({ ir, rootElement, host, remote });
362
+ await app.start(); // rendered, restored, synchronized
363
+ app.authoritativeStateLoaded(); // true
250
364
  ```
251
365
 
252
- - 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.
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
- ```bash
294
- node packages/cli/dist/index.js serve app.js --export=createGraph --port=3000 --store=state.db
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
- One process serves the client page and the authority on one semantic endpoint. Nothing
298
- application-specific is generated: no routes, no controllers, no handlers, no SQL. The
299
- deployment artifact is the client page plus a serialized Server IR, executed by a generic
300
- runtime.
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
- const server = createAxiomServer({ ir: serverIR, persistence, host });
304
- await server.start();
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
- ## Not in 0.6
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.
@@ -1,6 +1,6 @@
1
1
  # Constraints
2
2
 
3
- Axiom 0.6.0-alpha.1. Two constructs, answering different questions. They are not
3
+ Axiom 0.6.2-alpha.1. Two constructs, answering different questions. They are not
4
4
  interchangeable.
5
5
 
6
6
  | | Question | Sees |
@@ -1,6 +1,6 @@
1
1
  # Expressions
2
2
 
3
- Axiom 0.6.0-alpha.1. An expression describes **what value is computed**. It is a tree of
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
 
@@ -1,6 +1,6 @@
1
1
  # Graph model
2
2
 
3
- Axiom 0.6.0-alpha.1. The `ApplicationGraph` is the authoritative representation of an
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
@@ -1,6 +1,6 @@
1
1
  # Locations
2
2
 
3
- Axiom 0.6.0-alpha.1.
3
+ Axiom 0.6.2-alpha.1.
4
4
 
5
5
  ```text
6
6
  Expression = a value
@@ -1,6 +1,6 @@
1
1
  # Presentation
2
2
 
3
- Axiom 0.6.0-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
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.0-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
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()` | — | Subscribes to path changes and renders. Idempotent. |
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. Full model: [`AUTHORITY.md`](AUTHORITY.md).
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.0-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
4
- does not teach. Where this file and any specification in `doc/` disagree, this file
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, external side effects participating in a transaction, realtime
204
- synchronization, query semantics, and multi-node execution.
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 `'cancelled'`, with that invocation's diagnostics.
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
- - There are no asynchronous action semantics, and therefore no loading or pending presentation states.
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
@@ -1,6 +1,6 @@
1
1
  # State
2
2
 
3
- Axiom 0.6.0-alpha.1. A `StateDef` is a named application value: stored, or computed from
3
+ Axiom 0.6.2-alpha.1. A `StateDef` is a named application value: stored, or computed from
4
4
  other state.
5
5
 
6
6
  ```ts
package/docs/UI.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # UI
2
2
 
3
- Axiom 0.6.0-alpha.1. Ten semantic UI node kinds describe **what exists and what it does**.
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. `destructive` may be declared here, but declaring it on
166
- the action is enough — presentation is inferred from the action.
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
- <input data-node="ui_line_quantity"
260
- data-instance="ui_line_quantity--line-7f3a"
261
- id="axiom-control-ui_line_quantity--line-7f3a">
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
@@ -1,6 +1,6 @@
1
1
  # Validation
2
2
 
3
- Axiom 0.6.0-alpha.1. Validation is authoring-time structural checking. It is not the same
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.0-alpha.1",
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.0-alpha.1",
36
- "@cynodia/axiom-runtime": "0.6.0-alpha.1",
37
- "@cynodia/axiom-compiler": "0.6.0-alpha.1",
38
- "@cynodia/axiom-agent-api": "0.6.0-alpha.1"
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"