@cynodia/axiom 0.4.1-alpha.1 → 0.5.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 +92 -195
- package/docs/ACTIONS_TRANSACTIONS.md +237 -0
- package/docs/AGENT_API.md +232 -0
- package/docs/AGENT_REFERENCE.md +479 -0
- package/docs/ANTI_PATTERNS.md +345 -0
- package/docs/CONSTRAINTS.md +201 -0
- package/docs/EXPRESSIONS.md +270 -0
- package/docs/GRAPH_MODEL.md +201 -0
- package/docs/LOCATIONS.md +168 -0
- package/docs/PRESENTATION.md +523 -0
- package/docs/RUNTIME.md +258 -0
- package/docs/SEMANTIC_CONTRACT.md +244 -0
- package/docs/STATE.md +174 -0
- package/docs/UI.md +329 -0
- package/docs/VALIDATION.md +215 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# Axiom
|
|
2
2
|
|
|
3
|
-
AI-native semantic
|
|
3
|
+
AI-native semantic application framework.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Axiom represents application behavior, state, UI structure and presentation as structured
|
|
6
|
+
semantic data executed by generic runtimes. An application is a typed graph, not source
|
|
7
|
+
files: the JavaScript and HTML that reach the browser are output, and are never edited.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
that graph and a generic runtime executes it in an unmodified browser — the JavaScript
|
|
11
|
-
and HTML that reach the browser are output, never something you maintain by hand.
|
|
9
|
+
**Status: experimental / alpha (0.5.2-alpha.x).** The API may change between alpha
|
|
10
|
+
releases. The documentation in `docs/` describes this exact version.
|
|
12
11
|
|
|
13
12
|
## Installation
|
|
14
13
|
|
|
@@ -16,236 +15,134 @@ and HTML that reach the browser are output, never something you maintain by hand
|
|
|
16
15
|
npm install @cynodia/axiom@alpha
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Canonical mental model
|
|
19
|
+
|
|
20
|
+
| Concept | Is |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `ApplicationGraph` | The authoritative representation of an application. |
|
|
23
|
+
| `Expression` | What value is computed. Pure; never writes. |
|
|
24
|
+
| `Location` | Where a writable value lives. An address, not a value. |
|
|
25
|
+
| `StateDef` | A stored or derived application value. |
|
|
26
|
+
| `ActionDef` | A transactional semantic operation. |
|
|
27
|
+
| `ConstraintDef` | An invariant over proposed state. |
|
|
28
|
+
| `TransitionConstraintDef` | An invariant over previous committed state → proposed state. |
|
|
29
|
+
| UI nodes | Semantic interaction structure. |
|
|
30
|
+
| `Presentation` | Semantic UX intent. Roles and tokens, never CSS. |
|
|
31
|
+
| `Theme` | Translation of presentation intent into visual design. |
|
|
32
|
+
| Renderer | Platform-specific materialization. Not part of the graph. |
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
ApplicationGraph → validateGraph → compileToIR → runtime (+ theme → renderer) → application
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Load-bearing invariants
|
|
39
|
+
|
|
40
|
+
Know these before authoring an application. Each is stated in full in
|
|
41
|
+
`docs/SEMANTIC_CONTRACT.md`.
|
|
42
|
+
|
|
43
|
+
1. **Entity runtime values are keyed by `FieldId`**, not by field name.
|
|
44
|
+
2. **Expressions produce values; Locations name writable addresses.** Stored state is deeply frozen.
|
|
45
|
+
3. **Derived state is read-only.**
|
|
46
|
+
4. **An action is a transaction.** Every mutation commits, or every mutation rolls back.
|
|
47
|
+
5. **Operations see provisional state**, and `for-each` iteration N sees the writes of iterations `< N`.
|
|
48
|
+
6. **Constraints are evaluated against proposed state**, per instance, wherever it is stored.
|
|
49
|
+
7. **Transition constraints compare the instance at transaction entry with the instance proposed.** A newly inserted instance has no previous state and is not evaluated.
|
|
50
|
+
8. **Input writes are governed** by the same constraints, through the same engine and transaction.
|
|
51
|
+
9. **`hydrateState` bypasses semantic enforcement.** It is administrative.
|
|
52
|
+
10. **Presentation never authorizes behavior.** `hidden` ≠ forbidden.
|
|
53
|
+
11. **`null` and `[]` are distinct.** `null` fails a collection operator; `[]` does not. A collection is truthy only when non-empty.
|
|
54
|
+
12. **`required(x)` asks only whether a value exists.** `required([])` is `true`.
|
|
55
|
+
13. **A theme changes presentation only.**
|
|
56
|
+
|
|
57
|
+
## Minimal application
|
|
20
58
|
|
|
21
59
|
```ts
|
|
22
60
|
import {
|
|
23
|
-
ApplicationGraph,
|
|
24
|
-
|
|
25
|
-
call,
|
|
26
|
-
compileToHtml,
|
|
27
|
-
compileToIR,
|
|
28
|
-
createAxiomRuntime,
|
|
29
|
-
createMemoryHost,
|
|
30
|
-
literal,
|
|
31
|
-
nodeId,
|
|
32
|
-
primitiveType,
|
|
33
|
-
ref,
|
|
34
|
-
stateLocation,
|
|
35
|
-
synchronizeEdges,
|
|
36
|
-
validateGraph,
|
|
61
|
+
ApplicationGraph, binary, compileToIR, createAxiomRuntime, createMemoryHost,
|
|
62
|
+
literal, nodeId, primitiveType, ref, stateLocation, validateGraph,
|
|
37
63
|
} from '@cynodia/axiom';
|
|
38
64
|
import type { ActionDef, ButtonNode, RouteDef, StateDef, TextNode, ViewNode } from '@cynodia/axiom';
|
|
39
65
|
|
|
40
66
|
const COUNT = nodeId('state_count');
|
|
41
67
|
const INCREMENT = nodeId('action_increment');
|
|
42
|
-
const DISPLAY = nodeId('ui_display');
|
|
43
|
-
const BUTTON = nodeId('ui_increment');
|
|
44
68
|
const VIEW = nodeId('ui_view');
|
|
45
69
|
|
|
46
70
|
const graph = new ApplicationGraph('counter', 'Counter');
|
|
47
71
|
|
|
48
72
|
graph.addNode<StateDef>({
|
|
49
|
-
id: COUNT,
|
|
50
|
-
|
|
51
|
-
name: 'count',
|
|
52
|
-
valueType: primitiveType('number'),
|
|
53
|
-
initialValue: 0,
|
|
73
|
+
id: COUNT, kind: 'state', name: 'count',
|
|
74
|
+
valueType: primitiveType('number'), initialValue: 0,
|
|
54
75
|
});
|
|
55
76
|
|
|
56
|
-
//
|
|
77
|
+
// A value is an Expression. The position written to is a Location.
|
|
57
78
|
graph.addNode<ActionDef>({
|
|
58
|
-
id: INCREMENT,
|
|
59
|
-
kind: 'action',
|
|
60
|
-
name: 'increment',
|
|
79
|
+
id: INCREMENT, kind: 'action', name: 'increment',
|
|
61
80
|
operations: [
|
|
62
81
|
{ kind: 'set', target: stateLocation(COUNT), value: binary('add', ref(COUNT), literal(1)) },
|
|
63
82
|
],
|
|
64
83
|
});
|
|
65
84
|
|
|
66
85
|
graph.addNode<TextNode>({
|
|
67
|
-
id:
|
|
68
|
-
kind: '
|
|
69
|
-
|
|
86
|
+
id: nodeId('ui_display'), kind: 'text', value: ref(COUNT),
|
|
87
|
+
presentation: { textRole: 'title', format: { kind: 'number' } },
|
|
88
|
+
});
|
|
89
|
+
graph.addNode<ButtonNode>({
|
|
90
|
+
id: nodeId('ui_increment'), kind: 'button', label: 'Add one', actionId: INCREMENT,
|
|
91
|
+
presentation: { uxRole: 'primary-action', icon: 'add' },
|
|
92
|
+
});
|
|
93
|
+
graph.addNode<ViewNode>({
|
|
94
|
+
id: VIEW, kind: 'view', name: 'Counter',
|
|
95
|
+
children: [nodeId('ui_display'), nodeId('ui_increment')],
|
|
70
96
|
});
|
|
71
|
-
graph.addNode<ButtonNode>({ id: BUTTON, kind: 'button', label: 'Add one', actionId: INCREMENT });
|
|
72
|
-
graph.addNode<ViewNode>({ id: VIEW, kind: 'view', children: [DISPLAY, BUTTON] });
|
|
73
97
|
graph.addNode<RouteDef>({ id: nodeId('route_root'), kind: 'route', path: '/', viewId: VIEW });
|
|
74
98
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const result = validateGraph(graph);
|
|
78
|
-
if (!result.valid) {
|
|
79
|
-
throw new Error(result.errors.map((problem) => problem.message).join('\n'));
|
|
99
|
+
if (!validateGraph(graph).valid) {
|
|
100
|
+
throw new Error('invalid graph');
|
|
80
101
|
}
|
|
81
102
|
|
|
82
|
-
// Run it headlessly...
|
|
83
103
|
const host = createMemoryHost({ path: '/' });
|
|
84
104
|
const app = createAxiomRuntime({ ir: compileToIR(graph), rootElement: host.root, host });
|
|
85
105
|
app.start();
|
|
86
106
|
app.invokeAction(INCREMENT);
|
|
87
107
|
console.log(app.getState(COUNT)); // 1
|
|
88
|
-
|
|
89
|
-
// ...or emit a self-contained page.
|
|
90
|
-
const page = compileToHtml(graph);
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Actions are transactions
|
|
94
|
-
|
|
95
|
-
This is the guarantee the framework is built around:
|
|
96
|
-
|
|
97
|
-
> An Axiom action executes as a semantic transaction. Its mutations are applied
|
|
98
|
-
> provisionally, the relevant constraints are evaluated against the resulting **proposed
|
|
99
|
-
> state**, and either the complete action commits or every one of its state mutations is
|
|
100
|
-
> rolled back.
|
|
101
|
-
|
|
102
|
-
That includes iteration. If an action reduces stock for twenty order lines and the
|
|
103
|
-
seventeenth breaks an invariant, the first sixteen do not survive — you never write
|
|
104
|
-
rollback logic yourself, and `runtime.getMutationLog()` shows every attempted write with
|
|
105
|
-
its `outcome` of `committed` or `rolled-back`.
|
|
106
|
-
|
|
107
|
-
Within one action, each `for-each` iteration reads the state the previous iterations
|
|
108
|
-
proposed. Two lines for the same product debit it twice:
|
|
109
|
-
|
|
110
|
-
```text
|
|
111
|
-
stock 5 → line A (−3) → 2 → line A (−3) → −1 → stock >= 0 fails → all rolled back
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
That is a guarantee, not an implementation detail: an aggregate rule can be expressed as a
|
|
115
|
-
simple per-record invariant.
|
|
116
|
-
|
|
117
|
-
## What is enforced, and where
|
|
118
|
-
|
|
119
|
-
Two kinds of rule, and they answer different questions:
|
|
120
|
-
|
|
121
|
-
| | Question | Applies to |
|
|
122
|
-
| --- | --- | --- |
|
|
123
|
-
| **Constraint** | Is this state allowed? | Every instance of an entity, wherever it is stored — including instances nested inside other entities. |
|
|
124
|
-
| **Transition constraint** | Is this *change* allowed? | The instance as it was when the transaction began, compared with the instance the transaction proposes. |
|
|
125
|
-
|
|
126
|
-
A transition constraint is what makes a rule like "a confirmed order never changes" hold
|
|
127
|
-
no matter which path attempts the write:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
graph.addNode<TransitionConstraintDef>({
|
|
131
|
-
id: ORDER_SEALED,
|
|
132
|
-
kind: 'transition-constraint',
|
|
133
|
-
entityId: ORDER,
|
|
134
|
-
previousScopeId: PREVIOUS,
|
|
135
|
-
proposedScopeId: PROPOSED,
|
|
136
|
-
message: 'A confirmed order cannot be changed.',
|
|
137
|
-
expression: binary(
|
|
138
|
-
'or',
|
|
139
|
-
binary('neq', field(ref(PREVIOUS), STATUS), literal('confirmed')),
|
|
140
|
-
binary('eq', ref(PROPOSED), ref(PREVIOUS)),
|
|
141
|
-
),
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Governed paths** — actions, `for-each` iterations, and input bindings — all evaluate
|
|
146
|
-
entity constraints *and* transition constraints against the proposed state, and roll the
|
|
147
|
-
whole transaction back if either refuses. You do not have to remember not to bind an input
|
|
148
|
-
to a protected location: binding it and typing into it is simply refused, with a
|
|
149
|
-
`TRANSITION_CONSTRAINT_VIOLATION` naming the rule, the entity, and the previous and
|
|
150
|
-
proposed values.
|
|
151
|
-
|
|
152
|
-
**`hydrateState` is not governed.** It replaces a state value outright for hosts, tests and
|
|
153
|
-
seeding, and evaluates nothing. It is deliberately not named like a normal write.
|
|
154
|
-
|
|
155
|
-
**"Previous" means transaction entry** — committed state as it was before the outermost
|
|
156
|
-
transaction began. Not the previous operation, and not the previous iteration.
|
|
157
|
-
|
|
158
|
-
## Collections
|
|
159
|
-
|
|
160
|
-
Values are described by expressions, writable positions by **locations**. Collections add
|
|
161
|
-
projection, aggregation and ordering to the first, and iteration to the second.
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
import { binary, field, filter, forEach, map, ref, sum } from '@cynodia/axiom';
|
|
165
|
-
|
|
166
|
-
// An order total: project each line to its amount, then sum the projection.
|
|
167
|
-
const orderTotal = sum(
|
|
168
|
-
map(ref(LINES), LINE, binary('multiply', field(ref(LINE), QUANTITY), field(ref(LINE), PRICE))),
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// How much of one product this order asks for, across every line that mentions it.
|
|
172
|
-
const requested = sum(
|
|
173
|
-
map(
|
|
174
|
-
filter(ref(LINES), LINE, binary('eq', field(ref(LINE), PRODUCT), field(ref(P), PRODUCT_ID))),
|
|
175
|
-
LINE,
|
|
176
|
-
field(ref(LINE), QUANTITY),
|
|
177
|
-
),
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
// Reduce the stock of every product the order mentions — one mutation per line, one
|
|
181
|
-
// transaction for the action.
|
|
182
|
-
const confirm = forEach(ref(LINES), LINE, [
|
|
183
|
-
{
|
|
184
|
-
kind: 'set',
|
|
185
|
-
target: fieldLocation(
|
|
186
|
-
itemLocation(stateLocation(PRODUCTS), identitySelector(PRODUCT_ID, field(ref(LINE), PRODUCT))),
|
|
187
|
-
STOCK,
|
|
188
|
-
),
|
|
189
|
-
value: binary('subtract', currentStock, field(ref(LINE), QUANTITY)),
|
|
190
|
-
},
|
|
191
|
-
]);
|
|
192
108
|
```
|
|
193
109
|
|
|
194
|
-
|
|
195
|
-
`conditional` and `for-each` are data: they serialize, they validate, and an agent can ask
|
|
196
|
-
what they read and write.
|
|
110
|
+
`compileToHtml(graph)` emits the same application as one self-contained page.
|
|
197
111
|
|
|
198
|
-
|
|
199
|
-
collection and fails the evaluation; `[]` means an empty collection and works normally
|
|
200
|
-
(`sum([])` is `0`, `count([])` is `0`, `every([])` is `true`). Nothing returns a
|
|
201
|
-
plausible-looking value while reporting a failure. Where a collection may legitimately be
|
|
202
|
-
absent, say so:
|
|
112
|
+
## Documentation
|
|
203
113
|
|
|
204
|
-
|
|
205
|
-
coalesce(field(ref(CURRENT_ORDER), LINES), literal([]))
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**Presence is not emptiness.** `required(value)` asks only whether a value exists:
|
|
209
|
-
|
|
210
|
-
```text
|
|
211
|
-
required(null) → false required([]) → true
|
|
212
|
-
required(0) → true required('') → true
|
|
213
|
-
required(false)→ true
|
|
214
|
-
```
|
|
114
|
+
The complete operational contract ships with this package, in `docs/`.
|
|
215
115
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
```
|
|
116
|
+
| Need to understand | Read |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| Compressed reference for authoring or modifying an app | `docs/AGENT_REFERENCE.md` |
|
|
119
|
+
| Exact runtime guarantees | `docs/SEMANTIC_CONTRACT.md` |
|
|
120
|
+
| Graph, ids, types, entity value representation | `docs/GRAPH_MODEL.md` |
|
|
121
|
+
| Every expression kind, builtin and scope rule | `docs/EXPRESSIONS.md` |
|
|
122
|
+
| Addressing writable positions | `docs/LOCATIONS.md` |
|
|
123
|
+
| Stored, derived, draft and ephemeral state | `docs/STATE.md` |
|
|
124
|
+
| Actions, operations, transactions, iteration | `docs/ACTIONS_TRANSACTIONS.md` |
|
|
125
|
+
| Constraints and transition constraints | `docs/CONSTRAINTS.md` |
|
|
126
|
+
| Semantic UI nodes and bindings | `docs/UI.md` |
|
|
127
|
+
| Presentation, UX intent, themes, formatting | `docs/PRESENTATION.md` |
|
|
128
|
+
| Runtime API and diagnostic codes | `docs/RUNTIME.md` |
|
|
129
|
+
| Machine queries and graph transformations | `docs/AGENT_API.md` |
|
|
130
|
+
| Validation codes | `docs/VALIDATION.md` |
|
|
131
|
+
| Mistakes that compile but are wrong | `docs/ANTI_PATTERNS.md` |
|
|
234
132
|
|
|
235
|
-
`
|
|
236
|
-
|
|
133
|
+
Start with `docs/AGENT_REFERENCE.md`. It plus the `.d.ts` declarations are intended to be
|
|
134
|
+
sufficient on their own.
|
|
237
135
|
|
|
238
136
|
## What is in the box
|
|
239
137
|
|
|
240
|
-
|
|
241
|
-
individually:
|
|
138
|
+
This package re-exports four, which can also be installed individually:
|
|
242
139
|
|
|
243
|
-
| Package |
|
|
244
|
-
|
|
|
245
|
-
| `@cynodia/axiom-core` |
|
|
246
|
-
| `@cynodia/axiom-compiler` | Normalization into an IR,
|
|
247
|
-
| `@cynodia/axiom-runtime` |
|
|
248
|
-
| `@cynodia/axiom-agent-api` | Semantic queries, mutation impact, transactional transformations. |
|
|
140
|
+
| Package | Responsibility |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `@cynodia/axiom-core` | Graph, semantic types, expressions, locations, presentation, themes, validation. |
|
|
143
|
+
| `@cynodia/axiom-compiler` | Normalization into an IR, theme stylesheet, page emission. |
|
|
144
|
+
| `@cynodia/axiom-runtime` | State store, evaluation, mutation engine, constraint checking, renderer, routing. |
|
|
145
|
+
| `@cynodia/axiom-agent-api` | Semantic and presentation queries, mutation impact, transactional transformations. |
|
|
249
146
|
|
|
250
147
|
## License
|
|
251
148
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Actions and transactions
|
|
2
|
+
|
|
3
|
+
Axiom 0.5.2-alpha.1. An action is behavior expressed as data, executed as a transaction.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
{
|
|
7
|
+
id, kind: 'action', name?,
|
|
8
|
+
parameters?: [{ id, name?, valueType?, required? }],
|
|
9
|
+
guards?: [{ condition: Expression, failureMode?: { code, message? } }],
|
|
10
|
+
preconditions?: Expression[], // older, positional form
|
|
11
|
+
failureModes?: [{ code, message? }], // aligned with preconditions by index
|
|
12
|
+
operations: Operation[],
|
|
13
|
+
postconditions?: Expression[],
|
|
14
|
+
destructive?: boolean,
|
|
15
|
+
requiresConfirmation?: boolean,
|
|
16
|
+
confirmationMessage?: string,
|
|
17
|
+
confirmation?: { title?, description?, confirmLabel?, cancelLabel?, severity? },
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Guards and failure modes
|
|
22
|
+
|
|
23
|
+
Prefer `guards`: it pairs each condition with the failure it reports, so the two cannot
|
|
24
|
+
drift apart.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
guards: [
|
|
28
|
+
{ condition: isDraft, failureMode: { code: 'not-draft', message: 'This order is confirmed.' } },
|
|
29
|
+
{ condition: hasLines, failureMode: { code: 'empty-order', message: 'Add a line first.' } },
|
|
30
|
+
]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The older parallel arrays align **by position**: `failureModes[2]` reports
|
|
34
|
+
`preconditions[2]`. The compiler normalizes `guards` into that form, so the IR always
|
|
35
|
+
carries aligned arrays and a refusal names the condition that actually failed:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
result.diagnostics[0].details // { preconditionIndex: 2, failureMode: 'insufficient-stock' }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`actionGuards(action)` returns the conditions however they were written.
|
|
42
|
+
|
|
43
|
+
## Lifecycle
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
resolve the action → ACTION_NOT_FOUND, stop
|
|
47
|
+
bind parameters → PARAMETER_MISSING for an absent required parameter, stop
|
|
48
|
+
evaluate preconditions in order → PRECONDITION_FAILED { preconditionIndex, failureMode }, stop
|
|
49
|
+
ask for confirmation if required → declined: stop
|
|
50
|
+
── nothing above opens a transaction; nothing has been mutated ──
|
|
51
|
+
BEGIN TRANSACTION (entry state is captured here)
|
|
52
|
+
execute operations sequentially against provisional state
|
|
53
|
+
evaluate entity constraints + schema conformance over proposed state
|
|
54
|
+
evaluate transition constraints over entry state → proposed state
|
|
55
|
+
evaluate postconditions
|
|
56
|
+
any error-severity failure?
|
|
57
|
+
no → COMMIT
|
|
58
|
+
yes → ROLL BACK every mutation of the transaction
|
|
59
|
+
re-render
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`invokeAction(id, args?)` returns `{ ok, diagnostics }` for **that invocation**. No diffing
|
|
63
|
+
of global history is needed.
|
|
64
|
+
|
|
65
|
+
All four failure sources are evaluated; the action does not stop at the first.
|
|
66
|
+
|
|
67
|
+
## Operations
|
|
68
|
+
|
|
69
|
+
Seven kinds, enumerated by `OPERATION_KINDS`. `set`, `insert` and `remove` are the
|
|
70
|
+
mutations; each addresses a [`Location`](LOCATIONS.md).
|
|
71
|
+
|
|
72
|
+
### `set`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
{ kind: 'set', target: Location, value: Expression }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Writes the value. A missing field along the path is created; a missing collection **item**
|
|
79
|
+
is `LOCATION_RESOLUTION_FAILED`.
|
|
80
|
+
|
|
81
|
+
### `insert`
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
{ kind: 'insert', target: Location, value: Expression, position?: 'start' | 'end' }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Appends by default. The target must address a collection; a non-array current value is
|
|
88
|
+
treated as `[]`. The constructed value is deep-cloned before storing.
|
|
89
|
+
|
|
90
|
+
### `remove`
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
{ kind: 'remove', target: CollectionItemLocation }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**A selector matching nothing is a no-op** — no mutation, no log entry, no diagnostic.
|
|
97
|
+
|
|
98
|
+
### `for-each`
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
forEach(collection: Expression, scopeId: NodeId, operations: MutationOperation[])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- The collection is evaluated **once**, before any member is mutated.
|
|
105
|
+
- Iteration N observes the provisional writes of iterations `< N`.
|
|
106
|
+
- It opens **no transaction of its own**: the mutations belong to the action's transaction.
|
|
107
|
+
- A failure in iteration N rolls back iterations `0..N-1` and the whole action.
|
|
108
|
+
- Nested operations MUST be mutations only. Nested iteration, navigation and invocation are not supported (`UNSUPPORTED_OPERATION`).
|
|
109
|
+
- `ref(scopeId)` is the current member, and a nested location may use it to address the canonical record that member points at.
|
|
110
|
+
|
|
111
|
+
Two members touching the same record debit it twice, and the invariant catches the total:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
stock 5 → member A (−3) → 2 → member B (−3) → −1 → `stock >= 0` fails → all rolled back
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
That is public contract, not incidental behavior. It is what lets an aggregate rule be
|
|
118
|
+
written as a per-record invariant.
|
|
119
|
+
|
|
120
|
+
### `invoke`
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
{ kind: 'invoke', actionId: NodeId, arguments?: Record<string, Expression> }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Runs another action. The nested transaction **joins the outermost** one and shares its
|
|
127
|
+
fate: if the nested action fails, everything rolls back and the enclosing action fails too.
|
|
128
|
+
Arguments are keyed by the target action's parameter ids.
|
|
129
|
+
|
|
130
|
+
### `navigate`
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
{ kind: 'navigate', routeId?: NodeId, path?: string, parameters?: Record<string, Expression> }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Changes the route. Parameters are keyed by route parameter id and are rendered as text. An
|
|
137
|
+
unresolvable `routeId` reports `ROUTE_NOT_FOUND`. Navigation is not a state mutation and is
|
|
138
|
+
not rolled back.
|
|
139
|
+
|
|
140
|
+
### `native`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
{
|
|
144
|
+
kind: 'native',
|
|
145
|
+
implementationId: string,
|
|
146
|
+
inputs?: Record<string, Expression>,
|
|
147
|
+
resultTarget?: Location,
|
|
148
|
+
declaredEffects?: NativeEffect[],
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The controlled boundary for behavior the operation vocabulary cannot express.
|
|
153
|
+
|
|
154
|
+
- The implementation is registered with the runtime (`registerNativeOperation` or `nativeOperations`), never embedded in the graph. Missing → `NATIVE_OPERATION_MISSING`.
|
|
155
|
+
- Inputs are **cloned copies**; native code can never reach into managed state.
|
|
156
|
+
- Native code does not write state. It returns a value, which the mutation engine writes to `resultTarget` with source `native`.
|
|
157
|
+
- Without `declaredEffects`, dependency analysis reports `analysisComplete: false` and names the gap.
|
|
158
|
+
|
|
159
|
+
**Use it only where no semantic primitive exists.** A native operation is opaque to every
|
|
160
|
+
analysis Axiom offers.
|
|
161
|
+
|
|
162
|
+
## Postconditions
|
|
163
|
+
|
|
164
|
+
Evaluated after the operations, inside the transaction, against proposed state. A failure
|
|
165
|
+
is `POSTCONDITION_FAILED` and rolls the action back. Use them for a property of the whole
|
|
166
|
+
action; use constraints for a property of the state.
|
|
167
|
+
|
|
168
|
+
## Confirmation
|
|
169
|
+
|
|
170
|
+
`requiresConfirmation: true` asks the host before the transaction opens. `confirmation`
|
|
171
|
+
describes what is asked, without constructing any dialog markup:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
requiresConfirmation: true,
|
|
175
|
+
confirmation: {
|
|
176
|
+
title: 'Confirm this order?',
|
|
177
|
+
description: 'Confirming reduces stock for every line and seals the order.',
|
|
178
|
+
confirmLabel: 'Confirm order',
|
|
179
|
+
severity: 'warning',
|
|
180
|
+
},
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A host that implements `confirmRequest` receives the structured request; a host that only
|
|
184
|
+
has `confirm(message)` receives a composed sentence. `confirmationMessage` overrides the
|
|
185
|
+
composed message.
|
|
186
|
+
|
|
187
|
+
A declined confirmation ends the invocation with `ok: false` and no diagnostics.
|
|
188
|
+
|
|
189
|
+
## Destructive intent
|
|
190
|
+
|
|
191
|
+
`destructive: true` declares that the action destroys something. It is what
|
|
192
|
+
`AgentAPI.findDestructiveActions()` reports, and presentation is inferred from it — a bound
|
|
193
|
+
button is presented as destructive without the graph saying so twice.
|
|
194
|
+
|
|
195
|
+
An action containing a `remove` operation is *treated* as destructive by inference, and
|
|
196
|
+
validation warns (`DESTRUCTIVE_ACTION_UNMARKED`) if it does not declare the flag, because
|
|
197
|
+
the declaration is what an agent reads.
|
|
198
|
+
|
|
199
|
+
`destructive` is metadata. It does not restrict anything — see
|
|
200
|
+
[Presentation never authorizes](PRESENTATION.md#presentation-never-authorizes-behavior).
|
|
201
|
+
|
|
202
|
+
## The mutation log
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
app.getMutationLog();
|
|
206
|
+
// [{ transactionId, source, sourceNodeId, operation, path, description,
|
|
207
|
+
// oldValue?, newValue?, outcome }]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- `source` is `action` | `ui` | `system` | `native`.
|
|
211
|
+
- `description` renders the resolved path: `state_products → [product-1] → field_product_stock`.
|
|
212
|
+
- `outcome` is set when the surrounding transaction settles. Rejected attempts remain in the log as `rolled-back`.
|
|
213
|
+
- Only the outermost transaction decides an outcome, so the log never suggests that early iterations of a failed loop committed.
|
|
214
|
+
- `oldValue` / `newValue` are recorded unless `recordMutationValues: false`.
|
|
215
|
+
|
|
216
|
+
## Invalid usage
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// WRONG — nested iteration is not supported and is rejected by validateGraph.
|
|
220
|
+
forEach(a, S1, [forEach(b, S2, [...])])
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// WRONG — enforcing a rule by not offering the control.
|
|
225
|
+
{ kind: 'button', visibleWhen: isDraft, actionId: ACTION_EDIT }
|
|
226
|
+
|
|
227
|
+
// RIGHT — the control may be hidden for clarity, but the rule lives in the graph.
|
|
228
|
+
{ kind: 'transition-constraint', entityId: ENTITY_ORDER, expression: … }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// WRONG — a native operation for something the vocabulary expresses.
|
|
233
|
+
{ kind: 'native', implementationId: 'app.computeTotal', resultTarget: … }
|
|
234
|
+
|
|
235
|
+
// RIGHT
|
|
236
|
+
{ id: STATE_TOTAL, kind: 'state', derivation: sum(map(...)) }
|
|
237
|
+
```
|