@cynodia/axiom-core 0.3.1-alpha.1 → 0.4.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/dist/derive-edges.d.ts +11 -7
- package/dist/derive-edges.js +199 -133
- package/dist/diagnostics.d.ts +13 -0
- package/dist/diagnostics.js +9 -0
- package/dist/expressions.d.ts +39 -2
- package/dist/expressions.js +61 -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 +62 -9
- package/dist/nodes.d.ts +20 -1
- package/dist/nodes.js +13 -0
- package/dist/validate.js +194 -21
- 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
|
@@ -24,184 +24,239 @@ 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));
|
|
52
43
|
}
|
|
53
44
|
}
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
return
|
|
45
|
+
const link = (from, to, kind, fieldIds = []) => {
|
|
46
|
+
if (from === to || !known.has(from) || !known.has(to)) {
|
|
47
|
+
return;
|
|
57
48
|
}
|
|
58
|
-
|
|
49
|
+
const key = `${from}|${to}|${kind}`;
|
|
50
|
+
const entry = pending.get(key) ?? { from, to, kind, fieldIds: new Set() };
|
|
51
|
+
for (const fieldId of fieldIds) {
|
|
52
|
+
entry.fieldIds.add(fieldId);
|
|
53
|
+
}
|
|
54
|
+
pending.set(key, entry);
|
|
59
55
|
};
|
|
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
|
-
},
|
|
56
|
+
const reads = (from, expression, scope, kind = 'reads') => {
|
|
57
|
+
for (const [stateId, fieldIds] of collectReads(expression, scope, states)) {
|
|
58
|
+
link(from, stateId, kind, [...fieldIds]);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const writes = (from, location, scope, kind = 'writes', extraFields = []) => {
|
|
62
|
+
link(from, locationRootStateId(location), kind, [...locationFieldIds(location), ...extraFields]);
|
|
63
|
+
// Addressing the location is itself a read of whatever the selectors consult.
|
|
64
|
+
for (const expression of locationExpressions(location)) {
|
|
65
|
+
reads(from, expression, scope);
|
|
66
|
+
}
|
|
67
|
+
const selectorFields = locationSelectorFieldIds(location);
|
|
68
|
+
if (selectorFields.length > 0) {
|
|
69
|
+
link(from, locationRootStateId(location), 'reads', selectorFields);
|
|
70
|
+
}
|
|
110
71
|
};
|
|
111
72
|
for (const node of nodes) {
|
|
112
73
|
if (isUINode(node)) {
|
|
113
|
-
linkUiNode(node,
|
|
74
|
+
linkUiNode(node, { link, reads, writes, scope: rootScope, states });
|
|
114
75
|
continue;
|
|
115
76
|
}
|
|
116
77
|
switch (node.kind) {
|
|
117
78
|
case 'entity':
|
|
118
|
-
|
|
79
|
+
for (const field of node.fields) {
|
|
80
|
+
for (const target of entityIdsIn(field.valueType)) {
|
|
81
|
+
link(node.id, target, 'references', [field.id]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
119
84
|
break;
|
|
120
85
|
case 'state':
|
|
121
|
-
linkState(node,
|
|
86
|
+
linkState(node, link, reads, rootScope);
|
|
122
87
|
break;
|
|
123
88
|
case 'action':
|
|
124
|
-
linkAction(node,
|
|
89
|
+
linkAction(node, { link, reads, writes, scope: rootScope, states });
|
|
125
90
|
break;
|
|
126
91
|
case 'constraint':
|
|
127
|
-
|
|
92
|
+
if (node.entityId) {
|
|
93
|
+
link(node.id, node.entityId, 'constrains', expressionFieldIds(node.expression));
|
|
94
|
+
}
|
|
95
|
+
reads(node.id, node.expression, rootScope);
|
|
128
96
|
break;
|
|
129
97
|
case 'route':
|
|
130
|
-
|
|
98
|
+
link(node.id, node.viewId, 'routes-to');
|
|
131
99
|
break;
|
|
132
100
|
default:
|
|
133
101
|
}
|
|
134
102
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
103
|
+
return [...pending.values()].map((entry) => ({
|
|
104
|
+
id: `${entry.from}:${entry.kind}:${entry.to}`,
|
|
105
|
+
from: entry.from,
|
|
106
|
+
to: entry.to,
|
|
107
|
+
kind: entry.kind,
|
|
108
|
+
metadata: {
|
|
109
|
+
derived: true,
|
|
110
|
+
...(entry.fieldIds.size > 0 ? { fieldIds: [...entry.fieldIds] } : {}),
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/** The states an expression ultimately draws its members from. */
|
|
115
|
+
function statesOf(expression, scope, states) {
|
|
116
|
+
switch (expression.kind) {
|
|
117
|
+
case 'ref': {
|
|
118
|
+
const bound = scope.get(expression.targetId);
|
|
119
|
+
if (bound) {
|
|
120
|
+
return [...bound];
|
|
121
|
+
}
|
|
122
|
+
return states.has(expression.targetId) ? [expression.targetId] : [];
|
|
123
|
+
}
|
|
124
|
+
case 'filter':
|
|
125
|
+
case 'find':
|
|
126
|
+
case 'sort':
|
|
127
|
+
case 'map':
|
|
128
|
+
return statesOf(expression.source, scope, states);
|
|
129
|
+
default:
|
|
130
|
+
return [];
|
|
142
131
|
}
|
|
143
|
-
return graph.listEdges();
|
|
144
132
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
133
|
+
function bind(scope, id, targets) {
|
|
134
|
+
const next = new Map(scope);
|
|
135
|
+
next.set(id, targets);
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reads an expression performs, as state id → fields. Iteration scopes are followed, so
|
|
140
|
+
* projecting a field of each member is recorded as a read of that field of the state the
|
|
141
|
+
* members came from.
|
|
142
|
+
*/
|
|
143
|
+
function collectReads(expression, scope, states, found = new Map()) {
|
|
144
|
+
const record = (stateId, fieldId) => {
|
|
145
|
+
const entry = found.get(stateId) ?? new Set();
|
|
146
|
+
if (fieldId) {
|
|
147
|
+
entry.add(fieldId);
|
|
149
148
|
}
|
|
149
|
+
found.set(stateId, entry);
|
|
150
|
+
};
|
|
151
|
+
switch (expression.kind) {
|
|
152
|
+
case 'ref':
|
|
153
|
+
for (const stateId of statesOf(expression, scope, states)) {
|
|
154
|
+
record(stateId);
|
|
155
|
+
}
|
|
156
|
+
return found;
|
|
157
|
+
case 'field':
|
|
158
|
+
for (const stateId of statesOf(expression.source, scope, states)) {
|
|
159
|
+
record(stateId, expression.fieldId);
|
|
160
|
+
}
|
|
161
|
+
collectReads(expression.source, scope, states, found);
|
|
162
|
+
return found;
|
|
163
|
+
case 'object':
|
|
164
|
+
for (const entry of expression.entries) {
|
|
165
|
+
collectReads(entry.value, scope, states, found);
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
case 'binary':
|
|
169
|
+
collectReads(expression.left, scope, states, found);
|
|
170
|
+
collectReads(expression.right, scope, states, found);
|
|
171
|
+
return found;
|
|
172
|
+
case 'unary':
|
|
173
|
+
collectReads(expression.operand, scope, states, found);
|
|
174
|
+
return found;
|
|
175
|
+
case 'call':
|
|
176
|
+
for (const argument of expression.arguments) {
|
|
177
|
+
collectReads(argument, scope, states, found);
|
|
178
|
+
}
|
|
179
|
+
return found;
|
|
180
|
+
case 'filter':
|
|
181
|
+
case 'find':
|
|
182
|
+
case 'map':
|
|
183
|
+
case 'sort': {
|
|
184
|
+
collectReads(expression.source, scope, states, found);
|
|
185
|
+
const inner = bind(scope, expression.scopeId, statesOf(expression.source, scope, states));
|
|
186
|
+
const body = expression.kind === 'map'
|
|
187
|
+
? expression.projection
|
|
188
|
+
: expression.kind === 'sort'
|
|
189
|
+
? expression.by
|
|
190
|
+
: expression.predicate;
|
|
191
|
+
collectReads(body, inner, states, found);
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return found;
|
|
150
196
|
}
|
|
151
197
|
}
|
|
152
|
-
function linkState(state,
|
|
198
|
+
function linkState(state, link, reads, scope) {
|
|
153
199
|
for (const target of entityIdsIn(state.valueType)) {
|
|
154
|
-
|
|
200
|
+
link(state.id, target, 'references');
|
|
155
201
|
}
|
|
156
202
|
if (state.derivation) {
|
|
157
|
-
|
|
203
|
+
reads(state.id, state.derivation, scope, 'derives-from');
|
|
158
204
|
}
|
|
159
205
|
}
|
|
160
206
|
function linkAction(action, linker) {
|
|
161
207
|
for (const expression of [...(action.preconditions ?? []), ...(action.postconditions ?? [])]) {
|
|
162
|
-
linker.reads(action.id, expression);
|
|
208
|
+
linker.reads(action.id, expression, linker.scope);
|
|
163
209
|
}
|
|
164
|
-
|
|
210
|
+
linkOperations(action.id, action.operations ?? [], linker, linker.scope);
|
|
211
|
+
}
|
|
212
|
+
function linkOperations(actionId, operations, linker, scope) {
|
|
213
|
+
for (const operation of operations) {
|
|
165
214
|
switch (operation.kind) {
|
|
166
215
|
case 'set':
|
|
167
|
-
linker.writes(
|
|
168
|
-
linker.reads(
|
|
216
|
+
linker.writes(actionId, operation.target, scope);
|
|
217
|
+
linker.reads(actionId, operation.value, scope);
|
|
169
218
|
break;
|
|
170
219
|
case 'insert':
|
|
171
220
|
// Inserting a constructed record writes every field the record declares.
|
|
172
|
-
linker.writes(
|
|
173
|
-
linker.reads(
|
|
221
|
+
linker.writes(actionId, operation.target, scope, 'writes', expressionFieldIds(operation.value));
|
|
222
|
+
linker.reads(actionId, operation.value, scope);
|
|
174
223
|
break;
|
|
175
224
|
case 'remove':
|
|
176
|
-
linker.writes(
|
|
225
|
+
linker.writes(actionId, operation.target, scope);
|
|
177
226
|
break;
|
|
227
|
+
case 'for-each': {
|
|
228
|
+
linker.reads(actionId, operation.collection, scope);
|
|
229
|
+
const inner = bind(scope, operation.scopeId, statesOf(operation.collection, scope, linker.states));
|
|
230
|
+
linkOperations(actionId, operation.operations, linker, inner);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
178
233
|
case 'invoke':
|
|
179
|
-
linker.link(
|
|
234
|
+
linker.link(actionId, operation.actionId, 'depends-on');
|
|
180
235
|
for (const argument of Object.values(operation.arguments ?? {})) {
|
|
181
|
-
linker.reads(
|
|
236
|
+
linker.reads(actionId, argument, scope);
|
|
182
237
|
}
|
|
183
238
|
break;
|
|
184
239
|
case 'navigate':
|
|
185
240
|
if (operation.routeId) {
|
|
186
|
-
linker.link(
|
|
241
|
+
linker.link(actionId, operation.routeId, 'depends-on');
|
|
187
242
|
}
|
|
188
243
|
for (const argument of Object.values(operation.parameters ?? {})) {
|
|
189
|
-
linker.reads(
|
|
244
|
+
linker.reads(actionId, argument, scope);
|
|
190
245
|
}
|
|
191
246
|
break;
|
|
192
247
|
case 'native':
|
|
193
248
|
for (const input of Object.values(operation.inputs ?? {})) {
|
|
194
|
-
linker.reads(
|
|
249
|
+
linker.reads(actionId, input, scope);
|
|
195
250
|
}
|
|
196
251
|
if (operation.resultTarget) {
|
|
197
|
-
linker.writes(
|
|
252
|
+
linker.writes(actionId, operation.resultTarget, scope);
|
|
198
253
|
}
|
|
199
254
|
for (const effect of operation.declaredEffects ?? []) {
|
|
200
255
|
if (effect.kind === 'reads-state') {
|
|
201
|
-
linker.link(
|
|
256
|
+
linker.link(actionId, effect.stateId, 'reads');
|
|
202
257
|
}
|
|
203
258
|
if (effect.kind === 'writes-state') {
|
|
204
|
-
linker.link(
|
|
259
|
+
linker.link(actionId, effect.stateId, 'writes');
|
|
205
260
|
}
|
|
206
261
|
}
|
|
207
262
|
break;
|
|
@@ -209,74 +264,85 @@ function linkAction(action, linker) {
|
|
|
209
264
|
}
|
|
210
265
|
}
|
|
211
266
|
}
|
|
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
267
|
function linkUiNode(node, linker) {
|
|
268
|
+
const { link, reads, writes, scope } = linker;
|
|
219
269
|
if (node.visibleWhen) {
|
|
220
|
-
|
|
270
|
+
reads(node.id, node.visibleWhen, scope);
|
|
221
271
|
}
|
|
222
272
|
switch (node.kind) {
|
|
223
273
|
case 'view':
|
|
224
274
|
case 'container':
|
|
225
275
|
for (const childId of node.children) {
|
|
226
|
-
|
|
276
|
+
link(node.id, childId, 'contains');
|
|
227
277
|
}
|
|
228
278
|
return;
|
|
229
279
|
case 'form':
|
|
230
280
|
for (const childId of node.children) {
|
|
231
|
-
|
|
281
|
+
link(node.id, childId, 'contains');
|
|
232
282
|
}
|
|
233
|
-
|
|
283
|
+
reads(node.id, node.target, scope);
|
|
234
284
|
if (node.submitActionId) {
|
|
235
|
-
|
|
285
|
+
link(node.id, node.submitActionId, 'invokes');
|
|
236
286
|
}
|
|
237
287
|
return;
|
|
238
288
|
case 'conditional':
|
|
239
289
|
for (const childId of [...node.whenTrue, ...(node.whenFalse ?? [])]) {
|
|
240
|
-
|
|
290
|
+
link(node.id, childId, 'contains');
|
|
241
291
|
}
|
|
242
|
-
|
|
292
|
+
reads(node.id, node.condition, scope);
|
|
243
293
|
return;
|
|
244
294
|
case 'repeat':
|
|
245
|
-
|
|
295
|
+
link(node.id, node.templateId, 'renders');
|
|
246
296
|
if (node.emptyTemplateId) {
|
|
247
|
-
|
|
297
|
+
link(node.id, node.emptyTemplateId, 'renders');
|
|
248
298
|
}
|
|
249
|
-
|
|
299
|
+
reads(node.id, node.source, scope);
|
|
250
300
|
return;
|
|
251
301
|
case 'text':
|
|
252
302
|
if (typeof node.value !== 'string') {
|
|
253
|
-
|
|
303
|
+
reads(node.id, node.value, scope);
|
|
254
304
|
}
|
|
255
305
|
return;
|
|
256
306
|
case 'field-display':
|
|
257
|
-
for (const
|
|
258
|
-
|
|
307
|
+
for (const stateId of statesOf(node.source, scope, linker.states)) {
|
|
308
|
+
link(node.id, stateId, 'reads', [node.fieldId]);
|
|
259
309
|
}
|
|
310
|
+
reads(node.id, node.source, scope);
|
|
260
311
|
return;
|
|
261
312
|
case 'input':
|
|
262
313
|
// An input both reads and writes the location it is bound to.
|
|
263
314
|
if (node.binding?.location) {
|
|
264
|
-
|
|
265
|
-
|
|
315
|
+
writes(node.id, node.binding.location, scope, 'binds');
|
|
316
|
+
writes(node.id, node.binding.location, scope, 'writes');
|
|
266
317
|
}
|
|
267
318
|
if (node.options) {
|
|
268
|
-
|
|
319
|
+
reads(node.id, node.options.source, scope);
|
|
269
320
|
}
|
|
270
321
|
return;
|
|
271
322
|
case 'button':
|
|
272
|
-
|
|
323
|
+
link(node.id, node.actionId, 'invokes');
|
|
273
324
|
if (typeof node.label !== 'string') {
|
|
274
|
-
|
|
325
|
+
reads(node.id, node.label, scope);
|
|
275
326
|
}
|
|
276
327
|
for (const argument of Object.values(node.arguments ?? {})) {
|
|
277
|
-
|
|
328
|
+
reads(node.id, argument, scope);
|
|
278
329
|
}
|
|
279
330
|
return;
|
|
280
331
|
default:
|
|
281
332
|
}
|
|
282
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Writes the derived edges into the graph. Queries derive them on demand, so this is only
|
|
336
|
+
* needed to materialize edges into serialized graph data.
|
|
337
|
+
*/
|
|
338
|
+
export function synchronizeEdges(graph) {
|
|
339
|
+
for (const edge of graph.listEdges()) {
|
|
340
|
+
if (edge.metadata?.derived === true) {
|
|
341
|
+
graph.removeEdge(edge.id);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const edge of deriveEdges(graph.listNodes())) {
|
|
345
|
+
graph.addEdge(edge.from, edge.to, edge.kind, { id: edge.id, metadata: edge.metadata });
|
|
346
|
+
}
|
|
347
|
+
return graph.listEdges();
|
|
348
|
+
}
|
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,14 @@ 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";
|
|
38
51
|
};
|
|
39
52
|
//# sourceMappingURL=diagnostics.d.ts.map
|
package/dist/diagnostics.js
CHANGED
|
@@ -22,4 +22,13 @@ 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',
|
|
25
34
|
};
|
package/dist/expressions.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ 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;
|
|
8
|
+
export type Expression = LiteralExpression | RefExpression | FieldExpression | ObjectExpression | BinaryExpression | UnaryExpression | CallExpression | FilterExpression | FindExpression | MapExpression | SortExpression;
|
|
9
|
+
export type ExpressionKind = Expression['kind'];
|
|
10
|
+
/** Every expression kind the runtime is required to evaluate. */
|
|
11
|
+
export declare const EXPRESSION_KINDS: readonly ExpressionKind[];
|
|
9
12
|
export type LiteralPrimitive = string | number | boolean | null;
|
|
10
13
|
/** Literal data. Structured values are allowed; executable text is not. */
|
|
11
14
|
export interface LiteralExpression {
|
|
@@ -49,8 +52,15 @@ export interface UnaryExpression {
|
|
|
49
52
|
operator: UnaryOperator;
|
|
50
53
|
operand: Expression;
|
|
51
54
|
}
|
|
52
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* The built-in function vocabulary. Deliberately small and domain-neutral. Every entry
|
|
57
|
+
* must be implemented by the runtime: a function that is declared here but unevaluated
|
|
58
|
+
* would be a construct that typechecks, validates and then does nothing.
|
|
59
|
+
*/
|
|
53
60
|
export type BuiltinFunction = 'required' | 'is-empty' | 'length' | 'contains' | 'concat' | 'coalesce' | 'one-of' | 'count' | 'sum' | 'lowercase' | 'to-string' | 'now' | 'uuid';
|
|
61
|
+
export declare const BUILTIN_FUNCTIONS: readonly BuiltinFunction[];
|
|
62
|
+
/** Functions that reduce a collection of numbers to a number. */
|
|
63
|
+
export declare const AGGREGATE_FUNCTIONS: readonly BuiltinFunction[];
|
|
54
64
|
export interface CallExpression {
|
|
55
65
|
kind: 'call';
|
|
56
66
|
function: BuiltinFunction;
|
|
@@ -70,12 +80,39 @@ export interface FindExpression {
|
|
|
70
80
|
scopeId: NodeId;
|
|
71
81
|
predicate: Expression;
|
|
72
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Projects every member of a collection. `scopeId` introduces an iteration scope, so the
|
|
85
|
+
* projection refers to the current member as `ref(scopeId)` — the same way a `repeat`
|
|
86
|
+
* node's template refers to its item. Collection<A> projected by A → B is Collection<B>.
|
|
87
|
+
*/
|
|
88
|
+
export interface MapExpression {
|
|
89
|
+
kind: 'map';
|
|
90
|
+
source: Expression;
|
|
91
|
+
scopeId: NodeId;
|
|
92
|
+
projection: Expression;
|
|
93
|
+
}
|
|
94
|
+
/** Orders a collection by a projected key. Deterministic for strings and numbers. */
|
|
95
|
+
export interface SortExpression {
|
|
96
|
+
kind: 'sort';
|
|
97
|
+
source: Expression;
|
|
98
|
+
scopeId: NodeId;
|
|
99
|
+
by: Expression;
|
|
100
|
+
direction?: 'asc' | 'desc';
|
|
101
|
+
}
|
|
73
102
|
export declare function literal(value: LiteralValue): LiteralExpression;
|
|
74
103
|
export declare function ref(targetId: NodeId): RefExpression;
|
|
75
104
|
export declare function field(source: Expression, id: FieldId): FieldExpression;
|
|
76
105
|
export declare function binary(operator: BinaryOperator, left: Expression, right: Expression): BinaryExpression;
|
|
77
106
|
export declare function unary(operator: UnaryOperator, operand: Expression): UnaryExpression;
|
|
78
107
|
export declare function call(fn: BuiltinFunction, ...args: Expression[]): CallExpression;
|
|
108
|
+
export declare function object(entries: ObjectEntry[], entityId?: NodeId): ObjectExpression;
|
|
109
|
+
export declare function filter(source: Expression, scopeId: NodeId, predicate: Expression): FilterExpression;
|
|
110
|
+
export declare function find(source: Expression, scopeId: NodeId, predicate: Expression): FindExpression;
|
|
111
|
+
export declare function map(source: Expression, scopeId: NodeId, projection: Expression): MapExpression;
|
|
112
|
+
export declare function sort(source: Expression, scopeId: NodeId, by: Expression, direction?: 'asc' | 'desc'): SortExpression;
|
|
113
|
+
/** Sums a collection of numbers. An empty collection sums to zero. */
|
|
114
|
+
export declare function sum(source: Expression): CallExpression;
|
|
115
|
+
export declare function count(source: Expression): CallExpression;
|
|
79
116
|
/** Visits every sub-expression, parents before children. */
|
|
80
117
|
export declare function walkExpression(expression: Expression, visit: (node: Expression) => void): void;
|
|
81
118
|
/** Field ids an expression reads, including nested sources and constructed records. */
|