@cynodia/axiom 0.4.1-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.
Files changed (2) hide show
  1. package/README.md +197 -0
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -216,6 +216,203 @@ required(false)→ true
216
216
  Use `is-empty` / `non-empty` for collections and strings, and `coalesce` to fall back on
217
217
  absence — which means falling back *to* an empty collection now works.
218
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.
415
+
219
416
  ## Diagnostics
220
417
 
221
418
  Failures are structured. Match on `code` rather than reading the message:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom",
3
- "version": "0.4.1-alpha.1",
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.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"
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"