@cynodia/axiom-core 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 +5 -0
- package/dist/derive-edges.js +67 -6
- package/dist/diagnostics.d.ts +17 -0
- package/dist/diagnostics.js +19 -0
- package/dist/expressions.d.ts +44 -2
- package/dist/expressions.js +52 -0
- package/dist/graph.d.ts +9 -0
- package/dist/graph.js +22 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/infer.js +10 -0
- package/dist/ir.d.ts +21 -1
- package/dist/nodes.d.ts +48 -6
- package/dist/nodes.js +13 -0
- package/dist/presentation.d.ts +270 -0
- package/dist/presentation.js +152 -0
- package/dist/resolve-presentation.d.ts +64 -0
- package/dist/resolve-presentation.js +545 -0
- package/dist/theme.d.ts +152 -0
- package/dist/theme.js +207 -0
- package/dist/types.d.ts +6 -3
- package/dist/ui.d.ts +13 -2
- package/dist/validate-presentation.d.ts +14 -0
- package/dist/validate-presentation.js +561 -0
- package/dist/validate.js +108 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,11 @@ The Application Graph and its semantic model: nodes, fields, structured types,
|
|
|
9
9
|
expressions, **locations** (addressable writable positions), edge derivation,
|
|
10
10
|
validation and type inference.
|
|
11
11
|
|
|
12
|
+
It also owns the presentation and UX layer — semantic roles, layout, spacing and sizing
|
|
13
|
+
tokens, device classes, value formats and the `Theme` — together with presentation
|
|
14
|
+
resolution and validation. Presentation lives here because it is part of the canonical
|
|
15
|
+
graph: it is intent, not styling, and it names no colour, length or CSS property.
|
|
16
|
+
|
|
12
17
|
## Installation
|
|
13
18
|
|
|
14
19
|
```bash
|
package/dist/derive-edges.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expressionFieldIds, walkExpression } from './expressions.js';
|
|
1
|
+
import { constructedFieldIds, expressionFieldIds, walkExpression } from './expressions.js';
|
|
2
2
|
import { locationExpressions, locationFieldIds, locationRootStateId, locationSelectorFieldIds, } from './location.js';
|
|
3
3
|
import { isUINode } from './ui.js';
|
|
4
4
|
/** Ids a `ref` expression mentions anywhere in the tree. */
|
|
@@ -42,6 +42,19 @@ export function deriveEdges(nodes) {
|
|
|
42
42
|
rootScope.set(node.id, statesOf(node.source, new Map(), states));
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
// Which states can hold instances of an entity, including nested ones. An entity-scoped
|
|
46
|
+
// constraint reads its fields wherever those instances actually live.
|
|
47
|
+
const entities = new Map(nodes.filter((node) => node.kind === 'entity').map((node) => [node.id, node]));
|
|
48
|
+
const statesByEntity = new Map();
|
|
49
|
+
for (const node of nodes) {
|
|
50
|
+
if (node.kind !== 'state' || node.draft || node.derivation) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const entityId of reachableEntities(node.valueType, entities)) {
|
|
54
|
+
statesByEntity.set(entityId, [...(statesByEntity.get(entityId) ?? []), node.id]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const entityScope = (entityId) => new Map([...rootScope, [entityId, statesByEntity.get(entityId) ?? []]]);
|
|
45
58
|
const link = (from, to, kind, fieldIds = []) => {
|
|
46
59
|
if (from === to || !known.has(from) || !known.has(to)) {
|
|
47
60
|
return;
|
|
@@ -92,8 +105,14 @@ export function deriveEdges(nodes) {
|
|
|
92
105
|
if (node.entityId) {
|
|
93
106
|
link(node.id, node.entityId, 'constrains', expressionFieldIds(node.expression));
|
|
94
107
|
}
|
|
95
|
-
reads(node.id, node.expression, rootScope);
|
|
108
|
+
reads(node.id, node.expression, node.entityId ? entityScope(node.entityId) : rootScope);
|
|
109
|
+
break;
|
|
110
|
+
case 'transition-constraint': {
|
|
111
|
+
link(node.id, node.entityId, 'constrains', expressionFieldIds(node.expression));
|
|
112
|
+
const holders = statesByEntity.get(node.entityId) ?? [];
|
|
113
|
+
reads(node.id, node.expression, new Map([...rootScope, [node.previousScopeId, holders], [node.proposedScopeId, holders]]));
|
|
96
114
|
break;
|
|
115
|
+
}
|
|
97
116
|
case 'route':
|
|
98
117
|
link(node.id, node.viewId, 'routes-to');
|
|
99
118
|
break;
|
|
@@ -111,7 +130,11 @@ export function deriveEdges(nodes) {
|
|
|
111
130
|
},
|
|
112
131
|
}));
|
|
113
132
|
}
|
|
114
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* The states an expression ultimately draws its members from. Following `field` and calls
|
|
135
|
+
* matters: a collection reached as `coalesce(field(ref(state), lines), [])` still comes
|
|
136
|
+
* from that state, and its members' fields are still reads of it.
|
|
137
|
+
*/
|
|
115
138
|
function statesOf(expression, scope, states) {
|
|
116
139
|
switch (expression.kind) {
|
|
117
140
|
case 'ref': {
|
|
@@ -125,11 +148,38 @@ function statesOf(expression, scope, states) {
|
|
|
125
148
|
case 'find':
|
|
126
149
|
case 'sort':
|
|
127
150
|
case 'map':
|
|
151
|
+
case 'flatten':
|
|
152
|
+
return statesOf(expression.source, scope, states);
|
|
153
|
+
case 'conditional':
|
|
154
|
+
return [
|
|
155
|
+
...new Set([
|
|
156
|
+
...statesOf(expression.whenTrue, scope, states),
|
|
157
|
+
...statesOf(expression.whenFalse, scope, states),
|
|
158
|
+
]),
|
|
159
|
+
];
|
|
160
|
+
case 'field':
|
|
128
161
|
return statesOf(expression.source, scope, states);
|
|
162
|
+
case 'call':
|
|
163
|
+
return [...new Set(expression.arguments.flatMap((argument) => statesOf(argument, scope, states)))];
|
|
129
164
|
default:
|
|
130
165
|
return [];
|
|
131
166
|
}
|
|
132
167
|
}
|
|
168
|
+
/** Entities reachable from a type, following entity fields as well as collections. */
|
|
169
|
+
function reachableEntities(type, entities, seen = new Set()) {
|
|
170
|
+
const found = [];
|
|
171
|
+
for (const entityId of entityIdsIn(type)) {
|
|
172
|
+
if (seen.has(entityId)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
seen.add(entityId);
|
|
176
|
+
found.push(entityId);
|
|
177
|
+
for (const field of entities.get(entityId)?.fields ?? []) {
|
|
178
|
+
found.push(...reachableEntities(field.valueType, entities, seen));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return found;
|
|
182
|
+
}
|
|
133
183
|
function bind(scope, id, targets) {
|
|
134
184
|
const next = new Map(scope);
|
|
135
185
|
next.set(id, targets);
|
|
@@ -180,7 +230,9 @@ function collectReads(expression, scope, states, found = new Map()) {
|
|
|
180
230
|
case 'filter':
|
|
181
231
|
case 'find':
|
|
182
232
|
case 'map':
|
|
183
|
-
case 'sort':
|
|
233
|
+
case 'sort':
|
|
234
|
+
case 'every':
|
|
235
|
+
case 'some': {
|
|
184
236
|
collectReads(expression.source, scope, states, found);
|
|
185
237
|
const inner = bind(scope, expression.scopeId, statesOf(expression.source, scope, states));
|
|
186
238
|
const body = expression.kind === 'map'
|
|
@@ -191,6 +243,14 @@ function collectReads(expression, scope, states, found = new Map()) {
|
|
|
191
243
|
collectReads(body, inner, states, found);
|
|
192
244
|
return found;
|
|
193
245
|
}
|
|
246
|
+
case 'flatten':
|
|
247
|
+
collectReads(expression.source, scope, states, found);
|
|
248
|
+
return found;
|
|
249
|
+
case 'conditional':
|
|
250
|
+
collectReads(expression.condition, scope, states, found);
|
|
251
|
+
collectReads(expression.whenTrue, scope, states, found);
|
|
252
|
+
collectReads(expression.whenFalse, scope, states, found);
|
|
253
|
+
return found;
|
|
194
254
|
default:
|
|
195
255
|
return found;
|
|
196
256
|
}
|
|
@@ -217,8 +277,9 @@ function linkOperations(actionId, operations, linker, scope) {
|
|
|
217
277
|
linker.reads(actionId, operation.value, scope);
|
|
218
278
|
break;
|
|
219
279
|
case 'insert':
|
|
220
|
-
//
|
|
221
|
-
|
|
280
|
+
// A constructed record writes the fields it declares. Fields consulted while
|
|
281
|
+
// computing those values are reads, and must not be reported as writes.
|
|
282
|
+
linker.writes(actionId, operation.target, scope, 'writes', constructedFieldIds(operation.value));
|
|
222
283
|
linker.reads(actionId, operation.value, scope);
|
|
223
284
|
break;
|
|
224
285
|
case 'remove':
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -48,5 +48,22 @@ export declare const VALIDATION_CODES: {
|
|
|
48
48
|
readonly initialValueUnknownField: "INITIAL_VALUE_UNKNOWN_FIELD";
|
|
49
49
|
readonly initialValueMissingRequiredField: "INITIAL_VALUE_MISSING_REQUIRED_FIELD";
|
|
50
50
|
readonly initialValueInvalidEntity: "INITIAL_VALUE_INVALID_ENTITY";
|
|
51
|
+
readonly scopeShadowing: "SCOPE_SHADOWING";
|
|
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";
|
|
51
68
|
};
|
|
52
69
|
//# sourceMappingURL=diagnostics.d.ts.map
|
package/dist/diagnostics.js
CHANGED
|
@@ -31,4 +31,23 @@ export const VALIDATION_CODES = {
|
|
|
31
31
|
initialValueUnknownField: 'INITIAL_VALUE_UNKNOWN_FIELD',
|
|
32
32
|
initialValueMissingRequiredField: 'INITIAL_VALUE_MISSING_REQUIRED_FIELD',
|
|
33
33
|
initialValueInvalidEntity: 'INITIAL_VALUE_INVALID_ENTITY',
|
|
34
|
+
scopeShadowing: 'SCOPE_SHADOWING',
|
|
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',
|
|
34
53
|
};
|
package/dist/expressions.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { LiteralValue } from './nodes.js';
|
|
|
5
5
|
* identifier: `ref` resolves an id against the evaluation scope chain (route parameters,
|
|
6
6
|
* action parameters, iteration scopes, then state), and `field` reads a field by id.
|
|
7
7
|
*/
|
|
8
|
-
export type Expression = LiteralExpression | RefExpression | FieldExpression | ObjectExpression | BinaryExpression | UnaryExpression | CallExpression | FilterExpression | FindExpression | MapExpression | SortExpression;
|
|
8
|
+
export type Expression = LiteralExpression | RefExpression | FieldExpression | ObjectExpression | BinaryExpression | UnaryExpression | CallExpression | FilterExpression | FindExpression | MapExpression | SortExpression | EveryExpression | SomeExpression | FlattenExpression | ConditionalExpression;
|
|
9
9
|
export type ExpressionKind = Expression['kind'];
|
|
10
10
|
/** Every expression kind the runtime is required to evaluate. */
|
|
11
11
|
export declare const EXPRESSION_KINDS: readonly ExpressionKind[];
|
|
@@ -57,7 +57,7 @@ export interface UnaryExpression {
|
|
|
57
57
|
* must be implemented by the runtime: a function that is declared here but unevaluated
|
|
58
58
|
* would be a construct that typechecks, validates and then does nothing.
|
|
59
59
|
*/
|
|
60
|
-
export type BuiltinFunction = 'required' | 'is-empty' | 'length' | 'contains' | 'concat' | 'coalesce' | 'one-of' | 'count' | 'sum' | 'lowercase' | 'to-string' | 'now' | 'uuid';
|
|
60
|
+
export type BuiltinFunction = 'required' | 'is-empty' | 'non-empty' | 'length' | 'contains' | 'concat' | 'coalesce' | 'one-of' | 'count' | 'sum' | 'lowercase' | 'to-string' | 'now' | 'uuid';
|
|
61
61
|
export declare const BUILTIN_FUNCTIONS: readonly BuiltinFunction[];
|
|
62
62
|
/** Functions that reduce a collection of numbers to a number. */
|
|
63
63
|
export declare const AGGREGATE_FUNCTIONS: readonly BuiltinFunction[];
|
|
@@ -99,6 +99,32 @@ export interface SortExpression {
|
|
|
99
99
|
by: Expression;
|
|
100
100
|
direction?: 'asc' | 'desc';
|
|
101
101
|
}
|
|
102
|
+
/** True when every member satisfies the predicate. An empty collection satisfies it. */
|
|
103
|
+
export interface EveryExpression {
|
|
104
|
+
kind: 'every';
|
|
105
|
+
source: Expression;
|
|
106
|
+
scopeId: NodeId;
|
|
107
|
+
predicate: Expression;
|
|
108
|
+
}
|
|
109
|
+
/** True when at least one member satisfies the predicate. An empty collection does not. */
|
|
110
|
+
export interface SomeExpression {
|
|
111
|
+
kind: 'some';
|
|
112
|
+
source: Expression;
|
|
113
|
+
scopeId: NodeId;
|
|
114
|
+
predicate: Expression;
|
|
115
|
+
}
|
|
116
|
+
/** Collapses one level of nesting: Collection<Collection<T>> becomes Collection<T>. */
|
|
117
|
+
export interface FlattenExpression {
|
|
118
|
+
kind: 'flatten';
|
|
119
|
+
source: Expression;
|
|
120
|
+
}
|
|
121
|
+
/** Chooses between two values. Both branches are expressions, never callbacks. */
|
|
122
|
+
export interface ConditionalExpression {
|
|
123
|
+
kind: 'conditional';
|
|
124
|
+
condition: Expression;
|
|
125
|
+
whenTrue: Expression;
|
|
126
|
+
whenFalse: Expression;
|
|
127
|
+
}
|
|
102
128
|
export declare function literal(value: LiteralValue): LiteralExpression;
|
|
103
129
|
export declare function ref(targetId: NodeId): RefExpression;
|
|
104
130
|
export declare function field(source: Expression, id: FieldId): FieldExpression;
|
|
@@ -113,8 +139,24 @@ export declare function sort(source: Expression, scopeId: NodeId, by: Expression
|
|
|
113
139
|
/** Sums a collection of numbers. An empty collection sums to zero. */
|
|
114
140
|
export declare function sum(source: Expression): CallExpression;
|
|
115
141
|
export declare function count(source: Expression): CallExpression;
|
|
142
|
+
/** True when a value exists at all — an empty collection or string still exists. */
|
|
143
|
+
export declare function required(value: Expression): CallExpression;
|
|
144
|
+
/** True when a collection or string has no members. */
|
|
145
|
+
export declare function isEmpty(value: Expression): CallExpression;
|
|
146
|
+
export declare function nonEmpty(value: Expression): CallExpression;
|
|
147
|
+
/** The first value that exists. Only null and undefined are skipped. */
|
|
148
|
+
export declare function coalesce(...values: Expression[]): CallExpression;
|
|
149
|
+
export declare function every(source: Expression, scopeId: NodeId, predicate: Expression): EveryExpression;
|
|
150
|
+
export declare function some(source: Expression, scopeId: NodeId, predicate: Expression): SomeExpression;
|
|
151
|
+
export declare function flatten(source: Expression): FlattenExpression;
|
|
152
|
+
export declare function conditional(condition: Expression, whenTrue: Expression, whenFalse: Expression): ConditionalExpression;
|
|
116
153
|
/** Visits every sub-expression, parents before children. */
|
|
117
154
|
export declare function walkExpression(expression: Expression, visit: (node: Expression) => void): void;
|
|
155
|
+
/**
|
|
156
|
+
* Field ids a constructed record assigns. Only the record's own entries count: the
|
|
157
|
+
* expressions that compute those values are reads, not writes.
|
|
158
|
+
*/
|
|
159
|
+
export declare function constructedFieldIds(expression: Expression): FieldId[];
|
|
118
160
|
/** Field ids an expression reads, including nested sources and constructed records. */
|
|
119
161
|
export declare function expressionFieldIds(expression: Expression): FieldId[];
|
|
120
162
|
//# sourceMappingURL=expressions.d.ts.map
|
package/dist/expressions.js
CHANGED
|
@@ -11,10 +11,15 @@ export const EXPRESSION_KINDS = [
|
|
|
11
11
|
'find',
|
|
12
12
|
'map',
|
|
13
13
|
'sort',
|
|
14
|
+
'every',
|
|
15
|
+
'some',
|
|
16
|
+
'flatten',
|
|
17
|
+
'conditional',
|
|
14
18
|
];
|
|
15
19
|
export const BUILTIN_FUNCTIONS = [
|
|
16
20
|
'required',
|
|
17
21
|
'is-empty',
|
|
22
|
+
'non-empty',
|
|
18
23
|
'length',
|
|
19
24
|
'contains',
|
|
20
25
|
'concat',
|
|
@@ -69,6 +74,33 @@ export function sum(source) {
|
|
|
69
74
|
export function count(source) {
|
|
70
75
|
return call('count', source);
|
|
71
76
|
}
|
|
77
|
+
/** True when a value exists at all — an empty collection or string still exists. */
|
|
78
|
+
export function required(value) {
|
|
79
|
+
return call('required', value);
|
|
80
|
+
}
|
|
81
|
+
/** True when a collection or string has no members. */
|
|
82
|
+
export function isEmpty(value) {
|
|
83
|
+
return call('is-empty', value);
|
|
84
|
+
}
|
|
85
|
+
export function nonEmpty(value) {
|
|
86
|
+
return call('non-empty', value);
|
|
87
|
+
}
|
|
88
|
+
/** The first value that exists. Only null and undefined are skipped. */
|
|
89
|
+
export function coalesce(...values) {
|
|
90
|
+
return call('coalesce', ...values);
|
|
91
|
+
}
|
|
92
|
+
export function every(source, scopeId, predicate) {
|
|
93
|
+
return { kind: 'every', source, scopeId, predicate };
|
|
94
|
+
}
|
|
95
|
+
export function some(source, scopeId, predicate) {
|
|
96
|
+
return { kind: 'some', source, scopeId, predicate };
|
|
97
|
+
}
|
|
98
|
+
export function flatten(source) {
|
|
99
|
+
return { kind: 'flatten', source };
|
|
100
|
+
}
|
|
101
|
+
export function conditional(condition, whenTrue, whenFalse) {
|
|
102
|
+
return { kind: 'conditional', condition, whenTrue, whenFalse };
|
|
103
|
+
}
|
|
72
104
|
/** Visits every sub-expression, parents before children. */
|
|
73
105
|
export function walkExpression(expression, visit) {
|
|
74
106
|
visit(expression);
|
|
@@ -106,9 +138,29 @@ export function walkExpression(expression, visit) {
|
|
|
106
138
|
walkExpression(expression.source, visit);
|
|
107
139
|
walkExpression(expression.by, visit);
|
|
108
140
|
return;
|
|
141
|
+
case 'every':
|
|
142
|
+
case 'some':
|
|
143
|
+
walkExpression(expression.source, visit);
|
|
144
|
+
walkExpression(expression.predicate, visit);
|
|
145
|
+
return;
|
|
146
|
+
case 'flatten':
|
|
147
|
+
walkExpression(expression.source, visit);
|
|
148
|
+
return;
|
|
149
|
+
case 'conditional':
|
|
150
|
+
walkExpression(expression.condition, visit);
|
|
151
|
+
walkExpression(expression.whenTrue, visit);
|
|
152
|
+
walkExpression(expression.whenFalse, visit);
|
|
153
|
+
return;
|
|
109
154
|
default:
|
|
110
155
|
}
|
|
111
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Field ids a constructed record assigns. Only the record's own entries count: the
|
|
159
|
+
* expressions that compute those values are reads, not writes.
|
|
160
|
+
*/
|
|
161
|
+
export function constructedFieldIds(expression) {
|
|
162
|
+
return expression.kind === 'object' ? expression.entries.map((entry) => entry.fieldId) : [];
|
|
163
|
+
}
|
|
112
164
|
/** Field ids an expression reads, including nested sources and constructed records. */
|
|
113
165
|
export function expressionFieldIds(expression) {
|
|
114
166
|
const found = [];
|
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;
|
|
@@ -25,6 +26,14 @@ export declare class ApplicationGraph {
|
|
|
25
26
|
get id(): string;
|
|
26
27
|
get name(): string;
|
|
27
28
|
get version(): string;
|
|
29
|
+
/**
|
|
30
|
+
* The application's visual identity, completed against the default theme. A theme is
|
|
31
|
+
* presentation only: changing it cannot change an action, a constraint or a route.
|
|
32
|
+
*/
|
|
33
|
+
get theme(): Theme;
|
|
34
|
+
/** Exactly what the application declared, before defaults were filled in. */
|
|
35
|
+
get declaredTheme(): ThemeInput | undefined;
|
|
36
|
+
setTheme(theme: ThemeInput | undefined): void;
|
|
28
37
|
addNode<T extends AnyNode>(node: NodeInput<T>): NodeId;
|
|
29
38
|
getNode<T extends AnyNode = AnyNode>(id: NodeId): T | undefined;
|
|
30
39
|
hasNode(id: NodeId): boolean;
|
package/dist/graph.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
|
}
|
|
@@ -16,7 +17,7 @@ export class ApplicationGraph {
|
|
|
16
17
|
/** Bumped by every change, so the derived edge index can never serve stale data. */
|
|
17
18
|
revision = 0;
|
|
18
19
|
semanticIndex;
|
|
19
|
-
constructor(id, name, version = '0.
|
|
20
|
+
constructor(id, name, version = '0.5.0') {
|
|
20
21
|
this.data = { id, name, version, nodes: {}, edges: {} };
|
|
21
22
|
}
|
|
22
23
|
get id() {
|
|
@@ -28,6 +29,26 @@ export class ApplicationGraph {
|
|
|
28
29
|
get version() {
|
|
29
30
|
return this.data.version;
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The application's visual identity, completed against the default theme. A theme is
|
|
34
|
+
* presentation only: changing it cannot change an action, a constraint or a route.
|
|
35
|
+
*/
|
|
36
|
+
get theme() {
|
|
37
|
+
return resolveTheme(this.data.theme);
|
|
38
|
+
}
|
|
39
|
+
/** Exactly what the application declared, before defaults were filled in. */
|
|
40
|
+
get declaredTheme() {
|
|
41
|
+
return this.data.theme ? structuredClone(this.data.theme) : undefined;
|
|
42
|
+
}
|
|
43
|
+
setTheme(theme) {
|
|
44
|
+
if (theme === undefined) {
|
|
45
|
+
delete this.data.theme;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.data.theme = structuredClone(theme);
|
|
49
|
+
}
|
|
50
|
+
this.revision += 1;
|
|
51
|
+
}
|
|
31
52
|
addNode(node) {
|
|
32
53
|
const id = (node.id ?? createNodeId(node.kind));
|
|
33
54
|
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
|
@@ -141,6 +141,14 @@ export function inferExpressionType(expression, context, scope) {
|
|
|
141
141
|
const item = itemTypeOf(inferExpressionType(expression.source, context, scope));
|
|
142
142
|
return item ? optionalType(item) : undefined;
|
|
143
143
|
}
|
|
144
|
+
case 'every':
|
|
145
|
+
case 'some':
|
|
146
|
+
return primitiveType('boolean');
|
|
147
|
+
case 'flatten':
|
|
148
|
+
return itemTypeOf(inferExpressionType(expression.source, context, scope));
|
|
149
|
+
case 'conditional':
|
|
150
|
+
return (inferExpressionType(expression.whenTrue, context, scope) ??
|
|
151
|
+
inferExpressionType(expression.whenFalse, context, scope));
|
|
144
152
|
case 'map': {
|
|
145
153
|
const item = itemTypeOf(inferExpressionType(expression.source, context, scope));
|
|
146
154
|
const projected = inferExpressionType(expression.projection, context, withScope(scope, expression.scopeId, item));
|
|
@@ -157,6 +165,8 @@ export function scopeForExpression(expression, context, scope) {
|
|
|
157
165
|
case 'find':
|
|
158
166
|
case 'map':
|
|
159
167
|
case 'sort':
|
|
168
|
+
case 'every':
|
|
169
|
+
case 'some':
|
|
160
170
|
return withScope(scope, expression.scopeId, itemTypeOf(inferExpressionType(expression.source, context, scope)));
|
|
161
171
|
default:
|
|
162
172
|
return scope ?? new Map();
|
package/dist/ir.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { FieldId, NodeId } from './ids.js';
|
|
2
|
-
import type { ActionDef, ConstraintDef, EntityDef, GraphEdge, RouteParameter, StateDef } from './nodes.js';
|
|
2
|
+
import type { ActionDef, ConstraintDef, EntityDef, GraphEdge, RouteParameter, StateDef, TransitionConstraintDef } from './nodes.js';
|
|
3
3
|
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;
|
|
@@ -34,6 +36,8 @@ export interface ApplicationIR {
|
|
|
34
36
|
actions: Record<NodeId, ActionDef>;
|
|
35
37
|
uiNodes: Record<NodeId, UINode>;
|
|
36
38
|
constraints: ConstraintDef[];
|
|
39
|
+
/** Rules about how state may change, enforced on every governed mutation path. */
|
|
40
|
+
transitionConstraints: TransitionConstraintDef[];
|
|
37
41
|
routes: CompiledRoute[];
|
|
38
42
|
edges: GraphEdge[];
|
|
39
43
|
/**
|
|
@@ -46,5 +50,21 @@ export interface ApplicationIR {
|
|
|
46
50
|
* to canonical application state from a write to a draft.
|
|
47
51
|
*/
|
|
48
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
|
+
/** The application's visual identity, completed against the default theme. */
|
|
59
|
+
theme: Theme;
|
|
60
|
+
/**
|
|
61
|
+
* Presentation with every question already answered, per UI node: renderer defaults,
|
|
62
|
+
* theme, inheritance, semantic inference, node declaration and responsive overrides all
|
|
63
|
+
* resolved. A renderer reads this and needs to know nothing about how it was decided.
|
|
64
|
+
*
|
|
65
|
+
* It is deliberately still semantic — roles, tokens and device classes, not CSS — so a
|
|
66
|
+
* second renderer remains possible.
|
|
67
|
+
*/
|
|
68
|
+
presentation: Record<NodeId, ResolvedPresentation>;
|
|
49
69
|
}
|
|
50
70
|
//# sourceMappingURL=ir.d.ts.map
|
package/dist/nodes.d.ts
CHANGED
|
@@ -2,12 +2,7 @@ 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;
|
|
@@ -50,6 +45,16 @@ export interface StateDef extends NodeBase {
|
|
|
50
45
|
* definition, so instance validation skips them until an action commits the value.
|
|
51
46
|
*/
|
|
52
47
|
draft?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Marks ephemeral presentation state — which panel is expanded, which tab is selected,
|
|
50
|
+
* whether a dialog is open. It is not canonical domain state: instance validation skips
|
|
51
|
+
* it, and it may not be persisted. Marking it says so in the graph instead of leaving an
|
|
52
|
+
* agent to guess which states are domain facts.
|
|
53
|
+
*
|
|
54
|
+
* It changes what a state *is*, never what is permitted: a write reaching domain state
|
|
55
|
+
* is governed exactly as before.
|
|
56
|
+
*/
|
|
57
|
+
ephemeral?: boolean;
|
|
53
58
|
persistence?: StatePersistence;
|
|
54
59
|
}
|
|
55
60
|
export interface ActionParameter {
|
|
@@ -62,9 +67,16 @@ export interface FailureMode {
|
|
|
62
67
|
code: string;
|
|
63
68
|
message?: string;
|
|
64
69
|
}
|
|
70
|
+
/** A condition together with the failure it reports, so the two cannot drift apart. */
|
|
71
|
+
export interface ActionGuard {
|
|
72
|
+
condition: Expression;
|
|
73
|
+
failureMode?: FailureMode;
|
|
74
|
+
}
|
|
65
75
|
export interface ActionDef extends NodeBase {
|
|
66
76
|
kind: 'action';
|
|
67
77
|
parameters?: ActionParameter[];
|
|
78
|
+
/** Preferred over the parallel `preconditions` and `failureModes` arrays. */
|
|
79
|
+
guards?: ActionGuard[];
|
|
68
80
|
preconditions?: Expression[];
|
|
69
81
|
operations: Operation[];
|
|
70
82
|
postconditions?: Expression[];
|
|
@@ -73,7 +85,14 @@ export interface ActionDef extends NodeBase {
|
|
|
73
85
|
destructive?: boolean;
|
|
74
86
|
requiresConfirmation?: boolean;
|
|
75
87
|
confirmationMessage?: string;
|
|
88
|
+
/** What the confirmation says, when a plain message is not enough. */
|
|
89
|
+
confirmation?: ConfirmationPresentation;
|
|
76
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The conditions an action checks, however they were written. `guards` pairs each
|
|
93
|
+
* condition with its failure; the older parallel arrays are matched by position.
|
|
94
|
+
*/
|
|
95
|
+
export declare function actionGuards(action: ActionDef): ActionGuard[];
|
|
77
96
|
export type Operation = SetOperation | InsertOperation | RemoveOperation | ForEachOperation | InvokeOperation | NavigateOperation | NativeOperation;
|
|
78
97
|
export type OperationKind = Operation['kind'];
|
|
79
98
|
/** Every operation kind the runtime is required to execute. */
|
|
@@ -159,6 +178,29 @@ export interface ConstraintDef extends NodeBase {
|
|
|
159
178
|
severity?: 'error' | 'warning';
|
|
160
179
|
message?: string;
|
|
161
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* A rule about how state may change, rather than about what state may be.
|
|
183
|
+
*
|
|
184
|
+
* An ordinary constraint judges the proposed state on its own; a transition constraint
|
|
185
|
+
* sees the instance as it was when the transaction began *and* as the transaction
|
|
186
|
+
* proposes it. That is what lets a rule like "once confirmed, an order may not change"
|
|
187
|
+
* hold no matter which path attempts the write — an action, an input binding, an
|
|
188
|
+
* iteration, or something added later.
|
|
189
|
+
*
|
|
190
|
+
* `previousScopeId` and `proposedScopeId` bind those two instances for the expression.
|
|
191
|
+
* When the instance is being removed, the proposed scope is bound to nothing.
|
|
192
|
+
*/
|
|
193
|
+
export interface TransitionConstraintDef extends NodeBase {
|
|
194
|
+
kind: 'transition-constraint';
|
|
195
|
+
/** The entity whose transitions are governed. It must have an identity field. */
|
|
196
|
+
entityId: NodeId;
|
|
197
|
+
previousScopeId: NodeId;
|
|
198
|
+
proposedScopeId: NodeId;
|
|
199
|
+
/** Must hold for every governed transition. */
|
|
200
|
+
expression: Expression;
|
|
201
|
+
severity?: 'error' | 'warning';
|
|
202
|
+
message?: string;
|
|
203
|
+
}
|
|
162
204
|
export interface RouteParameter {
|
|
163
205
|
id: NodeId;
|
|
164
206
|
/** Matches the `:name` placeholder in the route path. */
|
package/dist/nodes.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conditions an action checks, however they were written. `guards` pairs each
|
|
3
|
+
* condition with its failure; the older parallel arrays are matched by position.
|
|
4
|
+
*/
|
|
5
|
+
export function actionGuards(action) {
|
|
6
|
+
if (action.guards && action.guards.length > 0) {
|
|
7
|
+
return action.guards;
|
|
8
|
+
}
|
|
9
|
+
return (action.preconditions ?? []).map((condition, index) => ({
|
|
10
|
+
condition,
|
|
11
|
+
...(action.failureModes?.[index] ? { failureMode: action.failureModes[index] } : {}),
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
1
14
|
/** Every operation kind the runtime is required to execute. */
|
|
2
15
|
export const OPERATION_KINDS = [
|
|
3
16
|
'set',
|