@cynodia/axiom 0.3.1-alpha.1 → 0.4.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 +145 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -90,6 +90,151 @@ console.log(app.getState(COUNT)); // 1
|
|
|
90
90
|
const page = compileToHtml(graph);
|
|
91
91
|
```
|
|
92
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
|
+
```
|
|
193
|
+
|
|
194
|
+
None of this is a callback. `map`, `sort`, `filter`, `find`, `every`, `some`, `flatten`,
|
|
195
|
+
`conditional` and `for-each` are data: they serialize, they validate, and an agent can ask
|
|
196
|
+
what they read and write.
|
|
197
|
+
|
|
198
|
+
**Collection operators are strict about their source.** `null` means a missing or invalid
|
|
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:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
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
|
+
```
|
|
215
|
+
|
|
216
|
+
Use `is-empty` / `non-empty` for collections and strings, and `coalesce` to fall back on
|
|
217
|
+
absence — which means falling back *to* an empty collection now works.
|
|
218
|
+
|
|
219
|
+
## Diagnostics
|
|
220
|
+
|
|
221
|
+
Failures are structured. Match on `code` rather than reading the message:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { RUNTIME_DIAGNOSTIC_CODES } from '@cynodia/axiom';
|
|
225
|
+
|
|
226
|
+
const result = app.invokeAction(CONFIRM_ORDER);
|
|
227
|
+
if (!result.ok) {
|
|
228
|
+
const stock = result.diagnostics.find(
|
|
229
|
+
(diagnostic) => diagnostic.code === RUNTIME_DIAGNOSTIC_CODES.PRECONDITION_FAILED,
|
|
230
|
+
);
|
|
231
|
+
console.log(stock?.details); // { preconditionIndex: 2, failureMode: 'insufficient-stock' }
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`result.diagnostics` belongs to that invocation. `app.diagnostics()` keeps the history and
|
|
236
|
+
`app.clearDiagnostics()` empties it.
|
|
237
|
+
|
|
93
238
|
## What is in the box
|
|
94
239
|
|
|
95
240
|
`@cynodia/axiom` re-exports the framework packages, which can also be installed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1-alpha.1",
|
|
4
4
|
"description": "AI-native semantic web application framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cynodia/axiom-core": "0.
|
|
35
|
-
"@cynodia/axiom-runtime": "0.
|
|
36
|
-
"@cynodia/axiom-compiler": "0.
|
|
37
|
-
"@cynodia/axiom-agent-api": "0.
|
|
34
|
+
"@cynodia/axiom-core": "0.4.1-alpha.1",
|
|
35
|
+
"@cynodia/axiom-runtime": "0.4.1-alpha.1",
|
|
36
|
+
"@cynodia/axiom-compiler": "0.4.1-alpha.1",
|
|
37
|
+
"@cynodia/axiom-agent-api": "0.4.1-alpha.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -b tsconfig.json"
|