@cynodia/axiom 0.6.3-alpha.1 → 0.7.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,13 +6,15 @@ 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.6.0-alpha.x).** The API may change between alpha
9
+ **Status: experimental / alpha (0.7.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
15
+ npm install @cynodia/axiom # the graph, compiler, runtime and agent API
16
+ npm install @cynodia/axiom-ui # semantic UI authoring patterns (build time only)
17
+ npm install @cynodia/axiom-server # only if the application has an authority
16
18
  ```
17
19
 
18
20
  ## Canonical mental model
@@ -26,15 +28,24 @@ npm install @cynodia/axiom
26
28
  | `ActionDef` | A transactional semantic operation. |
27
29
  | `ConstraintDef` | An invariant over proposed state. |
28
30
  | `TransitionConstraintDef` | An invariant over previous committed state → proposed state. |
29
- | UI nodes | Semantic interaction structure. |
31
+ | UI nodes | Semantic interaction structure (view, container, text, repeat, field-display, form, input, button, conditional, diagnostic, dialog). |
30
32
  | `Presentation` | Semantic UX intent. Roles and tokens, never CSS. |
31
33
  | `Theme` | Translation of presentation intent into visual design. |
32
34
  | Renderer | Platform-specific materialization. Not part of the graph. |
35
+ | `StateDef.authority` | Who may commit a value: the client, or the server. The one declaration the split follows from. |
36
+ | `ServerIR` | The half an authority executes. Portable JSON, frozen as `axiom.server.v1`. |
37
+ | Semantic protocol | What a client may ask for: named actions with arguments, never mutation programs. |
38
+ | `PersistenceAdapter` | Where a decided value survives. Not part of the semantics. |
33
39
 
34
40
  ```text
35
- ApplicationGraph → validateGraph → compileToIR → runtime (+ theme → renderer) → application
41
+ ApplicationGraph → validateGraph → compileToIR → runtime (+ theme → renderer) → page
42
+ → compileToServerIR → authority (+ persistence)
36
43
  ```
37
44
 
45
+ Authority is **derived, never declared twice**: an action that writes server-owned state is a
46
+ server action, so where code runs cannot disagree with what it does. Full model:
47
+ `docs/AUTHORITY.md`.
48
+
38
49
  ## Load-bearing invariants
39
50
 
40
51
  Know these before authoring an application. Each is stated in full in
@@ -53,6 +64,10 @@ Know these before authoring an application. Each is stated in full in
53
64
  11. **`null` and `[]` are distinct.** `null` fails a collection operator; `[]` does not. A collection is truthy only when non-empty.
54
65
  12. **`required(x)` asks only whether a value exists.** `required([])` is `true`.
55
66
  13. **A theme changes presentation only.**
67
+ 14. **A client cannot commit server-authoritative state**, by any path. An action that writes it executes on the authority.
68
+ 15. **The client is untrusted.** Guards, authorization and argument types are checked again on the authority.
69
+ 16. **A client requests semantic actions, never mutation programs.** The protocol carries no way to send operations.
70
+ 17. **`axiom.server.v1` is frozen and language-independent.** Its semantics are defined by `docs/AUTHORITY.md`, the published JSON Schemas and the conformance fixtures — not by this implementation.
56
71
 
57
72
  ## Minimal application
58
73
 
@@ -126,6 +141,7 @@ The complete operational contract ships with this package, in `docs/`.
126
141
  | Semantic UI nodes and bindings | `docs/UI.md` |
127
142
  | Presentation, UX intent, themes, formatting | `docs/PRESENTATION.md` |
128
143
  | Runtime API and diagnostic codes | `docs/RUNTIME.md` |
144
+ | Server authority, Server IR, the protocol and persistence | `docs/AUTHORITY.md` |
129
145
  | Machine queries and graph transformations | `docs/AGENT_API.md` |
130
146
  | Validation codes | `docs/VALIDATION.md` |
131
147
  | Mistakes that compile but are wrong | `docs/ANTI_PATTERNS.md` |
@@ -144,6 +160,13 @@ This package re-exports four, which can also be installed individually:
144
160
  | `@cynodia/axiom-runtime` | State store, evaluation, mutation engine, constraint checking, renderer, routing. |
145
161
  | `@cynodia/axiom-agent-api` | Semantic and presentation queries, mutation impact, transactional transformations. |
146
162
 
163
+ Two published packages are deliberately **not** re-exported, and are installed separately:
164
+
165
+ | Package | Why it stands apart |
166
+ | --- | --- |
167
+ | `@cynodia/axiom-server` | The authoritative runtime: Server IR execution, persistence adapters, the semantic protocol and the Node host. It imports `node:http` and `node:sqlite`, and a browser bundle must not. |
168
+ | `@cynodia/axiom-ui` | Semantic UI authoring patterns, expanded into ordinary graph nodes at **build time**. Re-exporting it would make every application carry an authoring dependency forever, and would make "this application no longer needs the toolkit" impossible to state or to test. |
169
+
147
170
  ## License
148
171
 
149
172
  MIT
@@ -1,6 +1,6 @@
1
1
  # Actions and transactions
2
2
 
3
- Axiom 0.6.3-alpha.1. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.7.0-alpha.2. An action is behavior expressed as data, executed as a transaction.
4
4
 
5
5
  ```ts
6
6
  {
package/docs/AGENT_API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent API
2
2
 
3
- Axiom 0.6.3-alpha.1. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.7.0-alpha.2. The machine-facing interface. Agents query semantics and apply
4
4
  structural transformations; they never edit generated code.
5
5
 
6
6
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.6.3-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.7.0-alpha.2. Compressed operational contract. Read this plus the `.d.ts`
4
4
  declarations before authoring or modifying an Axiom application.
5
5
 
6
6
  Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
@@ -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.6.0'
34
+ const graph = new ApplicationGraph(id, name); // version defaults to '0.7.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
@@ -92,7 +92,7 @@ output, and anything read back with `getState`.
92
92
 
93
93
  ## Expressions
94
94
 
95
- 15 kinds. Full semantics: [`EXPRESSIONS.md`](EXPRESSIONS.md).
95
+ Every kind is in `EXPRESSION_KINDS`. Full semantics: [`EXPRESSIONS.md`](EXPRESSIONS.md).
96
96
 
97
97
  ```ts
98
98
  literal(v) ref(id) field(src, fieldId) object(entries, entityId?)
@@ -100,8 +100,12 @@ binary(op, l, r) unary(op, operand) call(fn, ...args) conditional(c, t, f)
100
100
  filter(src, scopeId, predicate) find(src, scopeId, predicate)
101
101
  map(src, scopeId, projection) sort(src, scopeId, by, direction?)
102
102
  every(src, scopeId, predicate) some(src, scopeId, predicate) flatten(src)
103
+ group(src, scopeId, by) expressionRef(expressionId, args?)
103
104
  ```
104
105
 
106
+ - `group` partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`, read with `groupKey(g)` and `groupItems(g)`. Groups appear in **first-seen key order**, members keep source order, keys compare structurally. Nothing is sorted — use `sort` for that.
107
+ - `expressionRef` evaluates a named `ExpressionDef` node: the calculation exists **once** in the graph and every consumer references it. Arguments are evaluated in the calling scope; **the body is evaluated in an isolated scope** that sees its parameters and application state and nothing else, so a definition means the same thing everywhere and its scope ids can never collide with a caller's.
108
+
105
109
  Builtins (14): `required` `is-empty` `non-empty` `length` `contains` `concat` `coalesce`
106
110
  `one-of` `count` `sum` `lowercase` `to-string` `now` `uuid`.
107
111
 
@@ -316,13 +320,14 @@ boolean types (`TYPE_MISMATCH`).
316
320
 
317
321
  ## UI nodes
318
322
 
319
- Ten kinds: `view` `container` `text` `repeat` `field-display` `form` `input` `button`
320
- `conditional` `diagnostic`. Detail: [`UI.md`](UI.md).
323
+ Every kind is in `UI_NODE_KINDS`: `view` `container` `text` `repeat` `field-display` `form`
324
+ `input` `button` `conditional` `diagnostic` `dialog`. Detail: [`UI.md`](UI.md).
321
325
 
322
326
  - `RepeatNode` binds the current item to **the repeat node's own id**; the template refers to it as `ref(repeatNodeId)`.
323
327
  - `InputNode.binding` is `{ location }` — no expression, no field id. An input write goes through the same mutation engine and transaction as an action.
324
328
  - `ButtonNode.arguments` is keyed by **action parameter id**.
325
329
  - `FormNode` submits either a generated button (`submitActionId` + `submitLabel`) or a declared one (`submitButtonId`), which stays an ordinary queryable node.
330
+ - **`DialogNode` is how you ask for confirmation in a modal**, not `ActionDef.requiresConfirmation`. The two are different things: `requiresConfirmation` delegates to the host's own confirmation — `window.confirm` in a browser — which the application cannot label, focus, style or observe. A `dialog` declares the accessible name, the content, what closes it and where focus starts and returns; the runtime performs focus movement, containment, `Escape` and the ARIA relationships. Use `requiresConfirmation` for a coarse "are you sure" with no content of its own, and a `dialog` for anything a person needs to read.
326
331
  - `DiagnosticNode` presents why an action refused. See [Action diagnostics](#action-diagnostics).
327
332
  - `visibleWhen` and `ConditionalNode` are interaction behavior, **not authorization**.
328
333
 
@@ -343,6 +348,30 @@ identity field and falls back to a deterministic index; nested repeats compose.
343
348
 
344
349
  The graph still holds one node. `AgentAPI` reasons about that node, never about instances.
345
350
 
351
+ ## Authoring UI: pattern, primitive, or node
352
+
353
+ Nodes are the model. They are not the only authoring surface, and choosing between the three
354
+ is a decision an agent should make deliberately:
355
+
356
+ | The requirement is | Use | Where |
357
+ | --- | --- | --- |
358
+ | recurring application UX that expands deterministically into existing semantics | a **pattern** | `@cynodia/axiom-ui` |
359
+ | interaction behaviour the runtime must perform | a **canonical interaction primitive** | `dialog`, in core |
360
+ | custom but already expressible | **canonical nodes**, composed | this document |
361
+ | genuinely unsupported presentation | `rendererOverrides.web.className`, and nothing more | [`PRESENTATION.md`](PRESENTATION.md) |
362
+
363
+ The five patterns are `page`, `metric-grid`, `entity-list`, `entity-form` and `action-bar`.
364
+ Expansion happens **at authoring time**: afterwards the graph is ordinary canonical Axiom, and
365
+ nothing at run time knows a pattern existed. Ownership defaults to the **declaration**, so
366
+ editing a generated node is drift, reported per node and per property; `materializePattern`
367
+ hands ownership to the graph when an edit is what you actually want. A pattern never creates
368
+ state, an action, a constraint or an authority.
369
+
370
+ Interaction behaviour is never a pattern: a pattern can only emit nodes that already exist, so
371
+ focus movement, containment, `Escape`, typeahead and active descendant are unreachable from one
372
+ — see [interaction primitives](UI.md#interaction-primitives) for the classification, including
373
+ `combobox`, which is classified and not implemented.
374
+
346
375
  ## Action diagnostics
347
376
 
348
377
  The runtime already knows why an action refused. A `DiagnosticNode` makes that available
@@ -574,6 +603,50 @@ Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZ
574
603
  `CONCURRENCY_CONFLICT` `MALFORMED_REQUEST` `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE`
575
604
  and `REMOTE_ACTION_UNAVAILABLE` on the client.
576
605
 
606
+ ## Metadata classes
607
+
608
+ ```ts
609
+ metadata: { [AUTHORING_METADATA_KEY]: { … }, tracked: true }
610
+ // ↑ stripped from every compiled artifact ↑ kept
611
+ ```
612
+
613
+ Authoring metadata describes how a node was authored, never how it executes. Stripped from
614
+ client IR, server IR and the generated page by default; `compileToIR(graph, {
615
+ includeAuthoringMetadata: true })` keeps it for a tool. Nothing may branch on it at run time.
616
+
617
+ ## Renderability
618
+
619
+ ```ts
620
+ validateGraph(graph, { renderer: BROWSER_RENDERER_CAPABILITIES }); // UNSUPPORTED_UI_NODE_KIND
621
+ compileToIR(graph); // applies it by default
622
+ ```
623
+
624
+ A UI node kind is only in the contract if a renderer implements it. A renderer publishes
625
+ `{ target, supportedUiKinds }` and must implement everything it publishes.
626
+
627
+ Capabilities describe **node-kind** support, not partial support: a renderer cannot yet say
628
+ that it draws a kind but not one of its options. Nothing in the current vocabulary needs that,
629
+ and it is a deliberate future extension rather than an oversight.
630
+
631
+ ## Named expressions
632
+
633
+ ```ts
634
+ graph.addNode<ExpressionDef>({
635
+ id: X_LOW_STOCK,
636
+ kind: 'expression',
637
+ name: 'Low stock',
638
+ parameters: [{ id: P_SOURCE, valueType: collectionType(entityType(E_PRODUCT)) }],
639
+ expression: filter(ref(P_SOURCE), SC, binary('lte', field(ref(SC), F_STOCK), ref(S_THRESHOLD))),
640
+ });
641
+
642
+ expressionRef(X_LOW_STOCK, { [P_SOURCE]: ref(S_PRODUCTS) }) // in any number of consumers
643
+ ```
644
+
645
+ - **MUST** supply every declared parameter; an unsupplied one is `MISSING_EXPRESSION_ARGUMENT`.
646
+ - **MUST NOT** reach the caller's scope from the body. It resolves parameters and state only.
647
+ - A definition that reaches itself is `EXPRESSION_DEF_CYCLE`.
648
+ - Dependencies are graph edges: `agent.getExpressionConsumers(id)`, `agent.getExpressionDependencies(id)`, and a consumer's read edges include everything the definition reads — so an answer does not change because a calculation was given a name.
649
+
577
650
  ## Serialization
578
651
 
579
652
  The whole graph, theme included, is JSON. `graph.serialize()` /
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.6.3-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.7.0-alpha.2. Each of these compiles. Each is wrong. Each is followed by the correct
4
4
  alternative.
5
5
 
6
6
  ## 1. Field names as entity runtime keys
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.6.3-alpha.1. How an application crosses the trust boundary.
3
+ Axiom 0.7.0-alpha.2. How an application crosses the trust boundary.
4
4
 
5
5
  Until 0.5.x an Axiom application executed locally. 0.6 adds an **authority**: a generic
6
6
  runtime that owns state, decides mutations and persists them. The same semantic graph
@@ -95,8 +95,10 @@ which states a client may observe. It carries **no UI, no presentation, no theme
95
95
  routes**, because none of that decides anything.
96
96
 
97
97
  It is plain JSON — deterministic, closure-free, and specific to no language or host. It
98
- declares `contract: 'axiom.server.v1'`, and a runtime that does not recognize the value MUST
99
- refuse it rather than interpret it partially.
98
+ declares a `contract`, and a runtime that does not recognize the value MUST refuse it rather
99
+ than interpret it partially. **The declared contract is the oldest one that can carry the
100
+ document**, computed from the document rather than asserted: see
101
+ [contract identifiers](#contract-identifiers).
100
102
 
101
103
  Guards are normalized into aligned `preconditions` / `failureModes`, exactly as in the
102
104
  client IR, so an authority that read one and not the other cannot silently skip a check.
@@ -515,6 +517,34 @@ them.
515
517
  They describe **structure**. What a conforming runtime must *do* with a valid document is this
516
518
  page plus the conformance fixtures.
517
519
 
520
+ ## Contract identifiers
521
+
522
+ | Contract | Adds | Since |
523
+ | --- | --- | --- |
524
+ | `axiom.server.v1` | the frozen 0.6.1 contract, below | 0.6.1 |
525
+ | `axiom.server.v2` | the expression kinds `group` and `expression-ref`, and the `expressionDefs` they resolve against | 0.7.0 |
526
+
527
+ `SERVER_IR_CONTRACTS` enumerates both. The rules:
528
+
529
+ - **A document declares the oldest contract that can carry it.** `compileToServerIR` computes the label from the vocabulary the document actually uses, so an application that uses nothing from 0.7 produces a byte-identical `axiom.server.v1` document, and the committed v1 conformance fixtures are unchanged.
530
+ - **A runtime MUST refuse a contract it does not implement**, and MUST refuse a document whose vocabulary exceeds its declared contract. A v2 runtime executing a v1-labelled document that uses `group` would accept what a conforming v1 runtime elsewhere refuses, and the two would then disagree about the same file. `createAxiomServer` raises rather than executing one.
531
+ - **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref` or `expressionDefs`, and `server-ir.v1.schema.json` is byte-frozen. Vocabulary arrives under a new identifier or not at all.
532
+
533
+ There is one JSON Schema per contract, each generated from the runtime's own vocabulary and
534
+ each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`.
535
+
536
+ **`group`.** Partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`. Groups appear
537
+ in the order their key was **first seen** in the source; members keep source order; two keys are
538
+ the same key when they are **structurally** equal; an empty source produces no groups; a `null`
539
+ source fails the evaluation as every collection operator does. Nothing is sorted. A group is a
540
+ record carrying the two reserved field ids `field_group_key` and `field_group_items`, and no
541
+ entity may declare either.
542
+
543
+ **`expression-ref`.** Evaluates a named `ExpressionDef` from `expressionDefs`. Arguments are
544
+ evaluated in the calling scope; the body is evaluated in an **isolated** scope binding the
545
+ definition's parameters and application state and nothing else. Every declared parameter MUST be
546
+ supplied. A definition that reaches itself is invalid and MUST be refused rather than executed.
547
+
518
548
  ## Server IR v1 is frozen
519
549
 
520
550
  `axiom.server.v1` is a stable semantic contract as of 0.6.1. A runtime may depend on the
@@ -568,7 +598,7 @@ result is identical to some serial order. Across processes, correctness rests on
568
598
  persistence adapter's revision check — the contract guarantees that a commit from a stale
569
599
  snapshot is refused, not that two processes coordinate.
570
600
 
571
- ## Not in 0.6.3
601
+ ## Not in 0.7.0
572
602
 
573
603
  Stated plainly rather than left to discovery:
574
604
 
@@ -1,6 +1,6 @@
1
1
  # Constraints
2
2
 
3
- Axiom 0.6.3-alpha.1. Two constructs, answering different questions. They are not
3
+ Axiom 0.7.0-alpha.2. 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.3-alpha.1. An expression describes **what value is computed**. It is a tree of
3
+ Axiom 0.7.0-alpha.2. 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
 
@@ -116,7 +116,7 @@ Both branches are expressions. Only the chosen branch is evaluated.
116
116
 
117
117
  ### Collection operators
118
118
 
119
- All six bind the current member to `scopeId` and are strict about their source: `null`
119
+ All seven bind the current member to `scopeId` and are strict about their source: `null`
120
120
  fails, `[]` behaves normally.
121
121
 
122
122
  | Builder | Input | Output | On `[]` |
@@ -127,6 +127,7 @@ fails, `[]` behaves normally.
127
127
  | `sort(src, scopeId, by, direction?)` | `Collection<A>` | `Collection<A>` | `[]` |
128
128
  | `every(src, scopeId, predicate)` | `Collection<A>` | `boolean` | **`true`** |
129
129
  | `some(src, scopeId, predicate)` | `Collection<A>` | `boolean` | **`false`** |
130
+ | `group(src, scopeId, by)` | `Collection<A>` | `Collection<Group<K, A>>` | `[]` |
130
131
  | `flatten(src)` | `Collection<Collection<A>>` | `Collection<A>` | `[]` |
131
132
 
132
133
  - `sort` orders by the projected key, ascending unless `direction: 'desc'`. Numbers compare numerically; anything else compares as text. The sort is stable.
@@ -146,6 +147,67 @@ sum(
146
147
  )
147
148
  ```
148
149
 
150
+ ### `group(source, scopeId, by)`
151
+
152
+ Builds `{ kind: 'group', source, scopeId, by }`. Partitions a collection by a key projected
153
+ from each member. `Collection<A>` becomes
154
+ `Collection<Group<K, A>>`, where `by` is evaluated with the member bound to `scopeId` — the
155
+ same iteration scope every other collection operator introduces.
156
+
157
+ A group is a **record keyed by two reserved field ids**, so it is read with the ordinary
158
+ `field` vocabulary rather than an accessor invented for the occasion:
159
+
160
+ ```ts
161
+ group(ref(LINES), LINE, field(ref(LINE), F_CATEGORY)) // Collection<Group<string, Line>>
162
+
163
+ groupKey(ref(GROUP)) // field(ref(GROUP), GROUP_KEY_FIELD) — the shared key
164
+ groupItems(ref(GROUP)) // field(ref(GROUP), GROUP_ITEMS_FIELD) — the members
165
+ ```
166
+
167
+ **The ordering contract is part of the semantics**, not an accident of implementation:
168
+
169
+ | | |
170
+ | --- | --- |
171
+ | Group order | the order each key was **first seen** in the source |
172
+ | Member order within a group | source order, preserved |
173
+ | Key identity | structural equality, so a key may be a nested record and not only a primitive |
174
+ | Empty source | no groups |
175
+ | `null` source | fails the evaluation, like every collection operator |
176
+
177
+ Nothing is sorted. A caller that wants groups in key order says so with `sort`, which is the
178
+ operator whose job that is.
179
+
180
+ `GROUP_KEY_FIELD` and `GROUP_ITEMS_FIELD` are **reserved**: an entity that declares either is
181
+ `RESERVED_FIELD_ID`, and reading either from something that is not a group — or reading
182
+ anything else *from* a group — is `INVALID_GROUP_FIELD`. A field id that meant one thing in
183
+ one place and another elsewhere would defeat the reason ids exist.
184
+
185
+ ### `expressionRef(expressionId, arguments?)`
186
+
187
+ Builds `{ kind: 'expression-ref', expressionId, arguments? }`, which evaluates a named
188
+ `ExpressionDef` — the reuse mechanism. Without it, an expression used in
189
+ three places is written three times, each needing its own scope ids.
190
+
191
+ ```ts
192
+ graph.addNode<ExpressionDef>({
193
+ id: LINES_IN_CATEGORY,
194
+ kind: 'expression',
195
+ parameters: [{ id: P_CATEGORY, valueType: primitiveType('string') }],
196
+ expression: filter(ref(STATE_LINES), LINE, binary('eq', field(ref(LINE), F_CATEGORY), ref(P_CATEGORY))),
197
+ });
198
+
199
+ expressionRef(LINES_IN_CATEGORY, { [P_CATEGORY]: literal('fasteners') })
200
+ ```
201
+
202
+ **Arguments are evaluated in the calling scope; the body is evaluated in an isolated one.**
203
+ The body sees its parameters and application state, and nothing else. That isolation is the
204
+ point: a definition reused in three places cannot pick up an iteration scope from one of
205
+ them, and its internal scope ids can never collide with a caller's.
206
+
207
+ Validation: `UNKNOWN_EXPRESSION_DEF` for a reference to something that is not one;
208
+ `EXPRESSION_DEF_CYCLE` if a definition reaches itself; `MISSING_EXPRESSION_ARGUMENT` and
209
+ `UNKNOWN_EXPRESSION_ARGUMENT` for arguments that do not match the declared parameters.
210
+
149
211
  ## Built-in functions
150
212
 
151
213
  14 names, enumerated by `BUILTIN_FUNCTIONS`. `AGGREGATE_FUNCTIONS` lists those that reduce
@@ -1,6 +1,6 @@
1
1
  # Graph model
2
2
 
3
- Axiom 0.6.3-alpha.1. The `ApplicationGraph` is the authoritative representation of an
3
+ Axiom 0.7.0-alpha.2. 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
 
@@ -25,7 +25,7 @@ edited.
25
25
  ## API
26
26
 
27
27
  ```ts
28
- const graph = new ApplicationGraph(id, name, version?); // version defaults to '0.6.0'
28
+ const graph = new ApplicationGraph(id, name, version?); // version defaults to '0.7.0'
29
29
 
30
30
  graph.addNode<T>(node): NodeId // generates an id if omitted; throws if it exists
31
31
  graph.getNode<T>(id): T | undefined // deep clone
package/docs/LOCATIONS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Locations
2
2
 
3
- Axiom 0.6.3-alpha.1.
3
+ Axiom 0.7.0-alpha.2.
4
4
 
5
5
  ```text
6
6
  Expression = a value
@@ -1,6 +1,6 @@
1
1
  # Presentation
2
2
 
3
- Axiom 0.6.3-alpha.1. Presentation is **semantic UX intent**, expressed as data on a UI
3
+ Axiom 0.7.0-alpha.2. 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-alpha.1. The runtime executes an `ApplicationIR`. It is domain-independent: it
3
+ Axiom 0.7.0-alpha.2. The runtime executes an `ApplicationIR`. It is domain-independent: it
4
4
  contains no knowledge of any application.
5
5
 
6
6
  ## Constructing
@@ -1,6 +1,6 @@
1
1
  # Semantic contract
2
2
 
3
- Axiom 0.6.3-alpha.1. Runtime guarantees, stated formally. This file defines behavior; it
3
+ Axiom 0.7.0-alpha.2. Runtime guarantees, stated formally. This file defines behavior; it
4
4
  does not teach. Where this file and any specification in `../specs/` disagree, this file
5
5
  describes the implementation and is authoritative.
6
6
 
@@ -205,7 +205,7 @@ Full model: [`AUTHORITY.md`](AUTHORITY.md).
205
205
  - Server IR MUST be serializable, deterministic and free of closures, and MUST declare a contract version a runtime can refuse.
206
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.
207
207
 
208
- **Not guaranteed in 0.6.3**, and stated so rather than implied: read authorization per caller
208
+ **Not guaranteed in 0.7.0**, and stated so rather than implied: read authorization per caller
209
209
  or per record, binding a value generated by one operation in a later one, external side
210
210
  effects participating in a transaction, realtime synchronization, query semantics, and
211
211
  multi-node execution.
@@ -222,6 +222,20 @@ multi-node execution.
222
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.
223
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.
224
224
 
225
+ ## Metadata classes
226
+
227
+ - `metadata` is free-form and travels with a node. Anything under the reserved key `AUTHORING_METADATA_KEY` is **authoring metadata**: it describes how a node was authored, never how it executes.
228
+ - Authoring metadata MUST be stripped from every compiled artifact — client IR, server IR, generated page — unless `includeAuthoringMetadata` is passed. Semantic metadata beside it is untouched.
229
+ - Nothing may branch on authoring metadata at run time. Removing it MUST change validation, compilation, presentation resolution, rendering and runtime behaviour by exactly nothing.
230
+ - The mechanism is generic. A UI toolkit is the first thing that needs it, not the only one.
231
+
232
+ ## Renderability
233
+
234
+ - A UI node kind is part of the contract only if a renderer implements it. `validateGraph(graph, { renderer })` rejects a kind the named target cannot draw, with `UNSUPPORTED_UI_NODE_KIND`.
235
+ - `compileToIR` applies the browser renderer's capabilities by default, so compiling a page rejects an unrenderable node at authoring time rather than producing one that reports `UNSUPPORTED_UI_NODE` on screen.
236
+ - With no renderer named, every kind is accepted: a graph is not rejected for a target nobody named.
237
+ - A renderer publishes what it implements, and MUST implement everything it publishes.
238
+
225
239
  ## Render identity
226
240
 
227
241
  - A UI node inside a `repeat` is rendered once per member. `NodeId` MUST NOT be used alone to identify a rendered element.
@@ -271,6 +285,7 @@ These are current implementation limits, not design intentions.
271
285
  - Type inference is deliberately partial: it rejects obvious mismatches and stays silent where a type depends on an iteration scope.
272
286
  - Iteration scopes are ordinary `NodeId`s; misuse is caught by validation rather than by the type system.
273
287
  - Remote persistence is declared but not executed.
288
+ - A `dialog` declares what is open, its accessible name, its content, what closes it and whether it is modal. The runtime performs focus movement, focus containment, `Escape` dismissal, focus return and the ARIA relationships. Dismissing a dialog invokes its close action and infers no other meaning: closing is not cancelling.
274
289
  - 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
290
  - An action cannot bind a value it generated earlier in the same transaction; `uuid()` in one operation is not addressable by a later one.
276
291
  - 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.3-alpha.1. A `StateDef` is a named application value: stored, or computed from
3
+ Axiom 0.7.0-alpha.2. 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,9 +1,9 @@
1
1
  # UI
2
2
 
3
- Axiom 0.6.3-alpha.1. Ten semantic UI node kinds describe **what exists and what it does**.
3
+ Axiom 0.7.0-alpha.2. Eleven semantic UI node kinds describe **what exists and what it does**.
4
4
  How it looks is [presentation](PRESENTATION.md).
5
5
 
6
- All nine share `UIBase`:
6
+ All eleven share `UIBase`:
7
7
 
8
8
  ```ts
9
9
  { id, kind, name?, visibleWhen?: Expression, presentation?: Presentation, metadata? }
@@ -229,6 +229,48 @@ Lifecycle — see [`RUNTIME.md`](RUNTIME.md#action-diagnostics) for the full con
229
229
  - Messages come from the structured diagnostics — `failureMode.message` for a refused guard, `ConstraintDef.message` for a broken invariant. The renderer invents no wording.
230
230
  - The region is a live region (`role="alert"` for errors, `role="status"` for warnings) and is rendered even when empty, so later content is announced. A control invoking the same action is related to it with `aria-describedby` while it has content.
231
231
 
232
+ ### `dialog`
233
+
234
+ ```ts
235
+ {
236
+ kind: 'dialog',
237
+ openWhen: Expression, // true while it is open
238
+ title: string | Expression, // the accessible name; required
239
+ description?: string | Expression,
240
+ children: NodeId[],
241
+ closeActionId: NodeId, // what dismissal invokes
242
+ modal?: boolean, // default true
243
+ initialFocusId?: NodeId, // a descendant
244
+ returnFocusId?: NodeId, // usually whatever opened it
245
+ }
246
+ ```
247
+
248
+ Content that interrupts, together with the interaction rules that make it a dialog rather
249
+ than a box that appears. It is the case where composing existing nodes stops being enough:
250
+ visibility is expressible with a `conditional`, but focus movement, focus containment,
251
+ `Escape`, focus return and the announcement to assistive technology are behaviour no node
252
+ describes.
253
+
254
+ **The graph says what; the runtime does how.**
255
+
256
+ | The graph declares | The runtime performs |
257
+ | --- | --- |
258
+ | what is open, and what closes it | moving focus in when it opens |
259
+ | the accessible name and description | containing focus while it is modal |
260
+ | the content | dismissing on `Escape` |
261
+ | whether it is modal | returning focus when it closes |
262
+ | where focus starts and returns to | `role="dialog"`, `aria-modal`, `aria-labelledby`, `aria-describedby` |
263
+
264
+ - **Openness is ordinary state.** `openWhen` is an expression over ordinary state — usually an `ephemeral` boolean — not hidden runtime state. Opening and closing therefore go through actions and the mutation log, and are as inspectable as anything else.
265
+ - **A closed dialog is absent, not hidden.** Nothing inside it renders, so nothing inside it is reachable by keyboard or by assistive technology.
266
+ - **Dismissal is not cancellation.** `Escape` invokes `closeActionId` and nothing else. Closing a dialog does not revert, cancel or roll anything back unless that action does — the runtime never infers that it should.
267
+ - **Focus moves in once**, when the dialog is first rendered open, not on every re-render. A full re-render must not take focus back from wherever the person moved it.
268
+ - `title` MUST NOT be empty: a dialog with no accessible name is `INVALID_DIALOG`. `initialFocusId` MUST be inside the dialog and `returnFocusId` MUST NOT be, since it has to exist after the dialog is gone.
269
+ - Nothing here names an element, a class, a position or a stacking order. A renderer that is not a browser implements the same semantics its own way.
270
+ - **Verified in a real browser.** Role, accessible name, `aria-modal`, focus entry, `Tab` and `Shift+Tab` containment, `Escape`, focus return to the correct render instance, a text field inside the dialog, and the absence of a closed dialog are all asserted against Chromium, not only against the in-memory host.
271
+ - **Keyboard containment, not pointer containment.** A modal contains focus and announces itself as modal. It does **not** currently make the rest of the document inert, so a pointer can still reach what is behind it. Declare the rule in a guard, never in the dialog — see [visibility is not authorization](#visibility-is-not-authorization).
272
+ - **Focus return prefers the exact render instance that opened it**, and falls back to another instance of the same control when the action removed that row. A destructive confirmation usually deletes the row its trigger was in; dropping focus to the top of the document instead would lose a keyboard user's place.
273
+
232
274
  ### `conditional`
233
275
 
234
276
  ```ts
@@ -238,6 +280,55 @@ Lifecycle — see [`RUNTIME.md`](RUNTIME.md#action-diagnostics) for the full con
238
280
  Renders one branch. The condition uses the truthiness rules in
239
281
  [`EXPRESSIONS.md`](EXPRESSIONS.md#conversions) — note that `[]` is falsy.
240
282
 
283
+ ## Interaction primitives
284
+
285
+ `dialog` is the first of a class, and the rule that puts it here rather than in a pattern
286
+ library is worth stating once:
287
+
288
+ > Interaction semantics that **cannot be reduced to existing canonical nodes** belong in core.
289
+
290
+ An authoring pattern can only emit nodes that already exist. So anything whose defining
291
+ behaviour is not a node — focus movement, containment, `Escape`, typeahead, active descendant,
292
+ the ARIA relationships that go with them — is unreachable from a pattern at any level of
293
+ cleverness, and has to be canonical semantics with runtime support.
294
+
295
+ | Candidate | Class | State |
296
+ | --- | --- | --- |
297
+ | `dialog` | canonical-semantic | implemented, browser-verified |
298
+ | `combobox` | canonical-semantic | **classified, not implemented** |
299
+ | `menu`, `tabs`, `accordion`, `tooltip`, `popover` | canonical-semantic | unexamined; each needs its own contract |
300
+ | focus trap, keyboard scheme, live announcement | renderer-only | reached through canonical semantics, never declared |
301
+ | page, metric grid, entity list, entity form, action bar | pattern-expandable | `@cynodia/axiom-ui` |
302
+
303
+ **Combobox** was probed deliberately because it looks least like a dialog: no modality, no
304
+ interruption, a control inside a form. It divides at the identical seam. Expressible today,
305
+ with no new node kind: the bound value, the option source (`InputNode.options`), option
306
+ identity and label, filtering, and open state — all ordinary state and expressions. Not
307
+ expressible at all: arrow-key navigation, active descendant, typeahead, `aria-expanded` and the
308
+ listbox relationships. Two primitives that share no shape dividing the same way is what makes
309
+ the split a rule rather than an observation about dialogs.
310
+
311
+ ## Semantic UI authoring
312
+
313
+ Nodes are the model, not the authoring surface an application has to use. `@cynodia/axiom-ui`
314
+ adds five patterns that expand — at build time — into exactly the nodes described on this page.
315
+
316
+ **Which to reach for:**
317
+
318
+ | The requirement is | Use |
319
+ | --- | --- |
320
+ | recurring application UX that expands deterministically into existing semantics | a **pattern** (`page`, `metric-grid`, `entity-list`, `entity-form`, `action-bar`) |
321
+ | interaction behaviour that needs the runtime to do something | a **canonical interaction primitive** (`dialog`) |
322
+ | custom, but already expressible | **ordinary canonical nodes**, composed |
323
+ | genuinely unsupported presentation | the renderer escape (`rendererOverrides.web.className`), and nothing more |
324
+
325
+ A pattern is an authoring abstraction and nothing else: after expansion the application is an
326
+ ordinary Axiom application, and `validateGraph`, `compileToIR`, `AgentAPI` and the runtime know
327
+ nothing about patterns. Ownership defaults to the **declaration**, so editing a generated node
328
+ is drift rather than an edit. The contract travels with the package rather than being restated here: install
329
+ `@cynodia/axiom-ui` and read its `README.md`, `docs/TOOLKIT_AGENT_REFERENCE.md` and
330
+ `docs/PATTERN_CATALOG.json` — the last is addressable as `@cynodia/axiom-ui/catalog`.
331
+
241
332
  ## Containment
242
333
 
243
334
  `uiChildIds(node)` returns a node's children in render order, for every kind:
@@ -1,6 +1,6 @@
1
1
  # Validation
2
2
 
3
- Axiom 0.6.3-alpha.1. Validation is authoring-time structural checking. It is not the same
3
+ Axiom 0.7.0-alpha.2. 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
 
@@ -80,6 +80,14 @@ non-numeric collections, and obviously incompatible assignments.
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
82
  | `MISSING_ACTION_ARGUMENT` | A control invokes an action without supplying a required parameter. |
83
+ | `UNSUPPORTED_UI_NODE_KIND` | A UI node kind the intended renderer cannot draw. |
84
+ | `INVALID_DIALOG` | A `dialog` whose declaration cannot produce a usable dialog. |
85
+ | `RESERVED_FIELD_ID` | An entity declaring a field id reserved for group results. |
86
+ | `INVALID_GROUP_FIELD` | Reading a group field from a non-group, or a non-group field from a group. |
87
+ | `UNKNOWN_EXPRESSION_DEF` | An `expression-ref` naming something that is not an expression definition. |
88
+ | `EXPRESSION_DEF_CYCLE` | An expression definition that reaches itself. |
89
+ | `MISSING_EXPRESSION_ARGUMENT` | An `expression-ref` that omits a declared parameter. |
90
+ | `UNKNOWN_EXPRESSION_ARGUMENT` | An `expression-ref` supplying a parameter the definition does not declare. |
83
91
 
84
92
  ### Initial values
85
93
 
@@ -150,6 +158,14 @@ client commit authoritative state does not compile. Full model:
150
158
  | `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 |
151
159
  | `PRINCIPAL_REFERENCE_ON_CLIENT` | `PRINCIPAL` is read where a client evaluates, or an `authorization` sits on an action no authority executes. | error |
152
160
  | `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 |
161
+ | `INVALID_DIALOG` | A `dialog` with an empty title (no accessible name), an initial focus target outside itself, or a return focus target inside itself. **Also a warning** when a non-modal dialog moves focus on open. | error / warning |
162
+ | `RESERVED_FIELD_ID` | An entity declares `field_group_key` or `field_group_items`. Both are reserved for the records a `group` expression returns; an id meaning one thing in one place and another elsewhere would defeat the reason ids exist. | error |
163
+ | `INVALID_GROUP_FIELD` | A `field` expression reads a group field from a source that is statically not a group, or reads anything other than the two group fields *from* a group. Read `groupItems` first, then the member's own fields. | error |
164
+ | `UNKNOWN_EXPRESSION_DEF` | An `expression-ref` names a node that is not an `ExpressionDef`. | error |
165
+ | `EXPRESSION_DEF_CYCLE` | An expression definition reaches itself, directly or through others. The message names the cycle. | error |
166
+ | `MISSING_EXPRESSION_ARGUMENT` | An `expression-ref` does not supply a parameter the definition declares. | error |
167
+ | `UNKNOWN_EXPRESSION_ARGUMENT` | An `expression-ref` supplies an argument the definition declares no parameter for. | error |
168
+ | `UNSUPPORTED_UI_NODE_KIND` | The graph contains a UI node kind the target renderer does not implement. Raised only when `validateGraph` is given a `renderer`; `compileToIR` supplies the browser renderer's capabilities by default. Without it a graph could validate and then render nothing — the failure would surface as a runtime `UNSUPPORTED_UI_NODE` diagnostic instead of an authoring error. | error |
153
169
  | `INVALID_PRINCIPAL_ENTITY` | `principalEntityId` names something that is not an entity. | error |
154
170
 
155
171
  ### Accessibility
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom",
3
- "version": "0.6.3-alpha.1",
3
+ "version": "0.7.0-alpha.2",
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.3-alpha.1",
36
- "@cynodia/axiom-runtime": "0.6.3-alpha.1",
37
- "@cynodia/axiom-compiler": "0.6.3-alpha.1",
38
- "@cynodia/axiom-agent-api": "0.6.3-alpha.1"
35
+ "@cynodia/axiom-core": "0.7.0-alpha.2",
36
+ "@cynodia/axiom-runtime": "0.7.0-alpha.2",
37
+ "@cynodia/axiom-compiler": "0.7.0-alpha.2",
38
+ "@cynodia/axiom-agent-api": "0.7.0-alpha.2"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b tsconfig.json"