@cynodia/axiom-core 0.3.1-alpha.1 → 0.4.1-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/dist/derive-edges.d.ts +11 -7
- package/dist/derive-edges.js +262 -135
- package/dist/diagnostics.d.ts +15 -0
- package/dist/diagnostics.js +11 -0
- package/dist/expressions.d.ts +82 -3
- package/dist/expressions.js +113 -0
- package/dist/graph.d.ts +10 -0
- package/dist/graph.js +43 -3
- package/dist/infer.d.ts +12 -4
- package/dist/infer.js +72 -9
- package/dist/ir.d.ts +3 -1
- package/dist/nodes.d.ts +55 -1
- package/dist/nodes.js +26 -0
- package/dist/types.d.ts +3 -3
- package/dist/validate.js +279 -23
- package/package.json +1 -1
package/dist/derive-edges.d.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type { Expression } from './expressions.js';
|
|
2
2
|
import type { NodeId } from './ids.js';
|
|
3
3
|
import type { GraphEdge } from './nodes.js';
|
|
4
|
+
import type { AnyNode } from './types.js';
|
|
4
5
|
import type { ApplicationGraph } from './graph.js';
|
|
5
6
|
/** Ids a `ref` expression mentions anywhere in the tree. */
|
|
6
7
|
export declare function referencedIds(expression: Expression): NodeId[];
|
|
7
8
|
/**
|
|
8
|
-
* Recomputes the structural edges implied by node definitions. Edges index semantics
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Recomputes the structural edges implied by node definitions. Edges index semantics that
|
|
10
|
+
* already exist in the nodes, so they are derived rather than hand maintained — see
|
|
11
|
+
* `ApplicationGraph.semanticEdges()`, which keeps them current automatically.
|
|
11
12
|
*
|
|
12
|
-
* Write edges carry the fields they touch, so
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
* Write edges carry the fields they touch, so writing one field of a record is
|
|
14
|
+
* distinguishable from writing another.
|
|
15
|
+
*/
|
|
16
|
+
export declare function deriveEdges(nodes: readonly AnyNode[]): GraphEdge[];
|
|
17
|
+
/**
|
|
18
|
+
* Writes the derived edges into the graph. Queries derive them on demand, so this is only
|
|
19
|
+
* needed to materialize edges into serialized graph data.
|
|
16
20
|
*/
|
|
17
21
|
export declare function synchronizeEdges(graph: ApplicationGraph): GraphEdge[];
|
|
18
22
|
//# sourceMappingURL=derive-edges.d.ts.map
|
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. */
|
|
@@ -24,184 +24,300 @@ function entityIdsIn(type) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Recomputes the structural edges implied by node definitions. Edges index semantics
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Recomputes the structural edges implied by node definitions. Edges index semantics that
|
|
28
|
+
* already exist in the nodes, so they are derived rather than hand maintained — see
|
|
29
|
+
* `ApplicationGraph.semanticEdges()`, which keeps them current automatically.
|
|
30
30
|
*
|
|
31
|
-
* Write edges carry the fields they touch, so
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* Edges added by hand (without the `derived` marker) are preserved.
|
|
31
|
+
* Write edges carry the fields they touch, so writing one field of a record is
|
|
32
|
+
* distinguishable from writing another.
|
|
35
33
|
*/
|
|
36
|
-
export function
|
|
37
|
-
for (const edge of graph.listEdges()) {
|
|
38
|
-
if (edge.metadata?.derived === true) {
|
|
39
|
-
graph.removeEdge(edge.id);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const nodes = graph.listNodes();
|
|
34
|
+
export function deriveEdges(nodes) {
|
|
43
35
|
const known = new Set(nodes.map((node) => node.id));
|
|
44
36
|
const states = new Set(nodes.filter((node) => node.kind === 'state').map((node) => node.id));
|
|
45
37
|
const pending = new Map();
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
const scopeStates = new Map();
|
|
38
|
+
// A repeat's template refers to the current item by the repeat node's own id.
|
|
39
|
+
const rootScope = new Map();
|
|
49
40
|
for (const node of nodes) {
|
|
50
41
|
if (node.kind === 'repeat') {
|
|
51
|
-
|
|
42
|
+
rootScope.set(node.id, statesOf(node.source, new Map(), states));
|
|
43
|
+
}
|
|
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]);
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const entityScope = (entityId) => new Map([...rootScope, [entityId, statesByEntity.get(entityId) ?? []]]);
|
|
58
|
+
const link = (from, to, kind, fieldIds = []) => {
|
|
59
|
+
if (from === to || !known.has(from) || !known.has(to)) {
|
|
60
|
+
return;
|
|
57
61
|
}
|
|
58
|
-
|
|
62
|
+
const key = `${from}|${to}|${kind}`;
|
|
63
|
+
const entry = pending.get(key) ?? { from, to, kind, fieldIds: new Set() };
|
|
64
|
+
for (const fieldId of fieldIds) {
|
|
65
|
+
entry.fieldIds.add(fieldId);
|
|
66
|
+
}
|
|
67
|
+
pending.set(key, entry);
|
|
59
68
|
};
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// state's field, so "reads X.name" never over-reports.
|
|
76
|
-
const perState = new Map();
|
|
77
|
-
const referenced = new Set();
|
|
78
|
-
walkExpression(expression, (node) => {
|
|
79
|
-
if (node.kind === 'ref') {
|
|
80
|
-
for (const stateId of resolveStates(node.targetId)) {
|
|
81
|
-
referenced.add(stateId);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (node.kind === 'field' && node.source.kind === 'ref') {
|
|
85
|
-
for (const stateId of resolveStates(node.source.targetId)) {
|
|
86
|
-
const existing = perState.get(stateId) ?? new Set();
|
|
87
|
-
existing.add(node.fieldId);
|
|
88
|
-
perState.set(stateId, existing);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
for (const id of referenced) {
|
|
93
|
-
linker.link(from, id, kind, [...(perState.get(id) ?? [])]);
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
writes(from, location, kind = 'writes', extraFields = []) {
|
|
97
|
-
linker.link(from, locationRootStateId(location), kind, [
|
|
98
|
-
...locationFieldIds(location),
|
|
99
|
-
...extraFields,
|
|
100
|
-
]);
|
|
101
|
-
// Addressing the location is itself a read of whatever the selectors consult.
|
|
102
|
-
for (const expression of locationExpressions(location)) {
|
|
103
|
-
linker.reads(from, expression);
|
|
104
|
-
}
|
|
105
|
-
const selectorFields = locationSelectorFieldIds(location);
|
|
106
|
-
if (selectorFields.length > 0) {
|
|
107
|
-
linker.link(from, locationRootStateId(location), 'reads', selectorFields);
|
|
108
|
-
}
|
|
109
|
-
},
|
|
69
|
+
const reads = (from, expression, scope, kind = 'reads') => {
|
|
70
|
+
for (const [stateId, fieldIds] of collectReads(expression, scope, states)) {
|
|
71
|
+
link(from, stateId, kind, [...fieldIds]);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const writes = (from, location, scope, kind = 'writes', extraFields = []) => {
|
|
75
|
+
link(from, locationRootStateId(location), kind, [...locationFieldIds(location), ...extraFields]);
|
|
76
|
+
// Addressing the location is itself a read of whatever the selectors consult.
|
|
77
|
+
for (const expression of locationExpressions(location)) {
|
|
78
|
+
reads(from, expression, scope);
|
|
79
|
+
}
|
|
80
|
+
const selectorFields = locationSelectorFieldIds(location);
|
|
81
|
+
if (selectorFields.length > 0) {
|
|
82
|
+
link(from, locationRootStateId(location), 'reads', selectorFields);
|
|
83
|
+
}
|
|
110
84
|
};
|
|
111
85
|
for (const node of nodes) {
|
|
112
86
|
if (isUINode(node)) {
|
|
113
|
-
linkUiNode(node,
|
|
87
|
+
linkUiNode(node, { link, reads, writes, scope: rootScope, states });
|
|
114
88
|
continue;
|
|
115
89
|
}
|
|
116
90
|
switch (node.kind) {
|
|
117
91
|
case 'entity':
|
|
118
|
-
|
|
92
|
+
for (const field of node.fields) {
|
|
93
|
+
for (const target of entityIdsIn(field.valueType)) {
|
|
94
|
+
link(node.id, target, 'references', [field.id]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
119
97
|
break;
|
|
120
98
|
case 'state':
|
|
121
|
-
linkState(node,
|
|
99
|
+
linkState(node, link, reads, rootScope);
|
|
122
100
|
break;
|
|
123
101
|
case 'action':
|
|
124
|
-
linkAction(node,
|
|
102
|
+
linkAction(node, { link, reads, writes, scope: rootScope, states });
|
|
125
103
|
break;
|
|
126
104
|
case 'constraint':
|
|
127
|
-
|
|
105
|
+
if (node.entityId) {
|
|
106
|
+
link(node.id, node.entityId, 'constrains', expressionFieldIds(node.expression));
|
|
107
|
+
}
|
|
108
|
+
reads(node.id, node.expression, node.entityId ? entityScope(node.entityId) : rootScope);
|
|
128
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]]));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
129
116
|
case 'route':
|
|
130
|
-
|
|
117
|
+
link(node.id, node.viewId, 'routes-to');
|
|
131
118
|
break;
|
|
132
119
|
default:
|
|
133
120
|
}
|
|
134
121
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
122
|
+
return [...pending.values()].map((entry) => ({
|
|
123
|
+
id: `${entry.from}:${entry.kind}:${entry.to}`,
|
|
124
|
+
from: entry.from,
|
|
125
|
+
to: entry.to,
|
|
126
|
+
kind: entry.kind,
|
|
127
|
+
metadata: {
|
|
128
|
+
derived: true,
|
|
129
|
+
...(entry.fieldIds.size > 0 ? { fieldIds: [...entry.fieldIds] } : {}),
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
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
|
+
*/
|
|
138
|
+
function statesOf(expression, scope, states) {
|
|
139
|
+
switch (expression.kind) {
|
|
140
|
+
case 'ref': {
|
|
141
|
+
const bound = scope.get(expression.targetId);
|
|
142
|
+
if (bound) {
|
|
143
|
+
return [...bound];
|
|
144
|
+
}
|
|
145
|
+
return states.has(expression.targetId) ? [expression.targetId] : [];
|
|
146
|
+
}
|
|
147
|
+
case 'filter':
|
|
148
|
+
case 'find':
|
|
149
|
+
case 'sort':
|
|
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':
|
|
161
|
+
return statesOf(expression.source, scope, states);
|
|
162
|
+
case 'call':
|
|
163
|
+
return [...new Set(expression.arguments.flatMap((argument) => statesOf(argument, scope, states)))];
|
|
164
|
+
default:
|
|
165
|
+
return [];
|
|
142
166
|
}
|
|
143
|
-
return graph.listEdges();
|
|
144
167
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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));
|
|
149
179
|
}
|
|
150
180
|
}
|
|
181
|
+
return found;
|
|
182
|
+
}
|
|
183
|
+
function bind(scope, id, targets) {
|
|
184
|
+
const next = new Map(scope);
|
|
185
|
+
next.set(id, targets);
|
|
186
|
+
return next;
|
|
151
187
|
}
|
|
152
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Reads an expression performs, as state id → fields. Iteration scopes are followed, so
|
|
190
|
+
* projecting a field of each member is recorded as a read of that field of the state the
|
|
191
|
+
* members came from.
|
|
192
|
+
*/
|
|
193
|
+
function collectReads(expression, scope, states, found = new Map()) {
|
|
194
|
+
const record = (stateId, fieldId) => {
|
|
195
|
+
const entry = found.get(stateId) ?? new Set();
|
|
196
|
+
if (fieldId) {
|
|
197
|
+
entry.add(fieldId);
|
|
198
|
+
}
|
|
199
|
+
found.set(stateId, entry);
|
|
200
|
+
};
|
|
201
|
+
switch (expression.kind) {
|
|
202
|
+
case 'ref':
|
|
203
|
+
for (const stateId of statesOf(expression, scope, states)) {
|
|
204
|
+
record(stateId);
|
|
205
|
+
}
|
|
206
|
+
return found;
|
|
207
|
+
case 'field':
|
|
208
|
+
for (const stateId of statesOf(expression.source, scope, states)) {
|
|
209
|
+
record(stateId, expression.fieldId);
|
|
210
|
+
}
|
|
211
|
+
collectReads(expression.source, scope, states, found);
|
|
212
|
+
return found;
|
|
213
|
+
case 'object':
|
|
214
|
+
for (const entry of expression.entries) {
|
|
215
|
+
collectReads(entry.value, scope, states, found);
|
|
216
|
+
}
|
|
217
|
+
return found;
|
|
218
|
+
case 'binary':
|
|
219
|
+
collectReads(expression.left, scope, states, found);
|
|
220
|
+
collectReads(expression.right, scope, states, found);
|
|
221
|
+
return found;
|
|
222
|
+
case 'unary':
|
|
223
|
+
collectReads(expression.operand, scope, states, found);
|
|
224
|
+
return found;
|
|
225
|
+
case 'call':
|
|
226
|
+
for (const argument of expression.arguments) {
|
|
227
|
+
collectReads(argument, scope, states, found);
|
|
228
|
+
}
|
|
229
|
+
return found;
|
|
230
|
+
case 'filter':
|
|
231
|
+
case 'find':
|
|
232
|
+
case 'map':
|
|
233
|
+
case 'sort':
|
|
234
|
+
case 'every':
|
|
235
|
+
case 'some': {
|
|
236
|
+
collectReads(expression.source, scope, states, found);
|
|
237
|
+
const inner = bind(scope, expression.scopeId, statesOf(expression.source, scope, states));
|
|
238
|
+
const body = expression.kind === 'map'
|
|
239
|
+
? expression.projection
|
|
240
|
+
: expression.kind === 'sort'
|
|
241
|
+
? expression.by
|
|
242
|
+
: expression.predicate;
|
|
243
|
+
collectReads(body, inner, states, found);
|
|
244
|
+
return found;
|
|
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;
|
|
254
|
+
default:
|
|
255
|
+
return found;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function linkState(state, link, reads, scope) {
|
|
153
259
|
for (const target of entityIdsIn(state.valueType)) {
|
|
154
|
-
|
|
260
|
+
link(state.id, target, 'references');
|
|
155
261
|
}
|
|
156
262
|
if (state.derivation) {
|
|
157
|
-
|
|
263
|
+
reads(state.id, state.derivation, scope, 'derives-from');
|
|
158
264
|
}
|
|
159
265
|
}
|
|
160
266
|
function linkAction(action, linker) {
|
|
161
267
|
for (const expression of [...(action.preconditions ?? []), ...(action.postconditions ?? [])]) {
|
|
162
|
-
linker.reads(action.id, expression);
|
|
268
|
+
linker.reads(action.id, expression, linker.scope);
|
|
163
269
|
}
|
|
164
|
-
|
|
270
|
+
linkOperations(action.id, action.operations ?? [], linker, linker.scope);
|
|
271
|
+
}
|
|
272
|
+
function linkOperations(actionId, operations, linker, scope) {
|
|
273
|
+
for (const operation of operations) {
|
|
165
274
|
switch (operation.kind) {
|
|
166
275
|
case 'set':
|
|
167
|
-
linker.writes(
|
|
168
|
-
linker.reads(
|
|
276
|
+
linker.writes(actionId, operation.target, scope);
|
|
277
|
+
linker.reads(actionId, operation.value, scope);
|
|
169
278
|
break;
|
|
170
279
|
case 'insert':
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
linker.
|
|
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));
|
|
283
|
+
linker.reads(actionId, operation.value, scope);
|
|
174
284
|
break;
|
|
175
285
|
case 'remove':
|
|
176
|
-
linker.writes(
|
|
286
|
+
linker.writes(actionId, operation.target, scope);
|
|
287
|
+
break;
|
|
288
|
+
case 'for-each': {
|
|
289
|
+
linker.reads(actionId, operation.collection, scope);
|
|
290
|
+
const inner = bind(scope, operation.scopeId, statesOf(operation.collection, scope, linker.states));
|
|
291
|
+
linkOperations(actionId, operation.operations, linker, inner);
|
|
177
292
|
break;
|
|
293
|
+
}
|
|
178
294
|
case 'invoke':
|
|
179
|
-
linker.link(
|
|
295
|
+
linker.link(actionId, operation.actionId, 'depends-on');
|
|
180
296
|
for (const argument of Object.values(operation.arguments ?? {})) {
|
|
181
|
-
linker.reads(
|
|
297
|
+
linker.reads(actionId, argument, scope);
|
|
182
298
|
}
|
|
183
299
|
break;
|
|
184
300
|
case 'navigate':
|
|
185
301
|
if (operation.routeId) {
|
|
186
|
-
linker.link(
|
|
302
|
+
linker.link(actionId, operation.routeId, 'depends-on');
|
|
187
303
|
}
|
|
188
304
|
for (const argument of Object.values(operation.parameters ?? {})) {
|
|
189
|
-
linker.reads(
|
|
305
|
+
linker.reads(actionId, argument, scope);
|
|
190
306
|
}
|
|
191
307
|
break;
|
|
192
308
|
case 'native':
|
|
193
309
|
for (const input of Object.values(operation.inputs ?? {})) {
|
|
194
|
-
linker.reads(
|
|
310
|
+
linker.reads(actionId, input, scope);
|
|
195
311
|
}
|
|
196
312
|
if (operation.resultTarget) {
|
|
197
|
-
linker.writes(
|
|
313
|
+
linker.writes(actionId, operation.resultTarget, scope);
|
|
198
314
|
}
|
|
199
315
|
for (const effect of operation.declaredEffects ?? []) {
|
|
200
316
|
if (effect.kind === 'reads-state') {
|
|
201
|
-
linker.link(
|
|
317
|
+
linker.link(actionId, effect.stateId, 'reads');
|
|
202
318
|
}
|
|
203
319
|
if (effect.kind === 'writes-state') {
|
|
204
|
-
linker.link(
|
|
320
|
+
linker.link(actionId, effect.stateId, 'writes');
|
|
205
321
|
}
|
|
206
322
|
}
|
|
207
323
|
break;
|
|
@@ -209,74 +325,85 @@ function linkAction(action, linker) {
|
|
|
209
325
|
}
|
|
210
326
|
}
|
|
211
327
|
}
|
|
212
|
-
function linkConstraint(constraint, linker) {
|
|
213
|
-
if (constraint.entityId) {
|
|
214
|
-
linker.link(constraint.id, constraint.entityId, 'constrains', expressionFieldIds(constraint.expression));
|
|
215
|
-
}
|
|
216
|
-
linker.reads(constraint.id, constraint.expression);
|
|
217
|
-
}
|
|
218
328
|
function linkUiNode(node, linker) {
|
|
329
|
+
const { link, reads, writes, scope } = linker;
|
|
219
330
|
if (node.visibleWhen) {
|
|
220
|
-
|
|
331
|
+
reads(node.id, node.visibleWhen, scope);
|
|
221
332
|
}
|
|
222
333
|
switch (node.kind) {
|
|
223
334
|
case 'view':
|
|
224
335
|
case 'container':
|
|
225
336
|
for (const childId of node.children) {
|
|
226
|
-
|
|
337
|
+
link(node.id, childId, 'contains');
|
|
227
338
|
}
|
|
228
339
|
return;
|
|
229
340
|
case 'form':
|
|
230
341
|
for (const childId of node.children) {
|
|
231
|
-
|
|
342
|
+
link(node.id, childId, 'contains');
|
|
232
343
|
}
|
|
233
|
-
|
|
344
|
+
reads(node.id, node.target, scope);
|
|
234
345
|
if (node.submitActionId) {
|
|
235
|
-
|
|
346
|
+
link(node.id, node.submitActionId, 'invokes');
|
|
236
347
|
}
|
|
237
348
|
return;
|
|
238
349
|
case 'conditional':
|
|
239
350
|
for (const childId of [...node.whenTrue, ...(node.whenFalse ?? [])]) {
|
|
240
|
-
|
|
351
|
+
link(node.id, childId, 'contains');
|
|
241
352
|
}
|
|
242
|
-
|
|
353
|
+
reads(node.id, node.condition, scope);
|
|
243
354
|
return;
|
|
244
355
|
case 'repeat':
|
|
245
|
-
|
|
356
|
+
link(node.id, node.templateId, 'renders');
|
|
246
357
|
if (node.emptyTemplateId) {
|
|
247
|
-
|
|
358
|
+
link(node.id, node.emptyTemplateId, 'renders');
|
|
248
359
|
}
|
|
249
|
-
|
|
360
|
+
reads(node.id, node.source, scope);
|
|
250
361
|
return;
|
|
251
362
|
case 'text':
|
|
252
363
|
if (typeof node.value !== 'string') {
|
|
253
|
-
|
|
364
|
+
reads(node.id, node.value, scope);
|
|
254
365
|
}
|
|
255
366
|
return;
|
|
256
367
|
case 'field-display':
|
|
257
|
-
for (const
|
|
258
|
-
|
|
368
|
+
for (const stateId of statesOf(node.source, scope, linker.states)) {
|
|
369
|
+
link(node.id, stateId, 'reads', [node.fieldId]);
|
|
259
370
|
}
|
|
371
|
+
reads(node.id, node.source, scope);
|
|
260
372
|
return;
|
|
261
373
|
case 'input':
|
|
262
374
|
// An input both reads and writes the location it is bound to.
|
|
263
375
|
if (node.binding?.location) {
|
|
264
|
-
|
|
265
|
-
|
|
376
|
+
writes(node.id, node.binding.location, scope, 'binds');
|
|
377
|
+
writes(node.id, node.binding.location, scope, 'writes');
|
|
266
378
|
}
|
|
267
379
|
if (node.options) {
|
|
268
|
-
|
|
380
|
+
reads(node.id, node.options.source, scope);
|
|
269
381
|
}
|
|
270
382
|
return;
|
|
271
383
|
case 'button':
|
|
272
|
-
|
|
384
|
+
link(node.id, node.actionId, 'invokes');
|
|
273
385
|
if (typeof node.label !== 'string') {
|
|
274
|
-
|
|
386
|
+
reads(node.id, node.label, scope);
|
|
275
387
|
}
|
|
276
388
|
for (const argument of Object.values(node.arguments ?? {})) {
|
|
277
|
-
|
|
389
|
+
reads(node.id, argument, scope);
|
|
278
390
|
}
|
|
279
391
|
return;
|
|
280
392
|
default:
|
|
281
393
|
}
|
|
282
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Writes the derived edges into the graph. Queries derive them on demand, so this is only
|
|
397
|
+
* needed to materialize edges into serialized graph data.
|
|
398
|
+
*/
|
|
399
|
+
export function synchronizeEdges(graph) {
|
|
400
|
+
for (const edge of graph.listEdges()) {
|
|
401
|
+
if (edge.metadata?.derived === true) {
|
|
402
|
+
graph.removeEdge(edge.id);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const edge of deriveEdges(graph.listNodes())) {
|
|
406
|
+
graph.addEdge(edge.from, edge.to, edge.kind, { id: edge.id, metadata: edge.metadata });
|
|
407
|
+
}
|
|
408
|
+
return graph.listEdges();
|
|
409
|
+
}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface ValidationIssue {
|
|
|
5
5
|
nodeId?: NodeId;
|
|
6
6
|
fieldId?: FieldId;
|
|
7
7
|
edgeId?: EdgeId;
|
|
8
|
+
/** Where inside a value the problem is, such as `state_orders[2].field_lines[1]`. */
|
|
9
|
+
path?: string;
|
|
10
|
+
/** Structured context an agent can act on without parsing the message. */
|
|
11
|
+
details?: Record<string, unknown>;
|
|
8
12
|
}
|
|
9
13
|
export interface ValidationResult {
|
|
10
14
|
valid: boolean;
|
|
@@ -35,5 +39,16 @@ export declare const VALIDATION_CODES: {
|
|
|
35
39
|
readonly identityFieldMismatch: "IDENTITY_FIELD_MISMATCH";
|
|
36
40
|
readonly assignmentTypeMismatch: "ASSIGNMENT_TYPE_MISMATCH";
|
|
37
41
|
readonly invalidSelectorType: "INVALID_SELECTOR_TYPE";
|
|
42
|
+
readonly unsupportedExpression: "UNSUPPORTED_EXPRESSION";
|
|
43
|
+
readonly unsupportedOperation: "UNSUPPORTED_OPERATION";
|
|
44
|
+
readonly unsupportedConstraintScope: "UNSUPPORTED_CONSTRAINT_SCOPE";
|
|
45
|
+
readonly invalidAggregation: "INVALID_AGGREGATION";
|
|
46
|
+
readonly notACollection: "NOT_A_COLLECTION";
|
|
47
|
+
readonly initialValueTypeMismatch: "INITIAL_VALUE_TYPE_MISMATCH";
|
|
48
|
+
readonly initialValueUnknownField: "INITIAL_VALUE_UNKNOWN_FIELD";
|
|
49
|
+
readonly initialValueMissingRequiredField: "INITIAL_VALUE_MISSING_REQUIRED_FIELD";
|
|
50
|
+
readonly initialValueInvalidEntity: "INITIAL_VALUE_INVALID_ENTITY";
|
|
51
|
+
readonly scopeShadowing: "SCOPE_SHADOWING";
|
|
52
|
+
readonly scopeCollidesWithNode: "SCOPE_COLLIDES_WITH_NODE";
|
|
38
53
|
};
|
|
39
54
|
//# sourceMappingURL=diagnostics.d.ts.map
|
package/dist/diagnostics.js
CHANGED
|
@@ -22,4 +22,15 @@ export const VALIDATION_CODES = {
|
|
|
22
22
|
identityFieldMismatch: 'IDENTITY_FIELD_MISMATCH',
|
|
23
23
|
assignmentTypeMismatch: 'ASSIGNMENT_TYPE_MISMATCH',
|
|
24
24
|
invalidSelectorType: 'INVALID_SELECTOR_TYPE',
|
|
25
|
+
unsupportedExpression: 'UNSUPPORTED_EXPRESSION',
|
|
26
|
+
unsupportedOperation: 'UNSUPPORTED_OPERATION',
|
|
27
|
+
unsupportedConstraintScope: 'UNSUPPORTED_CONSTRAINT_SCOPE',
|
|
28
|
+
invalidAggregation: 'INVALID_AGGREGATION',
|
|
29
|
+
notACollection: 'NOT_A_COLLECTION',
|
|
30
|
+
initialValueTypeMismatch: 'INITIAL_VALUE_TYPE_MISMATCH',
|
|
31
|
+
initialValueUnknownField: 'INITIAL_VALUE_UNKNOWN_FIELD',
|
|
32
|
+
initialValueMissingRequiredField: 'INITIAL_VALUE_MISSING_REQUIRED_FIELD',
|
|
33
|
+
initialValueInvalidEntity: 'INITIAL_VALUE_INVALID_ENTITY',
|
|
34
|
+
scopeShadowing: 'SCOPE_SHADOWING',
|
|
35
|
+
scopeCollidesWithNode: 'SCOPE_COLLIDES_WITH_NODE',
|
|
25
36
|
};
|