@cynodia/axiom-core 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AskTech AS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Axiom Core
2
+
3
+ Part of [Axiom](https://github.com/cynodia/axiom), an AI-native semantic web application
4
+ framework.
5
+
6
+ **Status: experimental / alpha.** The API may change between alpha releases.
7
+
8
+ The Application Graph and its semantic model: nodes, fields, structured types,
9
+ expressions, **locations** (addressable writable positions), edge derivation,
10
+ validation and type inference.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @cynodia/axiom-core@alpha
16
+ ```
17
+
18
+ Most applications should install the facade package instead, which re-exports this one:
19
+
20
+ ```bash
21
+ npm install @cynodia/axiom@alpha
22
+ ```
23
+
24
+ ## License
25
+
26
+ MIT
27
+
28
+ Copyright (c) 2026 AskTech AS.
@@ -0,0 +1,5 @@
1
+ import type { ApplicationGraph } from './graph.js';
2
+ import type { SemanticContext } from './infer.js';
3
+ /** Adapts an authoring graph to the lookups static analysis needs. */
4
+ export declare function semanticContextFromGraph(graph: ApplicationGraph): SemanticContext;
5
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,30 @@
1
+ /** Adapts an authoring graph to the lookups static analysis needs. */
2
+ export function semanticContextFromGraph(graph) {
3
+ const parameterTypes = new Map();
4
+ const parameterNames = new Map();
5
+ for (const action of graph.getNodesByKind('action')) {
6
+ for (const parameter of action.parameters ?? []) {
7
+ parameterTypes.set(parameter.id, parameter.valueType);
8
+ parameterNames.set(parameter.id, parameter.name);
9
+ }
10
+ }
11
+ for (const route of graph.getNodesByKind('route')) {
12
+ for (const parameter of route.parameters ?? []) {
13
+ parameterTypes.set(parameter.id, parameter.valueType);
14
+ parameterNames.set(parameter.id, parameter.name);
15
+ }
16
+ }
17
+ return {
18
+ getState: (id) => {
19
+ const node = graph.getNode(id);
20
+ return node?.kind === 'state' ? node : undefined;
21
+ },
22
+ getEntity: (id) => {
23
+ const node = graph.getNode(id);
24
+ return node?.kind === 'entity' ? node : undefined;
25
+ },
26
+ getField: (id) => graph.getField(id),
27
+ getParameterType: (id) => parameterTypes.get(id),
28
+ getName: (id) => graph.getNode(id)?.name ?? parameterNames.get(id),
29
+ };
30
+ }
@@ -0,0 +1,18 @@
1
+ import type { Expression } from './expressions.js';
2
+ import type { NodeId } from './ids.js';
3
+ import type { GraphEdge } from './nodes.js';
4
+ import type { ApplicationGraph } from './graph.js';
5
+ /** Ids a `ref` expression mentions anywhere in the tree. */
6
+ export declare function referencedIds(expression: Expression): NodeId[];
7
+ /**
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.
11
+ *
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.
16
+ */
17
+ export declare function synchronizeEdges(graph: ApplicationGraph): GraphEdge[];
18
+ //# sourceMappingURL=derive-edges.d.ts.map
@@ -0,0 +1,282 @@
1
+ import { expressionFieldIds, walkExpression } from './expressions.js';
2
+ import { locationExpressions, locationFieldIds, locationRootStateId, locationSelectorFieldIds, } from './location.js';
3
+ import { isUINode } from './ui.js';
4
+ /** Ids a `ref` expression mentions anywhere in the tree. */
5
+ export function referencedIds(expression) {
6
+ const found = [];
7
+ walkExpression(expression, (node) => {
8
+ if (node.kind === 'ref') {
9
+ found.push(node.targetId);
10
+ }
11
+ });
12
+ return found;
13
+ }
14
+ function entityIdsIn(type) {
15
+ switch (type.kind) {
16
+ case 'entity':
17
+ return [type.entityId];
18
+ case 'collection':
19
+ return entityIdsIn(type.itemType);
20
+ case 'optional':
21
+ return entityIdsIn(type.valueType);
22
+ default:
23
+ return [];
24
+ }
25
+ }
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.
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.
35
+ */
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();
43
+ const known = new Set(nodes.map((node) => node.id));
44
+ const states = new Set(nodes.filter((node) => node.kind === 'state').map((node) => node.id));
45
+ 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();
49
+ for (const node of nodes) {
50
+ if (node.kind === 'repeat') {
51
+ scopeStates.set(node.id, referencedIds(node.source).filter((id) => states.has(id)));
52
+ }
53
+ }
54
+ const resolveStates = (id) => {
55
+ if (states.has(id)) {
56
+ return [id];
57
+ }
58
+ return scopeStates.get(id) ?? [];
59
+ };
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
+ },
110
+ };
111
+ for (const node of nodes) {
112
+ if (isUINode(node)) {
113
+ linkUiNode(node, linker);
114
+ continue;
115
+ }
116
+ switch (node.kind) {
117
+ case 'entity':
118
+ linkEntity(node, linker);
119
+ break;
120
+ case 'state':
121
+ linkState(node, linker);
122
+ break;
123
+ case 'action':
124
+ linkAction(node, linker);
125
+ break;
126
+ case 'constraint':
127
+ linkConstraint(node, linker);
128
+ break;
129
+ case 'route':
130
+ linker.link(node.id, node.viewId, 'routes-to');
131
+ break;
132
+ default:
133
+ }
134
+ }
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
+ });
142
+ }
143
+ return graph.listEdges();
144
+ }
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]);
149
+ }
150
+ }
151
+ }
152
+ function linkState(state, linker) {
153
+ for (const target of entityIdsIn(state.valueType)) {
154
+ linker.link(state.id, target, 'references');
155
+ }
156
+ if (state.derivation) {
157
+ linker.reads(state.id, state.derivation, 'derives-from');
158
+ }
159
+ }
160
+ function linkAction(action, linker) {
161
+ for (const expression of [...(action.preconditions ?? []), ...(action.postconditions ?? [])]) {
162
+ linker.reads(action.id, expression);
163
+ }
164
+ for (const operation of action.operations ?? []) {
165
+ switch (operation.kind) {
166
+ case 'set':
167
+ linker.writes(action.id, operation.target);
168
+ linker.reads(action.id, operation.value);
169
+ break;
170
+ case 'insert':
171
+ // 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);
174
+ break;
175
+ case 'remove':
176
+ linker.writes(action.id, operation.target);
177
+ break;
178
+ case 'invoke':
179
+ linker.link(action.id, operation.actionId, 'depends-on');
180
+ for (const argument of Object.values(operation.arguments ?? {})) {
181
+ linker.reads(action.id, argument);
182
+ }
183
+ break;
184
+ case 'navigate':
185
+ if (operation.routeId) {
186
+ linker.link(action.id, operation.routeId, 'depends-on');
187
+ }
188
+ for (const argument of Object.values(operation.parameters ?? {})) {
189
+ linker.reads(action.id, argument);
190
+ }
191
+ break;
192
+ case 'native':
193
+ for (const input of Object.values(operation.inputs ?? {})) {
194
+ linker.reads(action.id, input);
195
+ }
196
+ if (operation.resultTarget) {
197
+ linker.writes(action.id, operation.resultTarget);
198
+ }
199
+ for (const effect of operation.declaredEffects ?? []) {
200
+ if (effect.kind === 'reads-state') {
201
+ linker.link(action.id, effect.stateId, 'reads');
202
+ }
203
+ if (effect.kind === 'writes-state') {
204
+ linker.link(action.id, effect.stateId, 'writes');
205
+ }
206
+ }
207
+ break;
208
+ default:
209
+ }
210
+ }
211
+ }
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
+ function linkUiNode(node, linker) {
219
+ if (node.visibleWhen) {
220
+ linker.reads(node.id, node.visibleWhen);
221
+ }
222
+ switch (node.kind) {
223
+ case 'view':
224
+ case 'container':
225
+ for (const childId of node.children) {
226
+ linker.link(node.id, childId, 'contains');
227
+ }
228
+ return;
229
+ case 'form':
230
+ for (const childId of node.children) {
231
+ linker.link(node.id, childId, 'contains');
232
+ }
233
+ linker.reads(node.id, node.target);
234
+ if (node.submitActionId) {
235
+ linker.link(node.id, node.submitActionId, 'invokes');
236
+ }
237
+ return;
238
+ case 'conditional':
239
+ for (const childId of [...node.whenTrue, ...(node.whenFalse ?? [])]) {
240
+ linker.link(node.id, childId, 'contains');
241
+ }
242
+ linker.reads(node.id, node.condition);
243
+ return;
244
+ case 'repeat':
245
+ linker.link(node.id, node.templateId, 'renders');
246
+ if (node.emptyTemplateId) {
247
+ linker.link(node.id, node.emptyTemplateId, 'renders');
248
+ }
249
+ linker.reads(node.id, node.source);
250
+ return;
251
+ case 'text':
252
+ if (typeof node.value !== 'string') {
253
+ linker.reads(node.id, node.value);
254
+ }
255
+ return;
256
+ 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]);
259
+ }
260
+ return;
261
+ case 'input':
262
+ // An input both reads and writes the location it is bound to.
263
+ if (node.binding?.location) {
264
+ linker.writes(node.id, node.binding.location, 'binds');
265
+ linker.writes(node.id, node.binding.location, 'writes');
266
+ }
267
+ if (node.options) {
268
+ linker.reads(node.id, node.options.source);
269
+ }
270
+ return;
271
+ case 'button':
272
+ linker.link(node.id, node.actionId, 'invokes');
273
+ if (typeof node.label !== 'string') {
274
+ linker.reads(node.id, node.label);
275
+ }
276
+ for (const argument of Object.values(node.arguments ?? {})) {
277
+ linker.reads(node.id, argument);
278
+ }
279
+ return;
280
+ default:
281
+ }
282
+ }
@@ -0,0 +1,39 @@
1
+ import type { EdgeId, FieldId, NodeId } from './ids.js';
2
+ export interface ValidationIssue {
3
+ code: string;
4
+ message: string;
5
+ nodeId?: NodeId;
6
+ fieldId?: FieldId;
7
+ edgeId?: EdgeId;
8
+ }
9
+ export interface ValidationResult {
10
+ valid: boolean;
11
+ errors: ValidationIssue[];
12
+ warnings: ValidationIssue[];
13
+ }
14
+ export declare const VALIDATION_CODES: {
15
+ readonly duplicateNodeId: "DUPLICATE_NODE_ID";
16
+ readonly duplicateFieldId: "DUPLICATE_FIELD_ID";
17
+ readonly danglingNodeRef: "DANGLING_NODE_REF";
18
+ readonly danglingFieldRef: "DANGLING_FIELD_REF";
19
+ readonly invalidTypeRef: "INVALID_TYPE_REF";
20
+ readonly invalidEdgeKind: "INVALID_EDGE_KIND";
21
+ readonly invalidUiChild: "INVALID_UI_CHILD";
22
+ readonly invalidActionRef: "INVALID_ACTION_REF";
23
+ readonly invalidStateRef: "INVALID_STATE_REF";
24
+ readonly invalidRouteView: "INVALID_ROUTE_VIEW";
25
+ readonly invalidExpressionRef: "INVALID_EXPRESSION_REF";
26
+ readonly invalidRouteParameter: "INVALID_ROUTE_PARAMETER";
27
+ readonly duplicateRoutePath: "DUPLICATE_ROUTE_PATH";
28
+ readonly missingIdentityField: "MISSING_IDENTITY_FIELD";
29
+ readonly unreachableUiNode: "UNREACHABLE_UI_NODE";
30
+ readonly unknownStateRef: "UNKNOWN_STATE_REF";
31
+ readonly derivedStateWrite: "DERIVED_STATE_WRITE";
32
+ readonly fieldNotOnEntity: "FIELD_NOT_ON_ENTITY";
33
+ readonly selectorOnNonCollection: "SELECTOR_ON_NON_COLLECTION";
34
+ readonly fieldOnNonEntity: "FIELD_ON_NON_ENTITY";
35
+ readonly identityFieldMismatch: "IDENTITY_FIELD_MISMATCH";
36
+ readonly assignmentTypeMismatch: "ASSIGNMENT_TYPE_MISMATCH";
37
+ readonly invalidSelectorType: "INVALID_SELECTOR_TYPE";
38
+ };
39
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,25 @@
1
+ export const VALIDATION_CODES = {
2
+ duplicateNodeId: 'DUPLICATE_NODE_ID',
3
+ duplicateFieldId: 'DUPLICATE_FIELD_ID',
4
+ danglingNodeRef: 'DANGLING_NODE_REF',
5
+ danglingFieldRef: 'DANGLING_FIELD_REF',
6
+ invalidTypeRef: 'INVALID_TYPE_REF',
7
+ invalidEdgeKind: 'INVALID_EDGE_KIND',
8
+ invalidUiChild: 'INVALID_UI_CHILD',
9
+ invalidActionRef: 'INVALID_ACTION_REF',
10
+ invalidStateRef: 'INVALID_STATE_REF',
11
+ invalidRouteView: 'INVALID_ROUTE_VIEW',
12
+ invalidExpressionRef: 'INVALID_EXPRESSION_REF',
13
+ invalidRouteParameter: 'INVALID_ROUTE_PARAMETER',
14
+ duplicateRoutePath: 'DUPLICATE_ROUTE_PATH',
15
+ missingIdentityField: 'MISSING_IDENTITY_FIELD',
16
+ unreachableUiNode: 'UNREACHABLE_UI_NODE',
17
+ unknownStateRef: 'UNKNOWN_STATE_REF',
18
+ derivedStateWrite: 'DERIVED_STATE_WRITE',
19
+ fieldNotOnEntity: 'FIELD_NOT_ON_ENTITY',
20
+ selectorOnNonCollection: 'SELECTOR_ON_NON_COLLECTION',
21
+ fieldOnNonEntity: 'FIELD_ON_NON_ENTITY',
22
+ identityFieldMismatch: 'IDENTITY_FIELD_MISMATCH',
23
+ assignmentTypeMismatch: 'ASSIGNMENT_TYPE_MISMATCH',
24
+ invalidSelectorType: 'INVALID_SELECTOR_TYPE',
25
+ };
@@ -0,0 +1,83 @@
1
+ import type { FieldId, NodeId } from './ids.js';
2
+ import type { LiteralValue } from './nodes.js';
3
+ /**
4
+ * Expressions are structured trees, never source strings. Every value reference is an
5
+ * identifier: `ref` resolves an id against the evaluation scope chain (route parameters,
6
+ * action parameters, iteration scopes, then state), and `field` reads a field by id.
7
+ */
8
+ export type Expression = LiteralExpression | RefExpression | FieldExpression | ObjectExpression | BinaryExpression | UnaryExpression | CallExpression | FilterExpression | FindExpression;
9
+ export type LiteralPrimitive = string | number | boolean | null;
10
+ /** Literal data. Structured values are allowed; executable text is not. */
11
+ export interface LiteralExpression {
12
+ kind: 'literal';
13
+ value: LiteralValue;
14
+ }
15
+ /**
16
+ * Resolves an identifier in the current scope chain. The target may be a state node, a
17
+ * route parameter, an action parameter, an entity under validation, or an iteration
18
+ * scope (a `repeat` node, or a `filter`/`find` expression's `scopeId`).
19
+ */
20
+ export interface RefExpression {
21
+ kind: 'ref';
22
+ targetId: NodeId;
23
+ }
24
+ export interface FieldExpression {
25
+ kind: 'field';
26
+ source: Expression;
27
+ fieldId: FieldId;
28
+ }
29
+ /** Constructs a record keyed by field id — used to build new instances. */
30
+ export interface ObjectExpression {
31
+ kind: 'object';
32
+ entityId?: NodeId;
33
+ entries: ObjectEntry[];
34
+ }
35
+ export interface ObjectEntry {
36
+ fieldId: FieldId;
37
+ value: Expression;
38
+ }
39
+ export type BinaryOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'and' | 'or' | 'add' | 'subtract' | 'multiply' | 'divide';
40
+ export interface BinaryExpression {
41
+ kind: 'binary';
42
+ operator: BinaryOperator;
43
+ left: Expression;
44
+ right: Expression;
45
+ }
46
+ export type UnaryOperator = 'not' | 'negate';
47
+ export interface UnaryExpression {
48
+ kind: 'unary';
49
+ operator: UnaryOperator;
50
+ operand: Expression;
51
+ }
52
+ /** The built-in function vocabulary. Deliberately small and domain-neutral. */
53
+ export type BuiltinFunction = 'required' | 'is-empty' | 'length' | 'contains' | 'concat' | 'coalesce' | 'one-of' | 'count' | 'sum' | 'lowercase' | 'to-string' | 'now' | 'uuid';
54
+ export interface CallExpression {
55
+ kind: 'call';
56
+ function: BuiltinFunction;
57
+ arguments: Expression[];
58
+ }
59
+ /** Filters a collection. `predicate` is evaluated with the current item bound to `scopeId`. */
60
+ export interface FilterExpression {
61
+ kind: 'filter';
62
+ source: Expression;
63
+ scopeId: NodeId;
64
+ predicate: Expression;
65
+ }
66
+ /** Returns the first matching item, or null. */
67
+ export interface FindExpression {
68
+ kind: 'find';
69
+ source: Expression;
70
+ scopeId: NodeId;
71
+ predicate: Expression;
72
+ }
73
+ export declare function literal(value: LiteralValue): LiteralExpression;
74
+ export declare function ref(targetId: NodeId): RefExpression;
75
+ export declare function field(source: Expression, id: FieldId): FieldExpression;
76
+ export declare function binary(operator: BinaryOperator, left: Expression, right: Expression): BinaryExpression;
77
+ export declare function unary(operator: UnaryOperator, operand: Expression): UnaryExpression;
78
+ export declare function call(fn: BuiltinFunction, ...args: Expression[]): CallExpression;
79
+ /** Visits every sub-expression, parents before children. */
80
+ export declare function walkExpression(expression: Expression, visit: (node: Expression) => void): void;
81
+ /** Field ids an expression reads, including nested sources and constructed records. */
82
+ export declare function expressionFieldIds(expression: Expression): FieldId[];
83
+ //# sourceMappingURL=expressions.d.ts.map
@@ -0,0 +1,65 @@
1
+ export function literal(value) {
2
+ return { kind: 'literal', value };
3
+ }
4
+ export function ref(targetId) {
5
+ return { kind: 'ref', targetId };
6
+ }
7
+ export function field(source, id) {
8
+ return { kind: 'field', source, fieldId: id };
9
+ }
10
+ export function binary(operator, left, right) {
11
+ return { kind: 'binary', operator, left, right };
12
+ }
13
+ export function unary(operator, operand) {
14
+ return { kind: 'unary', operator, operand };
15
+ }
16
+ export function call(fn, ...args) {
17
+ return { kind: 'call', function: fn, arguments: args };
18
+ }
19
+ /** Visits every sub-expression, parents before children. */
20
+ export function walkExpression(expression, visit) {
21
+ visit(expression);
22
+ switch (expression.kind) {
23
+ case 'field':
24
+ walkExpression(expression.source, visit);
25
+ return;
26
+ case 'object':
27
+ for (const entry of expression.entries) {
28
+ walkExpression(entry.value, visit);
29
+ }
30
+ return;
31
+ case 'binary':
32
+ walkExpression(expression.left, visit);
33
+ walkExpression(expression.right, visit);
34
+ return;
35
+ case 'unary':
36
+ walkExpression(expression.operand, visit);
37
+ return;
38
+ case 'call':
39
+ for (const argument of expression.arguments) {
40
+ walkExpression(argument, visit);
41
+ }
42
+ return;
43
+ case 'filter':
44
+ case 'find':
45
+ walkExpression(expression.source, visit);
46
+ walkExpression(expression.predicate, visit);
47
+ return;
48
+ default:
49
+ }
50
+ }
51
+ /** Field ids an expression reads, including nested sources and constructed records. */
52
+ export function expressionFieldIds(expression) {
53
+ const found = [];
54
+ walkExpression(expression, (node) => {
55
+ if (node.kind === 'field') {
56
+ found.push(node.fieldId);
57
+ }
58
+ if (node.kind === 'object') {
59
+ for (const entry of node.entries) {
60
+ found.push(entry.fieldId);
61
+ }
62
+ }
63
+ });
64
+ return found;
65
+ }
@@ -0,0 +1,55 @@
1
+ import type { EdgeId, FieldId, NodeId } from './ids.js';
2
+ import type { EdgeKind, FieldDef, GraphEdge } from './nodes.js';
3
+ import type { AnyNode, ApplicationGraphData, NodeInput, NodeKind, NodeOfKind } from './types.js';
4
+ export interface FieldIndexEntry {
5
+ entityId: NodeId;
6
+ field: FieldDef;
7
+ }
8
+ export interface EdgeQuery {
9
+ kinds?: readonly EdgeKind[];
10
+ }
11
+ /**
12
+ * The Application Graph is the canonical representation of an application. Reads return
13
+ * deep clones, so a node retrieved from the graph must be written back with
14
+ * `updateNode` for the change to take effect.
15
+ */
16
+ export declare class ApplicationGraph {
17
+ private data;
18
+ private outgoing;
19
+ private incoming;
20
+ private fieldIndex;
21
+ constructor(id: string, name: string, version?: string);
22
+ get id(): string;
23
+ get name(): string;
24
+ get version(): string;
25
+ addNode<T extends AnyNode>(node: NodeInput<T>): NodeId;
26
+ getNode<T extends AnyNode = AnyNode>(id: NodeId): T | undefined;
27
+ hasNode(id: NodeId): boolean;
28
+ updateNode(node: AnyNode): void;
29
+ removeNode(id: NodeId): boolean;
30
+ getNodesByKind<K extends NodeKind>(kind: K): NodeOfKind<K>[];
31
+ listNodes(): AnyNode[];
32
+ /** Resolves a field id to its owning entity. Fields are globally identifiable. */
33
+ getField(id: FieldId): FieldIndexEntry | undefined;
34
+ listFields(): FieldIndexEntry[];
35
+ addEdge(from: NodeId, to: NodeId, kind: EdgeKind, options?: {
36
+ id?: EdgeId;
37
+ metadata?: Record<string, unknown>;
38
+ }): EdgeId;
39
+ removeEdge(id: EdgeId): boolean;
40
+ getEdge(id: EdgeId): GraphEdge | undefined;
41
+ listEdges(): GraphEdge[];
42
+ getEdges(nodeId: NodeId, query?: EdgeQuery): GraphEdge[];
43
+ getOutgoingEdges(nodeId: NodeId, query?: EdgeQuery): GraphEdge[];
44
+ getIncomingEdges(nodeId: NodeId, query?: EdgeQuery): GraphEdge[];
45
+ toJSON(): ApplicationGraphData;
46
+ serialize(): string;
47
+ restore(input: string | ApplicationGraphData): void;
48
+ static deserialize(input: string | ApplicationGraphData): ApplicationGraph;
49
+ private indexEdge;
50
+ private indexNodeFields;
51
+ private rebuildFieldIndex;
52
+ private rebuildEdgeIndexes;
53
+ private rebuildIndexes;
54
+ }
55
+ //# sourceMappingURL=graph.d.ts.map