@cynodia/axiom 0.4.0-alpha.1 → 0.5.0-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 +272 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -104,6 +104,57 @@ seventeenth breaks an invariant, the first sixteen do not survive — you never
|
|
|
104
104
|
rollback logic yourself, and `runtime.getMutationLog()` shows every attempted write with
|
|
105
105
|
its `outcome` of `committed` or `rolled-back`.
|
|
106
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
|
+
|
|
107
158
|
## Collections
|
|
108
159
|
|
|
109
160
|
Values are described by expressions, writable positions by **locations**. Collections add
|
|
@@ -140,8 +191,227 @@ const confirm = forEach(ref(LINES), LINE, [
|
|
|
140
191
|
]);
|
|
141
192
|
```
|
|
142
193
|
|
|
143
|
-
None of this is a callback. `map`, `sort`, `filter`, `find`
|
|
144
|
-
serialize, they validate, and an agent can ask
|
|
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
|
+
## Presentation and UX intent
|
|
220
|
+
|
|
221
|
+
Four things are kept apart on purpose:
|
|
222
|
+
|
|
223
|
+
| | Describes | Lives in |
|
|
224
|
+
| --- | --- | --- |
|
|
225
|
+
| **UI semantics** | What exists — views, containers, text, repeats, forms, inputs, buttons. | The graph |
|
|
226
|
+
| **Presentation semantics** | What it *means* and how it is organized — roles, layout, spacing, sizing, device classes. | The graph |
|
|
227
|
+
| **Theme** | What those meanings look like — colours, type scale, spacing values, radii, breakpoints. | The graph's `theme` |
|
|
228
|
+
| **Renderer** | How any of it reaches a screen — CSS, classes, media queries, DOM. | The framework |
|
|
229
|
+
|
|
230
|
+
An application says `role: 'destructive'`, not `color: '#c62a20'`. There is no inline style
|
|
231
|
+
model, no CSS property model, and no way to store a function anywhere in the graph.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
graph.addNode<ContainerNode>({
|
|
235
|
+
id: ACTIONS,
|
|
236
|
+
kind: 'container',
|
|
237
|
+
children: [CANCEL, SAVE],
|
|
238
|
+
presentation: {
|
|
239
|
+
uxRole: 'action-group',
|
|
240
|
+
responsive: { compact: { layout: 'vertical' } },
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`uxRole: 'action-group'` is high-information: it already implies a horizontal, wrapping,
|
|
246
|
+
centre-aligned, end-justified group. You annotate where intent differs from the default,
|
|
247
|
+
not on every node.
|
|
248
|
+
|
|
249
|
+
### Resolution order
|
|
250
|
+
|
|
251
|
+
Every resolved property is decided by exactly one layer, lowest first:
|
|
252
|
+
|
|
253
|
+
```text
|
|
254
|
+
renderer defaults → theme → inherited → semantic inference → node → responsive
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`resolvePresentation` records which layer won, so precedence is inspectable rather than
|
|
258
|
+
folklore:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const agent = new AgentAPI(graph);
|
|
262
|
+
agent.resolvePresentation(DELETE_BUTTON);
|
|
263
|
+
// { role: 'destructive', uxRole: 'destructive-action', density: 'comfortable', … ,
|
|
264
|
+
// origins: { role: 'inferred', density: 'theme', 'layout.kind': 'inferred' } }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Inheritance is deliberately narrow.** Only `density` cascades from a parent
|
|
268
|
+
(`INHERITED_PROPERTIES`). Nothing else does — a container with `emphasis: 'strong'` does
|
|
269
|
+
not make its whole subtree bold.
|
|
270
|
+
|
|
271
|
+
**Semantic inference** means presentation is derived from what the application already
|
|
272
|
+
says, rather than declared twice:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
graph.addNode<ActionDef>({ id: DELETE, kind: 'action', destructive: true, operations: [...] });
|
|
276
|
+
graph.addNode<ButtonNode>({ id: BUTTON, kind: 'button', label: 'Delete', actionId: DELETE });
|
|
277
|
+
// The button is presented as destructive. It declares no role at all.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
A button that submits its enclosing form becomes the primary action the same way. An
|
|
281
|
+
explicit `presentation.role` always wins; a contradiction — a destructive action presented
|
|
282
|
+
as a success — is reported as `DESTRUCTIVE_ACTION_PRESENTED_AS_SUCCESS`.
|
|
283
|
+
|
|
284
|
+
### Responsive behaviour without breakpoints
|
|
285
|
+
|
|
286
|
+
Presentation names device classes — `compact`, `regular`, `wide` — never pixels. The
|
|
287
|
+
renderer owns the breakpoints (`theme.responsive`), and provides sensible behaviour with no
|
|
288
|
+
configuration at all: rows wrap, fixed grids give up columns, bounded widths stop being
|
|
289
|
+
bounded, and controls go full width on a narrow screen.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
presentation: {
|
|
293
|
+
layout: { kind: 'grid', gap: 'medium', columns: { mode: 'adaptive', minimum: 'medium' } },
|
|
294
|
+
responsive: { compact: { padding: 'small' } },
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Vocabulary
|
|
299
|
+
|
|
300
|
+
Everything below is a closed set. A token outside it is a validation **error**, because a
|
|
301
|
+
renderer cannot act on a value it does not know.
|
|
302
|
+
|
|
303
|
+
| | Values |
|
|
304
|
+
| --- | --- |
|
|
305
|
+
| `role` | `primary` `secondary` `tertiary` `destructive` `success` `warning` `informational` `muted` |
|
|
306
|
+
| `uxRole` | `primary-action` `secondary-action` `destructive-action` `navigation-action` `form-section` `action-group` `navigation-group` `empty-state` `error-state` `warning-state` `success-state` `informational-state` `toolbar` `sidebar` `content-region` `header-region` `footer-region` |
|
|
307
|
+
| `emphasis` | `subtle` `normal` `strong` |
|
|
308
|
+
| `density` | `compact` `comfortable` `spacious` |
|
|
309
|
+
| `textRole` | `body` `caption` `label` `heading` `title` `display` |
|
|
310
|
+
| `surface` | `transparent` `base` `subtle` `raised` `inset` |
|
|
311
|
+
| `layout.kind` | `vertical` `horizontal` `grid` `stack` |
|
|
312
|
+
| `gap`, `padding` | `none` `xsmall` `small` `medium` `large` `xlarge` |
|
|
313
|
+
| `sizing.width` | `fit` `fill` `content` `narrow` `medium` `wide` |
|
|
314
|
+
| `align`, `justify` | `start` `center` `end` `stretch` (+ `between` for `justify`) |
|
|
315
|
+
| `treatment` | `plain` `badge` `pill` |
|
|
316
|
+
| `control` | `default` `switch` `checkbox` `radio-group` `select` `multiline` `stepper` |
|
|
317
|
+
| `icon` | `add` `delete` `edit` `save` `close` `warning` `success` `error` `information` `navigation-back` `navigation-forward` `menu` `search` `refresh` `settings` `more` |
|
|
318
|
+
|
|
319
|
+
### Value formatting
|
|
320
|
+
|
|
321
|
+
Formatting is presentation. The stored value never changes, and there is no way to supply
|
|
322
|
+
a function:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
presentation: { format: { kind: 'currency', currency: 'NOK' } } // 1250 → "NOK 1,250.00"
|
|
326
|
+
presentation: { format: { kind: 'percentage', decimals: 1 } } // 0.421 → "42.1%"
|
|
327
|
+
presentation: { format: { kind: 'boolean', trueLabel: 'Read', falseLabel: 'Unread' } }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
A display of a boolean, date or datetime field is formatted by inference, without being
|
|
331
|
+
asked. A value a format cannot describe falls back to its plain text rather than inventing
|
|
332
|
+
a plausible result.
|
|
333
|
+
|
|
334
|
+
### Theme
|
|
335
|
+
|
|
336
|
+
A theme is the one place concrete values belong, and it is plain serializable data. Declare
|
|
337
|
+
only what differs:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
graph.setTheme({ appearance: 'dark', defaults: { density: 'compact' }, spacing: { medium: 8 } });
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Light, dark and system appearances need no second graph. `DEFAULT_THEME` is a neutral,
|
|
344
|
+
accessible, responsive theme intended for business applications, and
|
|
345
|
+
`createThemeStylesheet(theme)` is the web renderer's translation of it into CSS custom
|
|
346
|
+
properties and rules.
|
|
347
|
+
|
|
348
|
+
**A theme cannot change behaviour.** Actions, constraints, transition constraints,
|
|
349
|
+
locations, state and routing are untouched by it — which is why "use a denser enterprise
|
|
350
|
+
identity" is one `setTheme` call rather than an edit to every node.
|
|
351
|
+
|
|
352
|
+
### Accessibility
|
|
353
|
+
|
|
354
|
+
Semantic roles produce accessible structure, so the two cannot drift apart:
|
|
355
|
+
|
|
356
|
+
- `header-region`, `navigation-group`, `content-region`, `footer-region`, `sidebar` and
|
|
357
|
+
`form-section` become `<header>`, `<nav>`, `<main>`, `<footer>`, `<aside>`, `<section>`.
|
|
358
|
+
- `textRole` `display` / `title` / `heading` become `<h1>` / `<h2>` / `<h3>`.
|
|
359
|
+
- `error-state` announces itself as an alert; the other status roles as a status.
|
|
360
|
+
- An input's label names its control by id, a required field is marked from the model's own
|
|
361
|
+
`required`, help text is related with `aria-describedby`, and a refused write is
|
|
362
|
+
announced next to the control it was refused on with `aria-invalid`.
|
|
363
|
+
|
|
364
|
+
Validation reports what it can determine reliably — `FORM_INPUT_MISSING_LABEL`,
|
|
365
|
+
`INTERACTIVE_ELEMENT_MISSING_LABEL`, `INVALID_HEADING_STRUCTURE`,
|
|
366
|
+
`DESTRUCTIVE_ACTION_UNMARKED` — and nothing speculative.
|
|
367
|
+
|
|
368
|
+
### UX findings
|
|
369
|
+
|
|
370
|
+
Presentation validation reports what a stylesheet could never tell you. All of it is
|
|
371
|
+
warnings; none of it stops an application from compiling:
|
|
372
|
+
|
|
373
|
+
```text
|
|
374
|
+
MULTIPLE_PRIMARY_ACTIONS FORM_WITHOUT_PRIMARY_ACTION
|
|
375
|
+
DESTRUCTIVE_ACTION_PRESENTED_AS_SUCCESS DESTRUCTIVE_ACTION_UNMARKED
|
|
376
|
+
EMPTY_STATE_WITHOUT_RECOVERY_ACTION EXCESSIVE_HORIZONTAL_ACTIONS
|
|
377
|
+
RIGID_HORIZONTAL_LAYOUT CONFLICTING_SIZING
|
|
378
|
+
PRESENTATION_SEMANTIC_CONFLICT OPAQUE_PRESENTATION
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
And it is queryable, which is the point:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
agent.getPrimaryActions(VIEW); // which action is the emphasised one here?
|
|
385
|
+
agent.getDestructiveActions(VIEW); // which controls are dangerous?
|
|
386
|
+
agent.getFormsWithoutPrimaryAction(); // where is the hierarchy missing?
|
|
387
|
+
agent.getFormStructure(FORM); // sections, required controls, action groups
|
|
388
|
+
agent.getResponsiveBehavior(ROW); // what happens on a phone?
|
|
389
|
+
agent.findNodesByUxRole('empty-state'); // where are the empty states?
|
|
390
|
+
agent.getPresentationWarnings(VIEW); // what is wrong with this screen?
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Presentation state
|
|
394
|
+
|
|
395
|
+
`StateDef.ephemeral: true` marks state that is a UI fact rather than a domain fact — which
|
|
396
|
+
panel is expanded, which tab is selected. Instance validation skips it and it may not be
|
|
397
|
+
persisted, and an agent can tell it from domain state with `getEphemeralStates()`.
|
|
398
|
+
|
|
399
|
+
Presentation never authorizes anything. Hiding a control is not the same as prohibiting an
|
|
400
|
+
operation: a rule belongs in a precondition or a transition constraint, and a governed
|
|
401
|
+
write is checked whether or not any control for it is visible.
|
|
402
|
+
|
|
403
|
+
### The escape hatch
|
|
404
|
+
|
|
405
|
+
`rendererOverrides` attaches renderer-specific presentation, and is explicitly the thing
|
|
406
|
+
semantic analysis does not understand:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
presentation: { rendererOverrides: { web: { className: 'legacy-panel' } } }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
It is keyed by renderer, it makes the node `opaque` in resolved presentation, it is
|
|
413
|
+
reported as `OPAQUE_PRESENTATION`, and `AgentAPI.getOpaquePresentationNodes()` lists every
|
|
414
|
+
node using it. Ordinary applications need none of it, and the acceptance fixtures use none.
|
|
145
415
|
|
|
146
416
|
## Diagnostics
|
|
147
417
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-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.5.0-alpha.1",
|
|
35
|
+
"@cynodia/axiom-runtime": "0.5.0-alpha.1",
|
|
36
|
+
"@cynodia/axiom-compiler": "0.5.0-alpha.1",
|
|
37
|
+
"@cynodia/axiom-agent-api": "0.5.0-alpha.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -b tsconfig.json"
|