@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.
@@ -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
- * that already exist in the nodes, so they are derived rather than hand maintained:
10
- * any transformation that changes a node can simply re-run this pass.
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 an agent can distinguish an action that
13
- * writes one field of a record from one that replaces the record.
14
- *
15
- * Edges added by hand (without the `derived` marker) are preserved.
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
@@ -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
- * that already exist in the nodes, so they are derived rather than hand maintained:
29
- * any transformation that changes a node can simply re-run this pass.
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 an agent can distinguish an action that
32
- * writes one field of a record from one that replaces the record.
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 synchronizeEdges(graph) {
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
- // An iteration scope stands for an item of whatever collection its repeat reads, so a
47
- // template that shows a field of the item still reads that field of the state.
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
- scopeStates.set(node.id, referencedIds(node.source).filter((id) => states.has(id)));
42
+ rootScope.set(node.id, statesOf(node.source, new Map(), states));
52
43
  }
53
44
  }
54
- const resolveStates = (id) => {
55
- if (states.has(id)) {
56
- return [id];
45
+ const link = (from, to, kind, fieldIds = []) => {
46
+ if (from === to || !known.has(from) || !known.has(to)) {
47
+ return;
57
48
  }
58
- return scopeStates.get(id) ?? [];
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 linker = {
61
- statesFor: resolveStates,
62
- link(from, to, kind, fieldIds = []) {
63
- if (from === to || !known.has(from) || !known.has(to)) {
64
- return;
65
- }
66
- const key = `${from}|${to}|${kind}`;
67
- const entry = pending.get(key) ?? { from, to, kind, fieldIds: new Set() };
68
- for (const fieldId of fieldIds) {
69
- entry.fieldIds.add(fieldId);
70
- }
71
- pending.set(key, entry);
72
- },
73
- reads(from, expression, kind = 'reads') {
74
- // Attribute a field to a state only where the expression actually reads that
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, linker);
74
+ linkUiNode(node, { link, reads, writes, scope: rootScope, states });
114
75
  continue;
115
76
  }
116
77
  switch (node.kind) {
117
78
  case 'entity':
118
- linkEntity(node, linker);
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, linker);
86
+ linkState(node, link, reads, rootScope);
122
87
  break;
123
88
  case 'action':
124
- linkAction(node, linker);
89
+ linkAction(node, { link, reads, writes, scope: rootScope, states });
125
90
  break;
126
91
  case 'constraint':
127
- linkConstraint(node, linker);
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
- linker.link(node.id, node.viewId, 'routes-to');
98
+ link(node.id, node.viewId, 'routes-to');
131
99
  break;
132
100
  default:
133
101
  }
134
102
  }
135
- for (const entry of pending.values()) {
136
- graph.addEdge(entry.from, entry.to, entry.kind, {
137
- metadata: {
138
- derived: true,
139
- ...(entry.fieldIds.size > 0 ? { fieldIds: [...entry.fieldIds] } : {}),
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 linkEntity(entity, linker) {
146
- for (const field of entity.fields) {
147
- for (const target of entityIdsIn(field.valueType)) {
148
- linker.link(entity.id, target, 'references', [field.id]);
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, linker) {
198
+ function linkState(state, link, reads, scope) {
153
199
  for (const target of entityIdsIn(state.valueType)) {
154
- linker.link(state.id, target, 'references');
200
+ link(state.id, target, 'references');
155
201
  }
156
202
  if (state.derivation) {
157
- linker.reads(state.id, state.derivation, 'derives-from');
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
- for (const operation of action.operations ?? []) {
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(action.id, operation.target);
168
- linker.reads(action.id, operation.value);
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(action.id, operation.target, 'writes', expressionFieldIds(operation.value));
173
- linker.reads(action.id, operation.value);
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(action.id, operation.target);
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(action.id, operation.actionId, 'depends-on');
234
+ linker.link(actionId, operation.actionId, 'depends-on');
180
235
  for (const argument of Object.values(operation.arguments ?? {})) {
181
- linker.reads(action.id, argument);
236
+ linker.reads(actionId, argument, scope);
182
237
  }
183
238
  break;
184
239
  case 'navigate':
185
240
  if (operation.routeId) {
186
- linker.link(action.id, operation.routeId, 'depends-on');
241
+ linker.link(actionId, operation.routeId, 'depends-on');
187
242
  }
188
243
  for (const argument of Object.values(operation.parameters ?? {})) {
189
- linker.reads(action.id, argument);
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(action.id, input);
249
+ linker.reads(actionId, input, scope);
195
250
  }
196
251
  if (operation.resultTarget) {
197
- linker.writes(action.id, operation.resultTarget);
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(action.id, effect.stateId, 'reads');
256
+ linker.link(actionId, effect.stateId, 'reads');
202
257
  }
203
258
  if (effect.kind === 'writes-state') {
204
- linker.link(action.id, effect.stateId, 'writes');
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
- linker.reads(node.id, node.visibleWhen);
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
- linker.link(node.id, childId, 'contains');
276
+ link(node.id, childId, 'contains');
227
277
  }
228
278
  return;
229
279
  case 'form':
230
280
  for (const childId of node.children) {
231
- linker.link(node.id, childId, 'contains');
281
+ link(node.id, childId, 'contains');
232
282
  }
233
- linker.reads(node.id, node.target);
283
+ reads(node.id, node.target, scope);
234
284
  if (node.submitActionId) {
235
- linker.link(node.id, node.submitActionId, 'invokes');
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
- linker.link(node.id, childId, 'contains');
290
+ link(node.id, childId, 'contains');
241
291
  }
242
- linker.reads(node.id, node.condition);
292
+ reads(node.id, node.condition, scope);
243
293
  return;
244
294
  case 'repeat':
245
- linker.link(node.id, node.templateId, 'renders');
295
+ link(node.id, node.templateId, 'renders');
246
296
  if (node.emptyTemplateId) {
247
- linker.link(node.id, node.emptyTemplateId, 'renders');
297
+ link(node.id, node.emptyTemplateId, 'renders');
248
298
  }
249
- linker.reads(node.id, node.source);
299
+ reads(node.id, node.source, scope);
250
300
  return;
251
301
  case 'text':
252
302
  if (typeof node.value !== 'string') {
253
- linker.reads(node.id, node.value);
303
+ reads(node.id, node.value, scope);
254
304
  }
255
305
  return;
256
306
  case 'field-display':
257
- for (const id of referencedIds(node.source).flatMap((target) => linker.statesFor(target))) {
258
- linker.link(node.id, id, 'reads', [node.fieldId]);
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
- linker.writes(node.id, node.binding.location, 'binds');
265
- linker.writes(node.id, node.binding.location, 'writes');
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
- linker.reads(node.id, node.options.source);
319
+ reads(node.id, node.options.source, scope);
269
320
  }
270
321
  return;
271
322
  case 'button':
272
- linker.link(node.id, node.actionId, 'invokes');
323
+ link(node.id, node.actionId, 'invokes');
273
324
  if (typeof node.label !== 'string') {
274
- linker.reads(node.id, node.label);
325
+ reads(node.id, node.label, scope);
275
326
  }
276
327
  for (const argument of Object.values(node.arguments ?? {})) {
277
- linker.reads(node.id, argument);
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
+ }
@@ -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
@@ -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
  };
@@ -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
- /** The built-in function vocabulary. Deliberately small and domain-neutral. */
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. */