@cynodia/axiom-core 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 +18 -2
- package/dist/diagnostics.d.ts +15 -0
- package/dist/diagnostics.js +17 -0
- package/dist/graph.d.ts +18 -3
- package/dist/graph.js +31 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/infer.js +9 -0
- package/dist/ir.d.ts +27 -0
- package/dist/nodes.d.ts +121 -14
- package/dist/presentation.d.ts +297 -0
- package/dist/presentation.js +167 -0
- package/dist/resolve-presentation.d.ts +65 -0
- package/dist/resolve-presentation.js +588 -0
- package/dist/theme.d.ts +171 -0
- package/dist/theme.js +215 -0
- package/dist/types.d.ts +3 -0
- package/dist/ui.d.ts +108 -6
- package/dist/ui.js +20 -0
- package/dist/validate-location.js +10 -1
- package/dist/validate-presentation.d.ts +14 -0
- package/dist/validate-presentation.js +767 -0
- package/dist/validate.js +66 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,17 @@ framework.
|
|
|
6
6
|
**Status: experimental / alpha.** The API may change between alpha releases.
|
|
7
7
|
|
|
8
8
|
The Application Graph and its semantic model: nodes, fields, structured types,
|
|
9
|
-
expressions, **locations** (addressable writable positions), edge derivation,
|
|
10
|
-
|
|
9
|
+
expressions, **locations** (addressable writable positions), edge derivation, validation
|
|
10
|
+
and type inference. It also owns the presentation and UX layer — semantic roles, layout and
|
|
11
|
+
spacing tokens, device classes, value formats and the `Theme` — with presentation
|
|
12
|
+
resolution and validation.
|
|
13
|
+
|
|
14
|
+
Presentation lives here because it is part of the canonical graph: it is intent, not
|
|
15
|
+
styling, and it names no colour, length or CSS property.
|
|
16
|
+
|
|
17
|
+
Main exports: `ApplicationGraph`, `TypeRef` builders, expression builders, `Location`
|
|
18
|
+
builders, `Presentation`, `Theme`, `resolvePresentationMap`, `validateGraph`,
|
|
19
|
+
`VALIDATION_CODES`, `ApplicationIR`.
|
|
11
20
|
|
|
12
21
|
## Installation
|
|
13
22
|
|
|
@@ -21,6 +30,13 @@ Most applications should install the facade package instead, which re-exports th
|
|
|
21
30
|
npm install @cynodia/axiom@alpha
|
|
22
31
|
```
|
|
23
32
|
|
|
33
|
+
|
|
34
|
+
## Documentation
|
|
35
|
+
|
|
36
|
+
The canonical operational contract lives in the `docs/` directory of the
|
|
37
|
+
[`@cynodia/axiom`](https://www.npmjs.com/package/@cynodia/axiom) package, and in
|
|
38
|
+
[the repository](https://github.com/cynodia/axiom). Start with `docs/AGENT_REFERENCE.md`.
|
|
39
|
+
|
|
24
40
|
## License
|
|
25
41
|
|
|
26
42
|
MIT
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -50,5 +50,20 @@ export declare const VALIDATION_CODES: {
|
|
|
50
50
|
readonly initialValueInvalidEntity: "INITIAL_VALUE_INVALID_ENTITY";
|
|
51
51
|
readonly scopeShadowing: "SCOPE_SHADOWING";
|
|
52
52
|
readonly scopeCollidesWithNode: "SCOPE_COLLIDES_WITH_NODE";
|
|
53
|
+
readonly ephemeralStatePersisted: "EPHEMERAL_STATE_PERSISTED";
|
|
54
|
+
readonly unknownPresentationToken: "UNKNOWN_PRESENTATION_TOKEN";
|
|
55
|
+
readonly presentationSemanticConflict: "PRESENTATION_SEMANTIC_CONFLICT";
|
|
56
|
+
readonly multiplePrimaryActions: "MULTIPLE_PRIMARY_ACTIONS";
|
|
57
|
+
readonly formWithoutPrimaryAction: "FORM_WITHOUT_PRIMARY_ACTION";
|
|
58
|
+
readonly destructiveActionPresentedAsSuccess: "DESTRUCTIVE_ACTION_PRESENTED_AS_SUCCESS";
|
|
59
|
+
readonly destructiveActionUnmarked: "DESTRUCTIVE_ACTION_UNMARKED";
|
|
60
|
+
readonly excessiveHorizontalActions: "EXCESSIVE_HORIZONTAL_ACTIONS";
|
|
61
|
+
readonly emptyStateWithoutRecoveryAction: "EMPTY_STATE_WITHOUT_RECOVERY_ACTION";
|
|
62
|
+
readonly rigidHorizontalLayout: "RIGID_HORIZONTAL_LAYOUT";
|
|
63
|
+
readonly conflictingSizing: "CONFLICTING_SIZING";
|
|
64
|
+
readonly interactiveElementMissingLabel: "INTERACTIVE_ELEMENT_MISSING_LABEL";
|
|
65
|
+
readonly formInputMissingLabel: "FORM_INPUT_MISSING_LABEL";
|
|
66
|
+
readonly invalidHeadingStructure: "INVALID_HEADING_STRUCTURE";
|
|
67
|
+
readonly opaquePresentation: "OPAQUE_PRESENTATION";
|
|
53
68
|
};
|
|
54
69
|
//# sourceMappingURL=diagnostics.d.ts.map
|
package/dist/diagnostics.js
CHANGED
|
@@ -33,4 +33,21 @@ export const VALIDATION_CODES = {
|
|
|
33
33
|
initialValueInvalidEntity: 'INITIAL_VALUE_INVALID_ENTITY',
|
|
34
34
|
scopeShadowing: 'SCOPE_SHADOWING',
|
|
35
35
|
scopeCollidesWithNode: 'SCOPE_COLLIDES_WITH_NODE',
|
|
36
|
+
ephemeralStatePersisted: 'EPHEMERAL_STATE_PERSISTED',
|
|
37
|
+
// Presentation and UX. Everything here is a warning except an unknown token, which the
|
|
38
|
+
// renderer genuinely cannot act on.
|
|
39
|
+
unknownPresentationToken: 'UNKNOWN_PRESENTATION_TOKEN',
|
|
40
|
+
presentationSemanticConflict: 'PRESENTATION_SEMANTIC_CONFLICT',
|
|
41
|
+
multiplePrimaryActions: 'MULTIPLE_PRIMARY_ACTIONS',
|
|
42
|
+
formWithoutPrimaryAction: 'FORM_WITHOUT_PRIMARY_ACTION',
|
|
43
|
+
destructiveActionPresentedAsSuccess: 'DESTRUCTIVE_ACTION_PRESENTED_AS_SUCCESS',
|
|
44
|
+
destructiveActionUnmarked: 'DESTRUCTIVE_ACTION_UNMARKED',
|
|
45
|
+
excessiveHorizontalActions: 'EXCESSIVE_HORIZONTAL_ACTIONS',
|
|
46
|
+
emptyStateWithoutRecoveryAction: 'EMPTY_STATE_WITHOUT_RECOVERY_ACTION',
|
|
47
|
+
rigidHorizontalLayout: 'RIGID_HORIZONTAL_LAYOUT',
|
|
48
|
+
conflictingSizing: 'CONFLICTING_SIZING',
|
|
49
|
+
interactiveElementMissingLabel: 'INTERACTIVE_ELEMENT_MISSING_LABEL',
|
|
50
|
+
formInputMissingLabel: 'FORM_INPUT_MISSING_LABEL',
|
|
51
|
+
invalidHeadingStructure: 'INVALID_HEADING_STRUCTURE',
|
|
52
|
+
opaquePresentation: 'OPAQUE_PRESENTATION',
|
|
36
53
|
};
|
package/dist/graph.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EdgeId, FieldId, NodeId } from './ids.js';
|
|
2
2
|
import type { EdgeKind, FieldDef, GraphEdge } from './nodes.js';
|
|
3
3
|
import type { AnyNode, ApplicationGraphData, NodeInput, NodeKind, NodeOfKind } from './types.js';
|
|
4
|
+
import type { Theme, ThemeInput } from './theme.js';
|
|
4
5
|
export interface FieldIndexEntry {
|
|
5
6
|
entityId: NodeId;
|
|
6
7
|
field: FieldDef;
|
|
@@ -9,9 +10,15 @@ export interface EdgeQuery {
|
|
|
9
10
|
kinds?: readonly EdgeKind[];
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
* The Application Graph is the canonical representation of an application.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* The Application Graph is the canonical representation of an application. Everything else
|
|
14
|
+
* — the IR, the emitted page, the DOM — is derived from it and is never edited.
|
|
15
|
+
*
|
|
16
|
+
* Reads return **deep clones**, so a node retrieved from the graph must be written back
|
|
17
|
+
* with `updateNode` for the change to take effect.
|
|
18
|
+
*
|
|
19
|
+
* Relationships are derived from the current nodes on demand and cached against a revision
|
|
20
|
+
* counter, so an edge cannot fall out of step with the nodes it describes and no
|
|
21
|
+
* correctness property depends on resynchronizing anything.
|
|
15
22
|
*/
|
|
16
23
|
export declare class ApplicationGraph {
|
|
17
24
|
private data;
|
|
@@ -25,6 +32,14 @@ export declare class ApplicationGraph {
|
|
|
25
32
|
get id(): string;
|
|
26
33
|
get name(): string;
|
|
27
34
|
get version(): string;
|
|
35
|
+
/**
|
|
36
|
+
* The application's visual identity, completed against the default theme. A theme is
|
|
37
|
+
* presentation only: changing it cannot change an action, a constraint or a route.
|
|
38
|
+
*/
|
|
39
|
+
get theme(): Theme;
|
|
40
|
+
/** Exactly what the application declared, before defaults were filled in. */
|
|
41
|
+
get declaredTheme(): ThemeInput | undefined;
|
|
42
|
+
setTheme(theme: ThemeInput | undefined): void;
|
|
28
43
|
addNode<T extends AnyNode>(node: NodeInput<T>): NodeId;
|
|
29
44
|
getNode<T extends AnyNode = AnyNode>(id: NodeId): T | undefined;
|
|
30
45
|
hasNode(id: NodeId): boolean;
|
package/dist/graph.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { deriveEdges } from './derive-edges.js';
|
|
2
2
|
import { createEdgeId, createNodeId } from './ids.js';
|
|
3
|
+
import { resolveTheme } from './theme.js';
|
|
3
4
|
function clone(value) {
|
|
4
5
|
return structuredClone(value);
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
|
-
* The Application Graph is the canonical representation of an application.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The Application Graph is the canonical representation of an application. Everything else
|
|
9
|
+
* — the IR, the emitted page, the DOM — is derived from it and is never edited.
|
|
10
|
+
*
|
|
11
|
+
* Reads return **deep clones**, so a node retrieved from the graph must be written back
|
|
12
|
+
* with `updateNode` for the change to take effect.
|
|
13
|
+
*
|
|
14
|
+
* Relationships are derived from the current nodes on demand and cached against a revision
|
|
15
|
+
* counter, so an edge cannot fall out of step with the nodes it describes and no
|
|
16
|
+
* correctness property depends on resynchronizing anything.
|
|
10
17
|
*/
|
|
11
18
|
export class ApplicationGraph {
|
|
12
19
|
data;
|
|
@@ -16,7 +23,7 @@ export class ApplicationGraph {
|
|
|
16
23
|
/** Bumped by every change, so the derived edge index can never serve stale data. */
|
|
17
24
|
revision = 0;
|
|
18
25
|
semanticIndex;
|
|
19
|
-
constructor(id, name, version = '0.
|
|
26
|
+
constructor(id, name, version = '0.5.2') {
|
|
20
27
|
this.data = { id, name, version, nodes: {}, edges: {} };
|
|
21
28
|
}
|
|
22
29
|
get id() {
|
|
@@ -28,6 +35,26 @@ export class ApplicationGraph {
|
|
|
28
35
|
get version() {
|
|
29
36
|
return this.data.version;
|
|
30
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The application's visual identity, completed against the default theme. A theme is
|
|
40
|
+
* presentation only: changing it cannot change an action, a constraint or a route.
|
|
41
|
+
*/
|
|
42
|
+
get theme() {
|
|
43
|
+
return resolveTheme(this.data.theme);
|
|
44
|
+
}
|
|
45
|
+
/** Exactly what the application declared, before defaults were filled in. */
|
|
46
|
+
get declaredTheme() {
|
|
47
|
+
return this.data.theme ? structuredClone(this.data.theme) : undefined;
|
|
48
|
+
}
|
|
49
|
+
setTheme(theme) {
|
|
50
|
+
if (theme === undefined) {
|
|
51
|
+
delete this.data.theme;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.data.theme = structuredClone(theme);
|
|
55
|
+
}
|
|
56
|
+
this.revision += 1;
|
|
57
|
+
}
|
|
31
58
|
addNode(node) {
|
|
32
59
|
const id = (node.id ?? createNodeId(node.kind));
|
|
33
60
|
if (this.data.nodes[id]) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './ids.js';
|
|
2
2
|
export * from './diagnostics.js';
|
|
3
3
|
export * from './location.js';
|
|
4
|
+
export * from './presentation.js';
|
|
5
|
+
export * from './theme.js';
|
|
4
6
|
export * from './type-ref.js';
|
|
5
7
|
export * from './expressions.js';
|
|
6
8
|
export * from './nodes.js';
|
|
@@ -10,7 +12,9 @@ export * from './graph.js';
|
|
|
10
12
|
export * from './infer.js';
|
|
11
13
|
export * from './context.js';
|
|
12
14
|
export * from './validate-location.js';
|
|
15
|
+
export * from './resolve-presentation.js';
|
|
13
16
|
export * from './validate.js';
|
|
17
|
+
export * from './validate-presentation.js';
|
|
14
18
|
export * from './derive-edges.js';
|
|
15
19
|
export * from './ir.js';
|
|
16
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './ids.js';
|
|
2
2
|
export * from './diagnostics.js';
|
|
3
3
|
export * from './location.js';
|
|
4
|
+
export * from './presentation.js';
|
|
5
|
+
export * from './theme.js';
|
|
4
6
|
export * from './type-ref.js';
|
|
5
7
|
export * from './expressions.js';
|
|
6
8
|
export * from './nodes.js';
|
|
@@ -10,6 +12,8 @@ export * from './graph.js';
|
|
|
10
12
|
export * from './infer.js';
|
|
11
13
|
export * from './context.js';
|
|
12
14
|
export * from './validate-location.js';
|
|
15
|
+
export * from './resolve-presentation.js';
|
|
13
16
|
export * from './validate.js';
|
|
17
|
+
export * from './validate-presentation.js';
|
|
14
18
|
export * from './derive-edges.js';
|
|
15
19
|
export * from './ir.js';
|
package/dist/infer.js
CHANGED
|
@@ -131,6 +131,15 @@ export function inferExpressionType(expression, context, scope) {
|
|
|
131
131
|
return primitiveType('string');
|
|
132
132
|
case 'now':
|
|
133
133
|
return primitiveType('datetime');
|
|
134
|
+
case 'coalesce': {
|
|
135
|
+
// The type of the first argument, with its optionality removed: a fallback is
|
|
136
|
+
// what makes the value present. Inferring this is what lets a repeat over
|
|
137
|
+
// `coalesce(field(...), [])` still know how to identify its members.
|
|
138
|
+
const first = expression.arguments[0]
|
|
139
|
+
? inferExpressionType(expression.arguments[0], context, scope)
|
|
140
|
+
: undefined;
|
|
141
|
+
return first?.kind === 'optional' ? first.valueType : first;
|
|
142
|
+
}
|
|
134
143
|
default:
|
|
135
144
|
return undefined;
|
|
136
145
|
}
|
package/dist/ir.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { FieldIndexEntry } from './graph.js';
|
|
|
4
4
|
import type { UINode } from './ui.js';
|
|
5
5
|
import type { AnyNode } from './types.js';
|
|
6
6
|
import type { TypeRef } from './type-ref.js';
|
|
7
|
+
import type { ResolvedPresentation } from './presentation.js';
|
|
8
|
+
import type { Theme } from './theme.js';
|
|
7
9
|
export interface RouteSegment {
|
|
8
10
|
kind: 'static' | 'parameter';
|
|
9
11
|
value: string;
|
|
@@ -48,5 +50,30 @@ export interface ApplicationIR {
|
|
|
48
50
|
* to canonical application state from a write to a draft.
|
|
49
51
|
*/
|
|
50
52
|
locationRoots: Record<NodeId, NodeId>;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the field each input addresses is declared required, resolved here so a
|
|
55
|
+
* renderer can mark it without re-deriving the model.
|
|
56
|
+
*/
|
|
57
|
+
locationRequired: Record<NodeId, boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* The field that distinguishes the members of each `repeat` node's collection, resolved
|
|
60
|
+
* here so a runtime carries no type inference of its own.
|
|
61
|
+
*
|
|
62
|
+
* A renderer uses it to give every rendered repeat instance a stable identity. A repeat
|
|
63
|
+
* whose member type cannot be resolved statically is absent from this map, and the
|
|
64
|
+
* renderer falls back to the iteration index.
|
|
65
|
+
*/
|
|
66
|
+
repeatIdentityFields: Record<NodeId, FieldId>;
|
|
67
|
+
/** The application's visual identity, completed against the default theme. */
|
|
68
|
+
theme: Theme;
|
|
69
|
+
/**
|
|
70
|
+
* Presentation with every question already answered, per UI node: renderer defaults,
|
|
71
|
+
* theme, inheritance, semantic inference, node declaration and responsive overrides all
|
|
72
|
+
* resolved. A renderer reads this and needs to know nothing about how it was decided.
|
|
73
|
+
*
|
|
74
|
+
* It is deliberately still semantic — roles, tokens and device classes, not CSS — so a
|
|
75
|
+
* second renderer remains possible.
|
|
76
|
+
*/
|
|
77
|
+
presentation: Record<NodeId, ResolvedPresentation>;
|
|
51
78
|
}
|
|
52
79
|
//# sourceMappingURL=ir.d.ts.map
|
package/dist/nodes.d.ts
CHANGED
|
@@ -2,21 +2,29 @@ import type { Expression } from './expressions.js';
|
|
|
2
2
|
import type { CollectionItemLocation, Location } from './location.js';
|
|
3
3
|
import type { EdgeId, FieldId, NodeId } from './ids.js';
|
|
4
4
|
import type { TypeRef } from './type-ref.js';
|
|
5
|
-
|
|
6
|
-
export interface PresentationHints {
|
|
7
|
-
role?: 'primary' | 'secondary' | 'danger';
|
|
8
|
-
density?: 'compact' | 'normal';
|
|
9
|
-
emphasis?: 'normal' | 'strong';
|
|
10
|
-
}
|
|
5
|
+
import type { ConfirmationPresentation } from './presentation.js';
|
|
11
6
|
export interface NodeBase {
|
|
12
7
|
id: NodeId;
|
|
13
8
|
name?: string;
|
|
14
9
|
metadata?: Record<string, unknown>;
|
|
15
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A field of an entity.
|
|
13
|
+
*
|
|
14
|
+
* Fields are independently identifiable and globally unique across the graph, and
|
|
15
|
+
* **instance data is keyed by `FieldId`, never by `name`**: a record looks like
|
|
16
|
+
* `{ [F_TITLE]: 'Dune' }`, not `{ title: 'Dune' }`. `name` is metadata for humans and
|
|
17
|
+
* resolves nothing.
|
|
18
|
+
*/
|
|
16
19
|
export interface FieldDef {
|
|
17
20
|
id: FieldId;
|
|
18
21
|
name?: string;
|
|
19
22
|
valueType: TypeRef;
|
|
23
|
+
/**
|
|
24
|
+
* The value must be **present** in every canonical instance — not `null` and not
|
|
25
|
+
* `undefined`. It says nothing about emptiness: `0`, `false`, `''` and `[]` all satisfy
|
|
26
|
+
* it. Express "must not be blank" as a `ConstraintDef` using `non-empty`.
|
|
27
|
+
*/
|
|
20
28
|
required?: boolean;
|
|
21
29
|
defaultValue?: LiteralValue;
|
|
22
30
|
metadata?: Record<string, unknown>;
|
|
@@ -24,10 +32,21 @@ export interface FieldDef {
|
|
|
24
32
|
export type LiteralValue = string | number | boolean | null | LiteralValue[] | {
|
|
25
33
|
[key: string]: LiteralValue;
|
|
26
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* A record type. Instances of it are found wherever a state's declared type says they
|
|
37
|
+
* live — including nested inside collections and inside other entities — and are validated
|
|
38
|
+
* there.
|
|
39
|
+
*/
|
|
27
40
|
export interface EntityDef extends NodeBase {
|
|
28
41
|
kind: 'entity';
|
|
29
42
|
fields: FieldDef[];
|
|
30
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Field used to distinguish instances of this entity.
|
|
45
|
+
*
|
|
46
|
+
* Required in order to address an instance by identity (`identitySelector`), and
|
|
47
|
+
* required by every `TransitionConstraintDef` on this entity — without it such a rule is
|
|
48
|
+
* **silently skipped**. Declare it on every entity stored in a collection.
|
|
49
|
+
*/
|
|
31
50
|
identityFieldId?: FieldId;
|
|
32
51
|
}
|
|
33
52
|
export type StatePersistence = {
|
|
@@ -39,17 +58,50 @@ export type StatePersistence = {
|
|
|
39
58
|
kind: 'remote';
|
|
40
59
|
sourceId: NodeId;
|
|
41
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* A named application value: stored, or computed from other state.
|
|
63
|
+
*
|
|
64
|
+
* A state with no `derivation` is stored, and one that is neither `draft` nor `ephemeral`
|
|
65
|
+
* is **canonical** — the state entity constraints and schema conformance apply to. Stored
|
|
66
|
+
* values are deeply frozen on entry to the store, and every read hands out a copy.
|
|
67
|
+
*/
|
|
42
68
|
export interface StateDef extends NodeBase {
|
|
43
69
|
kind: 'state';
|
|
44
70
|
valueType: TypeRef;
|
|
71
|
+
/**
|
|
72
|
+
* Seed data, keyed by `FieldId` wherever it contains a record. It is walked against
|
|
73
|
+
* `valueType` recursively at validation time, so data keyed by field *name* is rejected
|
|
74
|
+
* rather than surfacing later as an inexplicably empty UI.
|
|
75
|
+
*
|
|
76
|
+
* Absent, the state starts at the default for its type: `optional` → `null`,
|
|
77
|
+
* `collection` → `[]`, `number` → `0`, `boolean` → `false`, other primitive → `''`,
|
|
78
|
+
* `enum` → its first value, `entity` → `null`.
|
|
79
|
+
*/
|
|
45
80
|
initialValue?: LiteralValue;
|
|
46
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* When present the state is computed rather than stored.
|
|
83
|
+
*
|
|
84
|
+
* Derived state is **read-only**: a write is rejected by `validateGraph` and by the
|
|
85
|
+
* runtime. It is recomputed on demand and handed out as a frozen deep copy, so nothing
|
|
86
|
+
* can work by sharing an object with the state it was derived from. Instance validation
|
|
87
|
+
* skips it, because the data is already validated where it is stored.
|
|
88
|
+
*/
|
|
47
89
|
derivation?: Expression;
|
|
48
90
|
/**
|
|
49
91
|
* Marks a state that holds work in progress. Draft instances are incomplete by
|
|
50
92
|
* definition, so instance validation skips them until an action commits the value.
|
|
51
93
|
*/
|
|
52
94
|
draft?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Marks ephemeral presentation state — which panel is expanded, which tab is selected,
|
|
97
|
+
* whether a dialog is open. It is not canonical domain state: instance validation skips
|
|
98
|
+
* it, and it may not be persisted. Marking it says so in the graph instead of leaving an
|
|
99
|
+
* agent to guess which states are domain facts.
|
|
100
|
+
*
|
|
101
|
+
* It changes what a state *is*, never what is permitted: a write reaching domain state
|
|
102
|
+
* is governed exactly as before.
|
|
103
|
+
*/
|
|
104
|
+
ephemeral?: boolean;
|
|
53
105
|
persistence?: StatePersistence;
|
|
54
106
|
}
|
|
55
107
|
export interface ActionParameter {
|
|
@@ -67,6 +119,18 @@ export interface ActionGuard {
|
|
|
67
119
|
condition: Expression;
|
|
68
120
|
failureMode?: FailureMode;
|
|
69
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Behaviour expressed as data, executed as a transaction.
|
|
124
|
+
*
|
|
125
|
+
* An invocation proceeds: resolve the action, bind parameters, evaluate preconditions in
|
|
126
|
+
* order, ask for confirmation if required — **none of which opens a transaction, so a
|
|
127
|
+
* refusal at any of those stages mutates nothing** — then begin a transaction, run the
|
|
128
|
+
* operations sequentially against provisional state, evaluate entity constraints,
|
|
129
|
+
* transition constraints and postconditions, and either commit everything or roll back
|
|
130
|
+
* every mutation.
|
|
131
|
+
*
|
|
132
|
+
* `invokeAction` returns the diagnostics of that invocation.
|
|
133
|
+
*/
|
|
70
134
|
export interface ActionDef extends NodeBase {
|
|
71
135
|
kind: 'action';
|
|
72
136
|
parameters?: ActionParameter[];
|
|
@@ -80,6 +144,8 @@ export interface ActionDef extends NodeBase {
|
|
|
80
144
|
destructive?: boolean;
|
|
81
145
|
requiresConfirmation?: boolean;
|
|
82
146
|
confirmationMessage?: string;
|
|
147
|
+
/** What the confirmation says, when a plain message is not enough. */
|
|
148
|
+
confirmation?: ConfirmationPresentation;
|
|
83
149
|
}
|
|
84
150
|
/**
|
|
85
151
|
* The conditions an action checks, however they were written. `guards` pairs each
|
|
@@ -92,30 +158,53 @@ export type OperationKind = Operation['kind'];
|
|
|
92
158
|
export declare const OPERATION_KINDS: readonly OperationKind[];
|
|
93
159
|
/** Every mutation is a set, an insert or a remove against an addressed Location. */
|
|
94
160
|
export type MutationOperation = SetOperation | InsertOperation | RemoveOperation;
|
|
161
|
+
/**
|
|
162
|
+
* Writes a value to an addressed position.
|
|
163
|
+
*
|
|
164
|
+
* A missing field along the path is created. A missing **collection item** is not: an
|
|
165
|
+
* identity selector that matches nothing reports `LOCATION_RESOLUTION_FAILED`.
|
|
166
|
+
*/
|
|
95
167
|
export interface SetOperation {
|
|
96
168
|
kind: 'set';
|
|
97
169
|
target: Location;
|
|
98
170
|
value: Expression;
|
|
99
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Adds a member to a collection. The constructed value is deep-cloned before it is stored.
|
|
174
|
+
*
|
|
175
|
+
* A newly inserted entity instance has no previous state, so **no transition constraint is
|
|
176
|
+
* evaluated for it**. Govern creation with an action guard or an entity constraint.
|
|
177
|
+
*/
|
|
100
178
|
export interface InsertOperation {
|
|
101
179
|
kind: 'insert';
|
|
102
|
-
/** A location addressing a collection. */
|
|
180
|
+
/** A location addressing a collection. A non-array current value is treated as `[]`. */
|
|
103
181
|
target: Location;
|
|
104
182
|
value: Expression;
|
|
183
|
+
/** Defaults to `'end'`. */
|
|
105
184
|
position?: 'start' | 'end';
|
|
106
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Removes one member of a collection.
|
|
188
|
+
*
|
|
189
|
+
* A selector that matches nothing is a **no-op**: no mutation, no log entry and no
|
|
190
|
+
* diagnostic. Removing an existing instance *is* a transition, with the proposed value
|
|
191
|
+
* bound to nothing.
|
|
192
|
+
*/
|
|
107
193
|
export interface RemoveOperation {
|
|
108
194
|
kind: 'remove';
|
|
109
195
|
target: CollectionItemLocation;
|
|
110
196
|
}
|
|
111
197
|
/**
|
|
112
|
-
* Performs a set of mutations once per member of a collection.
|
|
113
|
-
*
|
|
114
|
-
*
|
|
198
|
+
* Performs a set of mutations once per member of a collection.
|
|
199
|
+
*
|
|
200
|
+
* Iteration N observes provisional writes from previous iterations. The loop executes
|
|
201
|
+
* inside the containing action's transaction and opens none of its own, so any failure
|
|
202
|
+
* rolls back the complete action — every iteration included. The collection itself is
|
|
203
|
+
* evaluated **once**, before the first member is mutated.
|
|
115
204
|
*
|
|
116
205
|
* `scopeId` introduces an iteration scope. Nested expressions refer to the current member
|
|
117
206
|
* as `ref(scopeId)`, and nested locations may use it to address the canonical record the
|
|
118
|
-
* member points at.
|
|
207
|
+
* member points at. Nested operations must be mutations only.
|
|
119
208
|
*/
|
|
120
209
|
export interface ForEachOperation {
|
|
121
210
|
kind: 'for-each';
|
|
@@ -163,11 +252,22 @@ export interface NativeOperation {
|
|
|
163
252
|
resultTarget?: Location;
|
|
164
253
|
declaredEffects?: NativeEffect[];
|
|
165
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* An invariant over proposed state, evaluated after every governed mutation.
|
|
257
|
+
*
|
|
258
|
+
* A constraint that cannot be evaluated counts as **violated**, never as satisfied.
|
|
259
|
+
*/
|
|
166
260
|
export interface ConstraintDef extends NodeBase {
|
|
167
261
|
kind: 'constraint';
|
|
168
262
|
expression: Expression;
|
|
169
|
-
/**
|
|
263
|
+
/**
|
|
264
|
+
* When set, the expression is evaluated once per canonical instance of this entity, with
|
|
265
|
+
* the instance bound to `ref(entityId)` — wherever that instance is stored, including
|
|
266
|
+
* nested inside another entity. Without it the expression is evaluated once, in the root
|
|
267
|
+
* scope.
|
|
268
|
+
*/
|
|
170
269
|
entityId?: NodeId;
|
|
270
|
+
/** Defaults to `'error'`. A `'warning'` is advice and **never blocks a write**. */
|
|
171
271
|
severity?: 'error' | 'warning';
|
|
172
272
|
message?: string;
|
|
173
273
|
}
|
|
@@ -182,6 +282,13 @@ export interface ConstraintDef extends NodeBase {
|
|
|
182
282
|
*
|
|
183
283
|
* `previousScopeId` and `proposedScopeId` bind those two instances for the expression.
|
|
184
284
|
* When the instance is being removed, the proposed scope is bound to nothing.
|
|
285
|
+
*
|
|
286
|
+
* "Previous" means committed state as it stood immediately before the **outermost**
|
|
287
|
+
* transaction began — not the previous operation, and not the previous iteration.
|
|
288
|
+
*
|
|
289
|
+
* A **newly inserted** instance has no previous state and is therefore **not evaluated**.
|
|
290
|
+
* Govern creation with an action guard or an entity constraint. The entity must declare
|
|
291
|
+
* `identityFieldId`; without one this rule is silently skipped.
|
|
185
292
|
*/
|
|
186
293
|
export interface TransitionConstraintDef extends NodeBase {
|
|
187
294
|
kind: 'transition-constraint';
|