@cynodia/axiom-runtime 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 +14 -0
- package/dist/dom.d.ts +17 -0
- package/dist/format.d.ts +8 -0
- package/dist/format.js +122 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/memory-host.d.ts +3 -1
- package/dist/memory-host.js +5 -0
- package/dist/presentation-classes.d.ts +21 -0
- package/dist/presentation-classes.js +154 -0
- package/dist/runtime.d.ts +35 -1
- package/dist/runtime.js +483 -91
- package/dist/source.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,6 +9,13 @@ The domain-independent runtime: the state store, expression evaluation, the muta
|
|
|
9
9
|
engine, constraint checking, the semantic UI renderer and routing. It takes its whole
|
|
10
10
|
environment through a `HostEnvironment`, so it runs in a browser or headlessly.
|
|
11
11
|
|
|
12
|
+
The renderer reads presentation the compiler has already resolved and turns it into
|
|
13
|
+
semantic class names, landmark and heading elements, and formatted values. It writes no
|
|
14
|
+
styles and computes no lengths.
|
|
15
|
+
|
|
16
|
+
Main exports: `createAxiomRuntime`, `createBrowserHost`, `createMemoryHost`,
|
|
17
|
+
`RUNTIME_DIAGNOSTIC_CODES`, `formatValue`, `presentationClassList`.
|
|
18
|
+
|
|
12
19
|
## Installation
|
|
13
20
|
|
|
14
21
|
```bash
|
|
@@ -21,6 +28,13 @@ Most applications should install the facade package instead, which re-exports th
|
|
|
21
28
|
npm install @cynodia/axiom@alpha
|
|
22
29
|
```
|
|
23
30
|
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
The canonical operational contract lives in the `docs/` directory of the
|
|
35
|
+
[`@cynodia/axiom`](https://www.npmjs.com/package/@cynodia/axiom) package, and in
|
|
36
|
+
[the repository](https://github.com/cynodia/axiom). Start with `docs/AGENT_REFERENCE.md`.
|
|
37
|
+
|
|
24
38
|
## License
|
|
25
39
|
|
|
26
40
|
MIT
|
package/dist/dom.d.ts
CHANGED
|
@@ -27,6 +27,21 @@ export interface StorageAdapter {
|
|
|
27
27
|
read(key: string): string | null;
|
|
28
28
|
write(key: string, value: string): void;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A confirmation, described rather than drawn. A host may present it however its platform
|
|
32
|
+
* presents such things; nothing here is browser-specific, and the graph never constructs
|
|
33
|
+
* dialog markup.
|
|
34
|
+
*/
|
|
35
|
+
export interface ConfirmationRequest {
|
|
36
|
+
actionId: string;
|
|
37
|
+
title: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
confirmLabel: string;
|
|
40
|
+
cancelLabel: string;
|
|
41
|
+
severity: 'informational' | 'warning' | 'destructive';
|
|
42
|
+
/** A single line for hosts that can only ask a plain question. */
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
30
45
|
/** Everything the runtime needs from its environment, so nothing is read from globals. */
|
|
31
46
|
export interface HostEnvironment {
|
|
32
47
|
document: DomDocument;
|
|
@@ -34,6 +49,8 @@ export interface HostEnvironment {
|
|
|
34
49
|
pushPath(path: string): void;
|
|
35
50
|
onPathChange(listener: () => void): void;
|
|
36
51
|
confirm(message: string): boolean;
|
|
52
|
+
/** Preferred over `confirm` when the host can present a structured confirmation. */
|
|
53
|
+
confirmRequest?(request: ConfirmationRequest): boolean;
|
|
37
54
|
now(): string;
|
|
38
55
|
uuid(): string;
|
|
39
56
|
storage?: StorageAdapter;
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ValueFormat } from '@cynodia/axiom-core';
|
|
2
|
+
/**
|
|
3
|
+
* Renders a value for display. A value the format cannot describe — a date field holding
|
|
4
|
+
* something that is not a date — falls back to its plain text rather than inventing a
|
|
5
|
+
* plausible-looking result.
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatValue(value: unknown, format: ValueFormat | undefined, locale?: string): string;
|
|
8
|
+
//# sourceMappingURL=format.d.ts.map
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isPresent, toText } from './mutation/values.js';
|
|
2
|
+
/**
|
|
3
|
+
* Semantic value formatting. Formatting is presentation: the stored value never changes,
|
|
4
|
+
* and the graph describes what it wants with a structured `ValueFormat` rather than a
|
|
5
|
+
* function, so the description survives serialization and can be reasoned about.
|
|
6
|
+
*/
|
|
7
|
+
const DATE_STYLES = {
|
|
8
|
+
short: 'short',
|
|
9
|
+
medium: 'medium',
|
|
10
|
+
long: 'long',
|
|
11
|
+
};
|
|
12
|
+
function numberFormat(locale, options) {
|
|
13
|
+
const intl = globalThis.Intl;
|
|
14
|
+
if (!intl?.NumberFormat) {
|
|
15
|
+
return (value) => String(value);
|
|
16
|
+
}
|
|
17
|
+
const formatter = new intl.NumberFormat(locale, options);
|
|
18
|
+
return (value) => formatter.format(value);
|
|
19
|
+
}
|
|
20
|
+
function asNumber(value) {
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return Number.isFinite(value) ? value : null;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
25
|
+
const parsed = Number(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function asDate(value) {
|
|
31
|
+
if (value instanceof Date) {
|
|
32
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
35
|
+
const parsed = new Date(value);
|
|
36
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function formatDate(value, locale, style, withTime) {
|
|
41
|
+
const date = asDate(value);
|
|
42
|
+
if (!date) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const intl = globalThis.Intl;
|
|
46
|
+
if (!intl?.DateTimeFormat) {
|
|
47
|
+
return date.toISOString();
|
|
48
|
+
}
|
|
49
|
+
const dateStyle = DATE_STYLES[style ?? 'medium'] ?? 'medium';
|
|
50
|
+
const options = withTime
|
|
51
|
+
? { dateStyle, timeStyle: dateStyle === 'long' ? 'medium' : 'short' }
|
|
52
|
+
: { dateStyle };
|
|
53
|
+
return new intl.DateTimeFormat(locale, options).format(date);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Renders a value for display. A value the format cannot describe — a date field holding
|
|
57
|
+
* something that is not a date — falls back to its plain text rather than inventing a
|
|
58
|
+
* plausible-looking result.
|
|
59
|
+
*/
|
|
60
|
+
export function formatValue(value, format, locale = 'en-US') {
|
|
61
|
+
if (!format) {
|
|
62
|
+
return toText(value);
|
|
63
|
+
}
|
|
64
|
+
if (format.kind !== 'boolean' && !isPresent(value)) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
switch (format.kind) {
|
|
68
|
+
case 'text':
|
|
69
|
+
return toText(value);
|
|
70
|
+
case 'number': {
|
|
71
|
+
const numeric = asNumber(value);
|
|
72
|
+
if (numeric === null) {
|
|
73
|
+
return toText(value);
|
|
74
|
+
}
|
|
75
|
+
return numberFormat(locale, {
|
|
76
|
+
useGrouping: format.grouping !== false,
|
|
77
|
+
...(format.decimals === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { minimumFractionDigits: format.decimals, maximumFractionDigits: format.decimals }),
|
|
80
|
+
})(numeric);
|
|
81
|
+
}
|
|
82
|
+
case 'currency': {
|
|
83
|
+
const numeric = asNumber(value);
|
|
84
|
+
if (numeric === null) {
|
|
85
|
+
return toText(value);
|
|
86
|
+
}
|
|
87
|
+
return numberFormat(locale, {
|
|
88
|
+
style: 'currency',
|
|
89
|
+
currency: format.currency,
|
|
90
|
+
...(format.decimals === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { minimumFractionDigits: format.decimals, maximumFractionDigits: format.decimals }),
|
|
93
|
+
})(numeric);
|
|
94
|
+
}
|
|
95
|
+
case 'percentage': {
|
|
96
|
+
const numeric = asNumber(value);
|
|
97
|
+
if (numeric === null) {
|
|
98
|
+
return toText(value);
|
|
99
|
+
}
|
|
100
|
+
// A percentage may be stored as a fraction or as a whole number of percent.
|
|
101
|
+
const fraction = format.scale === 'percent' ? numeric / 100 : numeric;
|
|
102
|
+
return numberFormat(locale, {
|
|
103
|
+
style: 'percent',
|
|
104
|
+
...(format.decimals === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { minimumFractionDigits: format.decimals, maximumFractionDigits: format.decimals }),
|
|
107
|
+
})(fraction);
|
|
108
|
+
}
|
|
109
|
+
case 'boolean': {
|
|
110
|
+
if (!isPresent(value)) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
return value ? format.trueLabel ?? 'Yes' : format.falseLabel ?? 'No';
|
|
114
|
+
}
|
|
115
|
+
case 'date':
|
|
116
|
+
return formatDate(value, locale, format.style, false) ?? toText(value);
|
|
117
|
+
case 'datetime':
|
|
118
|
+
return formatDate(value, locale, format.style, true) ?? toText(value);
|
|
119
|
+
default:
|
|
120
|
+
return toText(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from './mutation/store.js';
|
|
|
4
4
|
export * from './mutation/transaction.js';
|
|
5
5
|
export * from './mutation/resolve-location.js';
|
|
6
6
|
export * from './mutation/mutation-engine.js';
|
|
7
|
+
export * from './format.js';
|
|
8
|
+
export * from './presentation-classes.js';
|
|
7
9
|
export * from './runtime.js';
|
|
8
10
|
export * from './memory-host.js';
|
|
9
11
|
export * from './source.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export * from './mutation/store.js';
|
|
|
4
4
|
export * from './mutation/transaction.js';
|
|
5
5
|
export * from './mutation/resolve-location.js';
|
|
6
6
|
export * from './mutation/mutation-engine.js';
|
|
7
|
+
export * from './format.js';
|
|
8
|
+
export * from './presentation-classes.js';
|
|
7
9
|
export * from './runtime.js';
|
|
8
10
|
export * from './memory-host.js';
|
|
9
11
|
export * from './source.js';
|
package/dist/memory-host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DomDocument, DomElement, DomEvent, DomListener, HostEnvironment, StorageAdapter } from './dom.js';
|
|
1
|
+
import type { ConfirmationRequest, DomDocument, DomElement, DomEvent, DomListener, HostEnvironment, StorageAdapter } from './dom.js';
|
|
2
2
|
/**
|
|
3
3
|
* An in-memory DOM and host. The runtime never touches browser globals directly, so the
|
|
4
4
|
* same renderer that drives a real page can be driven headlessly in tests.
|
|
@@ -36,6 +36,8 @@ export interface MemoryHost extends HostEnvironment {
|
|
|
36
36
|
path: string;
|
|
37
37
|
reports: string[];
|
|
38
38
|
confirmations: string[];
|
|
39
|
+
/** The structured confirmations asked for, so a test can assert what was presented. */
|
|
40
|
+
confirmationRequests: ConfirmationRequest[];
|
|
39
41
|
storage?: StorageAdapter;
|
|
40
42
|
}
|
|
41
43
|
export declare function createMemoryHost(options?: MemoryHostOptions): MemoryHost;
|
package/dist/memory-host.js
CHANGED
|
@@ -61,6 +61,7 @@ export function createMemoryHost(options = {}) {
|
|
|
61
61
|
path: options.path ?? '/',
|
|
62
62
|
reports: [],
|
|
63
63
|
confirmations: [],
|
|
64
|
+
confirmationRequests: [],
|
|
64
65
|
document: new MemoryDocument(),
|
|
65
66
|
getPath: () => host.path,
|
|
66
67
|
pushPath: (next) => {
|
|
@@ -73,6 +74,10 @@ export function createMemoryHost(options = {}) {
|
|
|
73
74
|
host.confirmations.push(message);
|
|
74
75
|
return typeof confirmResult === 'function' ? confirmResult() : confirmResult;
|
|
75
76
|
},
|
|
77
|
+
confirmRequest: (request) => {
|
|
78
|
+
host.confirmationRequests.push(request);
|
|
79
|
+
return host.confirm(request.message);
|
|
80
|
+
},
|
|
76
81
|
now: () => {
|
|
77
82
|
counter += 1;
|
|
78
83
|
return `2026-01-01T00:00:${String(counter).padStart(2, '0')}.000Z`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ResolvedPresentation, UxRole } from '@cynodia/axiom-core';
|
|
2
|
+
/** Every class a node's resolved presentation implies, in a stable order. */
|
|
3
|
+
export declare function presentationClassList(resolved: ResolvedPresentation | undefined): string[];
|
|
4
|
+
export declare function presentationClasses(resolved: ResolvedPresentation | undefined, ...extra: string[]): string;
|
|
5
|
+
/**
|
|
6
|
+
* Landmarks. A UX role that names a region of the page becomes the element that means
|
|
7
|
+
* that region, so assistive technology gets the structure the graph declared.
|
|
8
|
+
*/
|
|
9
|
+
export declare function landmarkTag(uxRole: UxRole | undefined): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Headings are real headings, so the document has an outline rather than a set of large
|
|
12
|
+
* words.
|
|
13
|
+
*
|
|
14
|
+
* The element follows the resolved **`headingLevel`**, never the type scale: a value drawn
|
|
15
|
+
* at `display` size with `headingLevel: 'none'` is a `<span>`, which is what a monetary
|
|
16
|
+
* total or a dashboard statistic should be.
|
|
17
|
+
*/
|
|
18
|
+
export declare function headingTag(resolved: ResolvedPresentation | undefined): string | undefined;
|
|
19
|
+
/** The ARIA role a UX role implies, where one is warranted. */
|
|
20
|
+
export declare function ariaRoleFor(uxRole: UxRole | undefined): string | undefined;
|
|
21
|
+
//# sourceMappingURL=presentation-classes.d.ts.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web renderer's translation of resolved presentation into class names.
|
|
3
|
+
*
|
|
4
|
+
* The renderer emits semantic classes and nothing else — no inline styles, no computed
|
|
5
|
+
* lengths, no colours. What those classes mean is decided by the generated stylesheet, so
|
|
6
|
+
* the same resolved presentation can drive an entirely different renderer.
|
|
7
|
+
*/
|
|
8
|
+
function layoutClasses(prefix, layout) {
|
|
9
|
+
// A `none` token is the absence of the property, not a value to assert. Emitting it
|
|
10
|
+
// would override the component rules that give a control its own metrics.
|
|
11
|
+
const classes = [`axiom-${prefix}layout-${layout.kind}`];
|
|
12
|
+
if (layout.gap !== 'none') {
|
|
13
|
+
classes.push(`axiom-${prefix}gap-${layout.gap}`);
|
|
14
|
+
}
|
|
15
|
+
if (!prefix) {
|
|
16
|
+
classes.push(`axiom-align-${layout.align}`, `axiom-justify-${layout.justify}`);
|
|
17
|
+
classes.push(layout.wrap ? 'axiom-wrap' : 'axiom-nowrap');
|
|
18
|
+
if (layout.columns !== undefined) {
|
|
19
|
+
classes.push(typeof layout.columns === 'number'
|
|
20
|
+
? `axiom-columns-${layout.columns}`
|
|
21
|
+
: `axiom-columns-adaptive-${layout.columns.minimum}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return classes;
|
|
25
|
+
}
|
|
26
|
+
function sizingClasses(prefix, sizing) {
|
|
27
|
+
const classes = [`axiom-${prefix}width-${sizing.width}`];
|
|
28
|
+
if (!prefix) {
|
|
29
|
+
classes.push(`axiom-height-${sizing.height}`);
|
|
30
|
+
if (sizing.minWidth) {
|
|
31
|
+
classes.push(`axiom-minwidth-${sizing.minWidth}`);
|
|
32
|
+
}
|
|
33
|
+
if (sizing.maxWidth) {
|
|
34
|
+
classes.push(`axiom-maxwidth-${sizing.maxWidth}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return classes;
|
|
38
|
+
}
|
|
39
|
+
function paddingClasses(prefix, padding) {
|
|
40
|
+
const classes = [];
|
|
41
|
+
if (padding.horizontal !== 'none') {
|
|
42
|
+
classes.push(`axiom-${prefix}pad-x-${padding.horizontal}`);
|
|
43
|
+
}
|
|
44
|
+
if (padding.vertical !== 'none') {
|
|
45
|
+
classes.push(`axiom-${prefix}pad-y-${padding.vertical}`);
|
|
46
|
+
}
|
|
47
|
+
return classes;
|
|
48
|
+
}
|
|
49
|
+
function responsiveClasses(device, override) {
|
|
50
|
+
const prefix = `${device}-`;
|
|
51
|
+
const classes = [];
|
|
52
|
+
if (override.hidden) {
|
|
53
|
+
classes.push(`axiom-${prefix}hidden`);
|
|
54
|
+
}
|
|
55
|
+
if (override.layout) {
|
|
56
|
+
classes.push(...layoutClasses(prefix, override.layout));
|
|
57
|
+
}
|
|
58
|
+
if (override.sizing) {
|
|
59
|
+
classes.push(...sizingClasses(prefix, override.sizing));
|
|
60
|
+
}
|
|
61
|
+
if (override.padding) {
|
|
62
|
+
classes.push(...paddingClasses(prefix, override.padding));
|
|
63
|
+
}
|
|
64
|
+
if (override.density) {
|
|
65
|
+
classes.push(`axiom-${prefix}density-${override.density}`);
|
|
66
|
+
}
|
|
67
|
+
return classes;
|
|
68
|
+
}
|
|
69
|
+
/** Every class a node's resolved presentation implies, in a stable order. */
|
|
70
|
+
export function presentationClassList(resolved) {
|
|
71
|
+
if (!resolved) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const classes = [
|
|
75
|
+
`axiom-density-${resolved.density}`,
|
|
76
|
+
`axiom-emphasis-${resolved.emphasis}`,
|
|
77
|
+
`axiom-surface-${resolved.surface}`,
|
|
78
|
+
`axiom-text-${resolved.textRole}`,
|
|
79
|
+
`axiom-treatment-${resolved.treatment}`,
|
|
80
|
+
];
|
|
81
|
+
if (resolved.role) {
|
|
82
|
+
classes.push(`axiom-role-${resolved.role}`);
|
|
83
|
+
}
|
|
84
|
+
if (resolved.uxRole) {
|
|
85
|
+
classes.push(`axiom-ux-${resolved.uxRole}`);
|
|
86
|
+
}
|
|
87
|
+
classes.push(...layoutClasses('', resolved.layout));
|
|
88
|
+
classes.push(...sizingClasses('', resolved.sizing));
|
|
89
|
+
classes.push(...paddingClasses('', resolved.padding));
|
|
90
|
+
for (const device of ['compact', 'regular', 'wide']) {
|
|
91
|
+
const override = resolved.responsive[device];
|
|
92
|
+
if (override) {
|
|
93
|
+
classes.push(...responsiveClasses(device, override));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// The escape hatch: a renderer-specific class, attached and otherwise not understood.
|
|
97
|
+
const override = resolved.rendererOverrides?.web;
|
|
98
|
+
if (override && typeof override.className === 'string' && override.className.trim()) {
|
|
99
|
+
classes.push('axiom-opaque', ...override.className.trim().split(/\s+/));
|
|
100
|
+
}
|
|
101
|
+
return classes;
|
|
102
|
+
}
|
|
103
|
+
export function presentationClasses(resolved, ...extra) {
|
|
104
|
+
return [...extra, ...presentationClassList(resolved)].filter(Boolean).join(' ');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Landmarks. A UX role that names a region of the page becomes the element that means
|
|
108
|
+
* that region, so assistive technology gets the structure the graph declared.
|
|
109
|
+
*/
|
|
110
|
+
export function landmarkTag(uxRole) {
|
|
111
|
+
switch (uxRole) {
|
|
112
|
+
case 'header-region':
|
|
113
|
+
return 'header';
|
|
114
|
+
case 'footer-region':
|
|
115
|
+
return 'footer';
|
|
116
|
+
case 'navigation-group':
|
|
117
|
+
return 'nav';
|
|
118
|
+
case 'content-region':
|
|
119
|
+
return 'main';
|
|
120
|
+
case 'sidebar':
|
|
121
|
+
return 'aside';
|
|
122
|
+
case 'form-section':
|
|
123
|
+
return 'section';
|
|
124
|
+
default:
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Headings are real headings, so the document has an outline rather than a set of large
|
|
130
|
+
* words.
|
|
131
|
+
*
|
|
132
|
+
* The element follows the resolved **`headingLevel`**, never the type scale: a value drawn
|
|
133
|
+
* at `display` size with `headingLevel: 'none'` is a `<span>`, which is what a monetary
|
|
134
|
+
* total or a dashboard statistic should be.
|
|
135
|
+
*/
|
|
136
|
+
export function headingTag(resolved) {
|
|
137
|
+
const level = resolved?.headingLevel;
|
|
138
|
+
return typeof level === 'number' ? `h${level}` : undefined;
|
|
139
|
+
}
|
|
140
|
+
/** The ARIA role a UX role implies, where one is warranted. */
|
|
141
|
+
export function ariaRoleFor(uxRole) {
|
|
142
|
+
switch (uxRole) {
|
|
143
|
+
case 'toolbar':
|
|
144
|
+
return 'toolbar';
|
|
145
|
+
case 'error-state':
|
|
146
|
+
return 'alert';
|
|
147
|
+
case 'warning-state':
|
|
148
|
+
case 'success-state':
|
|
149
|
+
case 'informational-state':
|
|
150
|
+
return 'status';
|
|
151
|
+
default:
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export declare const RUNTIME_DIAGNOSTIC_CODES: {
|
|
|
31
31
|
readonly PERSISTED_STATE_UNREADABLE: "PERSISTED_STATE_UNREADABLE";
|
|
32
32
|
};
|
|
33
33
|
export type RuntimeDiagnosticCode = (typeof RUNTIME_DIAGNOSTIC_CODES)[keyof typeof RUNTIME_DIAGNOSTIC_CODES];
|
|
34
|
+
/**
|
|
35
|
+
* A structured runtime failure. Match on `code` and read `details`; the `message` is for
|
|
36
|
+
* people and is not a stable contract.
|
|
37
|
+
*/
|
|
34
38
|
export interface RuntimeDiagnostic {
|
|
35
39
|
code: RuntimeDiagnosticCode;
|
|
36
40
|
message: string;
|
|
@@ -49,13 +53,30 @@ export interface ActionResult {
|
|
|
49
53
|
ok: boolean;
|
|
50
54
|
diagnostics: RuntimeDiagnostic[];
|
|
51
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The outcome of an action's most recent invocation.
|
|
58
|
+
*
|
|
59
|
+
* `ok` and `cancelled` both carry no diagnostics, so a `diagnostic` UI node presenting
|
|
60
|
+
* this action shows nothing after either. They are distinguished here because the
|
|
61
|
+
* difference matters to an agent: `cancelled` means a person declined the confirmation,
|
|
62
|
+
* not that the action was refused.
|
|
63
|
+
*/
|
|
64
|
+
export interface ActionOutcome {
|
|
65
|
+
actionId: NodeId;
|
|
66
|
+
outcome: 'ok' | 'failed' | 'cancelled';
|
|
67
|
+
diagnostics: RuntimeDiagnostic[];
|
|
68
|
+
}
|
|
52
69
|
export interface RouteMatch {
|
|
53
70
|
route: CompiledRoute;
|
|
54
71
|
/** Parameter values keyed by route parameter id. */
|
|
55
72
|
parameters: Record<string, string>;
|
|
56
73
|
}
|
|
57
74
|
export type NativeImplementation = (inputs: Record<string, unknown>) => unknown;
|
|
58
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* When entity constraints are evaluated after an input writes to its location.
|
|
77
|
+
* `'deferred'` turns the per-keystroke check off entirely, leaving validity to the next
|
|
78
|
+
* action. Transition constraints are unaffected by this setting and always apply.
|
|
79
|
+
*/
|
|
59
80
|
export type InputValidationMode = 'immediate' | 'deferred';
|
|
60
81
|
export interface AxiomRuntimeOptions {
|
|
61
82
|
ir: ApplicationIR;
|
|
@@ -69,6 +90,7 @@ export interface AxiomRuntimeOptions {
|
|
|
69
90
|
export interface AxiomRuntime {
|
|
70
91
|
start(): void;
|
|
71
92
|
render(): void;
|
|
93
|
+
/** A deep clone of the value. Derived state is recomputed. */
|
|
72
94
|
getState(id: NodeId): unknown;
|
|
73
95
|
/**
|
|
74
96
|
* Replaces a state value outright, for hosts, tests and seeding.
|
|
@@ -78,12 +100,24 @@ export interface AxiomRuntime {
|
|
|
78
100
|
* belongs in actions and input bindings, which are governed.
|
|
79
101
|
*/
|
|
80
102
|
hydrateState(id: NodeId, value: unknown): void;
|
|
103
|
+
/**
|
|
104
|
+
* Runs an action as a transaction and returns the diagnostics **of that invocation**,
|
|
105
|
+
* so nothing has to diff global history. Either every mutation commits or every one is
|
|
106
|
+
* rolled back.
|
|
107
|
+
*/
|
|
81
108
|
invokeAction(id: NodeId, args?: Record<string, unknown>): ActionResult;
|
|
82
109
|
navigate(path: string): void;
|
|
83
110
|
currentRoute(): RouteMatch | null;
|
|
84
111
|
/** Every diagnostic reported so far. Per-invocation results carry their own. */
|
|
85
112
|
diagnostics(): RuntimeDiagnostic[];
|
|
113
|
+
/** Clears the running log **and** every recorded action outcome. */
|
|
86
114
|
clearDiagnostics(): void;
|
|
115
|
+
/**
|
|
116
|
+
* The outcome of this action's most recent invocation, or `undefined` if it has not been
|
|
117
|
+
* invoked since the last clear or route change. This is what a `diagnostic` UI node
|
|
118
|
+
* presents.
|
|
119
|
+
*/
|
|
120
|
+
getActionOutcome(id: NodeId): ActionOutcome | undefined;
|
|
87
121
|
/** Every mutation this runtime has applied, in order, with its semantic location. */
|
|
88
122
|
getMutationLog(): MutationLogEntry[];
|
|
89
123
|
registerNativeOperation(implementationId: string, implementation: NativeImplementation): void;
|