@cynodia/axiom 0.5.2-alpha.1 → 0.6.1-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
@@ -6,13 +6,13 @@ Axiom represents application behavior, state, UI structure and presentation as s
6
6
  semantic data executed by generic runtimes. An application is a typed graph, not source
7
7
  files: the JavaScript and HTML that reach the browser are output, and are never edited.
8
8
 
9
- **Status: experimental / alpha (0.5.2-alpha.x).** The API may change between alpha
9
+ **Status: experimental / alpha (0.6.0-alpha.x).** The API may change between alpha
10
10
  releases. The documentation in `docs/` describes this exact version.
11
11
 
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.5.2-alpha.1. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.6.1-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
@@ -62,6 +78,10 @@ re-render
62
78
  `invokeAction(id, args?)` returns `{ ok, diagnostics }` for **that invocation**. No diffing
63
79
  of global history is needed.
64
80
 
81
+ An action that writes server-authoritative state runs this lifecycle **on the authority**
82
+ instead, unchanged; the client dispatches it and receives the result. See
83
+ [`AUTHORITY.md`](AUTHORITY.md).
84
+
65
85
  All four failure sources are evaluated; the action does not stop at the first.
66
86
 
67
87
  ## Operations
@@ -159,6 +179,20 @@ The controlled boundary for behavior the operation vocabulary cannot express.
159
179
  **Use it only where no semantic primitive exists.** A native operation is opaque to every
160
180
  analysis Axiom offers.
161
181
 
182
+ ## Authorization
183
+
184
+ ```ts
185
+ authorization?: Expression
186
+ ```
187
+
188
+ Whether the caller may invoke this action at all, evaluated **on the authority** with the
189
+ caller bound to `PRINCIPAL`, before any guard and before any transaction opens. It is
190
+ stripped from the client IR, so a client never learns the rule and cannot satisfy it by
191
+ claiming to.
192
+
193
+ A guard asks whether the application's state permits this invocation. Authorization asks
194
+ whether this *caller* may make it. See [`AUTHORITY.md`](AUTHORITY.md#authentication-and-authorization).
195
+
162
196
  ## Postconditions
163
197
 
164
198
  Evaluated after the operations, inside the transaction, against proposed state. A failure
package/docs/AGENT_API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent API
2
2
 
3
- Axiom 0.5.2-alpha.1. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.6.1-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
@@ -141,6 +142,24 @@ Presentation resolution is recomputed per call rather than cached: a transaction
141
142
  the graph underneath these queries, and a stale presentation answer would be worse than a
142
143
  slow one.
143
144
 
145
+ ## Authority queries
146
+
147
+ ```ts
148
+ agent.getAuthority(stateId) // 'client' | 'server'
149
+ agent.getActionAuthority(actionId) // where it executes — derived, not declared
150
+ agent.getServerActions()
151
+ agent.getClientWritableStates()
152
+ agent.getServerWritableStates()
153
+ agent.getServerOnlyStates() // what the client never receives
154
+ agent.getActionsAffectingServerState() // [{ action, stateIds }]
155
+ agent.getAuthorizationForAction(actionId) // the rule, or undefined
156
+ agent.getUnauthorizedServerActions() // server actions any caller may invoke
157
+ agent.getPersistenceForState(stateId)
158
+ ```
159
+
160
+ Authority is derived from what an action writes, so these answers cannot disagree with what
161
+ the graph does. See [`AUTHORITY.md`](AUTHORITY.md).
162
+
144
163
  ## Transactions
145
164
 
146
165
  Every change is staged on a private copy. The graph an agent or a runtime can observe is
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.5.2-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.6.1-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
4
4
  declarations before authoring or modifying an Axiom application.
5
5
 
6
6
  Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
@@ -31,7 +31,7 @@ One canonical term per concept. These are not interchangeable.
31
31
  ## Graph construction
32
32
 
33
33
  ```ts
34
- const graph = new ApplicationGraph(id, name); // version defaults to '0.5.2'
34
+ const graph = new ApplicationGraph(id, name); // version defaults to '0.6.0'
35
35
  graph.addNode<StateDef>({ id, kind: 'state', ... }); // returns NodeId; throws if id exists
36
36
  graph.getNode<StateDef>(id); // deep clone, or undefined
37
37
  graph.updateNode(node); // write a modified node back
@@ -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
 
@@ -469,6 +482,98 @@ agent.transact((tx) => { tx.setDensity(FORM, 'compact'); }, { reason });
469
482
  a native operation with undeclared effects — cannot be analyzed. **An incomplete answer
470
483
  says so; it is never presented as exhaustive.** Detail: [`AGENT_API.md`](AGENT_API.md).
471
484
 
485
+ ## SERVER AUTHORITY
486
+
487
+ Full model: [`AUTHORITY.md`](AUTHORITY.md). These are the invariants to know before
488
+ authoring an application that crosses the trust boundary.
489
+
490
+ 1. **AUTHORITY** — a client cannot commit server-authoritative state, by any path.
491
+ 2. **EXECUTION** — a server action executes against state the authority owns, on the authority.
492
+ 3. **TRUST** — a client's validation results, derived values and claims are never authoritative.
493
+ 4. **TRANSACTION** — one semantic action commits atomically or not at all, wherever it runs.
494
+ 5. **CONCURRENCY** — two actions cannot both commit from incompatible snapshots.
495
+ 6. **PROTOCOL** — a client requests semantic actions, never mutation programs.
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.
503
+
504
+ ```ts
505
+ { id: STATE_PRODUCTS, kind: 'state', authority: 'server' } // the authority owns it
506
+ { id: STATE_AUDIT, kind: 'state', authority: 'server', serverOnly: true } // and the client never sees it
507
+ ```
508
+
509
+ - `authority` defaults to `'client'`. **Every 0.5.x graph is unchanged and still runs with no server.**
510
+ - Authority is separate from persistence: one says who decides a value, the other where a decided value survives.
511
+ - **Where an action executes is derived, never declared**: an action that writes any server-authoritative state is a server action.
512
+ - A server action reaches the client as its id, name and parameters only — no operations, no guards, no failure modes, no authorization.
513
+ - A server action MUST NOT read client state. Pass the value as an action parameter; that is what a draft is for.
514
+
515
+ ```ts
516
+ compileToIR(graph) // the client half, filtered at the boundary
517
+ compileToServerIR(graph) // what an authority executes: no UI, no presentation, no routes
518
+ ```
519
+
520
+ ### Authorization
521
+
522
+ ```ts
523
+ graph.setPrincipalEntity(ENTITY_USER);
524
+ { kind: 'action', authorization: binary('eq', field(ref(PRINCIPAL), F_ROLE), literal('admin')), … }
525
+ ```
526
+
527
+ `PRINCIPAL` is bound to a record keyed by the principal entity's field ids, and **only where
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.
539
+
540
+ ### Running one
541
+
542
+ One graph, one process — the generated page and the authority that answers it:
543
+
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
+ });
552
+ ```
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
+
559
+ A remote invocation returns `{ ok: false, pending: true }` and its outcome arrives later,
560
+ through the same action-outcome lifecycle a local refusal uses — so a `diagnostic` node
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
+ ```
572
+
573
+ Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZATION_DENIED`
574
+ `CONCURRENCY_CONFLICT` `MALFORMED_REQUEST` `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE`
575
+ and `REMOTE_ACTION_UNAVAILABLE` on the client.
576
+
472
577
  ## Serialization
473
578
 
474
579
  The whole graph, theme included, is JSON. `graph.serialize()` /
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.5.2-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.6.1-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
@@ -334,7 +334,66 @@ Renderer-generated ids are keyed by **render instance**: `data-node` is the sema
334
334
  all — see anti-pattern 3 — but a test or a host that must locate an element should select on
335
335
  `data-node` and pick the instance it means.
336
336
 
337
- ## 26. Restating what the model already knows
337
+ ## 26. Binding an input into server-authoritative state
338
+
339
+ ```ts
340
+ // WRONG — rejected by validateGraph (CLIENT_WRITE_TO_SERVER_STATE), and refused at run time.
341
+ { kind: 'input', binding: { location: itemFieldLocation(STATE_PRODUCTS, F_ID, …, F_STOCK) } }
342
+
343
+ // RIGHT — edit a client draft, and commit it through an action the authority executes.
344
+ { kind: 'input', binding: { location: stateLocation(STATE_DRAFT_QUANTITY) } }
345
+ { kind: 'button', actionId: ACTION_PLACE_ORDER, arguments: { [PARAM_QUANTITY]: ref(STATE_DRAFT_QUANTITY) } }
346
+ ```
347
+
348
+ Do not force every keystroke across the boundary either: a draft is client-local on purpose.
349
+
350
+ ## 27. A server action reading client state
351
+
352
+ ```ts
353
+ // WRONG — the authority does not have the client's draft. SERVER_DEPENDS_ON_CLIENT_STATE.
354
+ { kind: 'action', operations: [{ kind: 'insert', target: stateLocation(STATE_ORDERS), value: ref(STATE_DRAFT) }] }
355
+
356
+ // RIGHT — the client proposes values as arguments; the authority decides.
357
+ {
358
+ kind: 'action',
359
+ parameters: [{ id: PARAM_QUANTITY, valueType: primitiveType('number'), required: true }],
360
+ operations: [{ kind: 'insert', target: stateLocation(STATE_ORDERS), value: object([…]) }],
361
+ }
362
+ ```
363
+
364
+ ## 28. Treating client-side checks as authoritative
365
+
366
+ ```ts
367
+ // WRONG — a guard the client evaluated proves nothing. So does an argument saying so.
368
+ invoke(ACTION_PLACE_ORDER, { quantity: 5, alreadyValidated: true })
369
+ ```
370
+
371
+ The authority re-evaluates guards, constraints, transition rules, argument types and
372
+ authorization against **its own** state, every time. An extra argument is not a parameter and
373
+ is refused outright. Confirmation is interaction, never authorization.
374
+
375
+ ## 29. Putting a secret in the graph
376
+
377
+ ```ts
378
+ // WRONG — a graph is a serializable artifact, and this state ships to the client.
379
+ { id: STATE_API_KEY, kind: 'state', valueType: primitiveType('string'), initialValue: 'sk-…' }
380
+ ```
381
+
382
+ Server-only information belongs behind `authority: 'server'` with `serverOnly: true`, and a
383
+ credential belongs to the host, not the graph.
384
+
385
+ ## 30. Reaching for a native operation to make an external call
386
+
387
+ ```ts
388
+ // WRONG — a database write rolls back; an email does not. Nothing makes this participate
389
+ // in the transaction, and pretending it does is worse than not having it.
390
+ { kind: 'native', implementationId: 'app.sendEmail' }
391
+ ```
392
+
393
+ External effects are deliberately unsolved in 0.6. A design involving commands, a
394
+ transactional outbox and idempotency is future work; do not smuggle one in meanwhile.
395
+
396
+ ## 31. Restating what the model already knows
338
397
 
339
398
  ```ts
340
399
  // UNNECESSARY — required comes from the field, the label from the field's name, the