@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 +21 -0
- package/README.md +28 -0
- package/dist/context.d.ts +5 -0
- package/dist/context.js +30 -0
- package/dist/derive-edges.d.ts +18 -0
- package/dist/derive-edges.js +282 -0
- package/dist/diagnostics.d.ts +39 -0
- package/dist/diagnostics.js +25 -0
- package/dist/expressions.d.ts +83 -0
- package/dist/expressions.js +65 -0
- package/dist/graph.d.ts +55 -0
- package/dist/graph.js +176 -0
- package/dist/ids.d.ts +22 -0
- package/dist/ids.js +22 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/infer.d.ts +41 -0
- package/dist/infer.js +186 -0
- package/dist/ir.d.ts +50 -0
- package/dist/ir.js +1 -0
- package/dist/location.d.ts +57 -0
- package/dist/location.js +77 -0
- package/dist/nodes.d.ts +164 -0
- package/dist/nodes.js +16 -0
- package/dist/type-ref.d.ts +35 -0
- package/dist/type-ref.js +19 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.js +1 -0
- package/dist/ui.d.ts +101 -0
- package/dist/ui.js +29 -0
- package/dist/validate-location.d.ts +16 -0
- package/dist/validate-location.js +78 -0
- package/dist/validate.d.ts +9 -0
- package/dist/validate.js +574 -0
- package/package.json +37 -0
package/dist/nodes.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { Expression } from './expressions.js';
|
|
2
|
+
import type { CollectionItemLocation, Location } from './location.js';
|
|
3
|
+
import type { EdgeId, FieldId, NodeId } from './ids.js';
|
|
4
|
+
import type { TypeRef } from './type-ref.js';
|
|
5
|
+
/** Minimal, optional presentation hints. Styling is not a 0.2 research objective. */
|
|
6
|
+
export interface PresentationHints {
|
|
7
|
+
role?: 'primary' | 'secondary' | 'danger';
|
|
8
|
+
density?: 'compact' | 'normal';
|
|
9
|
+
emphasis?: 'normal' | 'strong';
|
|
10
|
+
}
|
|
11
|
+
export interface NodeBase {
|
|
12
|
+
id: NodeId;
|
|
13
|
+
name?: string;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface FieldDef {
|
|
17
|
+
id: FieldId;
|
|
18
|
+
name?: string;
|
|
19
|
+
valueType: TypeRef;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
defaultValue?: LiteralValue;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export type LiteralValue = string | number | boolean | null | LiteralValue[] | {
|
|
25
|
+
[key: string]: LiteralValue;
|
|
26
|
+
};
|
|
27
|
+
export interface EntityDef extends NodeBase {
|
|
28
|
+
kind: 'entity';
|
|
29
|
+
fields: FieldDef[];
|
|
30
|
+
/** Field used to distinguish instances of this entity. */
|
|
31
|
+
identityFieldId?: FieldId;
|
|
32
|
+
}
|
|
33
|
+
export type StatePersistence = {
|
|
34
|
+
kind: 'memory';
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'local-storage';
|
|
37
|
+
key?: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'remote';
|
|
40
|
+
sourceId: NodeId;
|
|
41
|
+
};
|
|
42
|
+
export interface StateDef extends NodeBase {
|
|
43
|
+
kind: 'state';
|
|
44
|
+
valueType: TypeRef;
|
|
45
|
+
initialValue?: LiteralValue;
|
|
46
|
+
/** When present the state is computed rather than stored. */
|
|
47
|
+
derivation?: Expression;
|
|
48
|
+
/**
|
|
49
|
+
* Marks a state that holds work in progress. Draft instances are incomplete by
|
|
50
|
+
* definition, so instance validation skips them until an action commits the value.
|
|
51
|
+
*/
|
|
52
|
+
draft?: boolean;
|
|
53
|
+
persistence?: StatePersistence;
|
|
54
|
+
}
|
|
55
|
+
export interface ActionParameter {
|
|
56
|
+
id: NodeId;
|
|
57
|
+
name?: string;
|
|
58
|
+
valueType?: TypeRef;
|
|
59
|
+
required?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface FailureMode {
|
|
62
|
+
code: string;
|
|
63
|
+
message?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ActionDef extends NodeBase {
|
|
66
|
+
kind: 'action';
|
|
67
|
+
parameters?: ActionParameter[];
|
|
68
|
+
preconditions?: Expression[];
|
|
69
|
+
operations: Operation[];
|
|
70
|
+
postconditions?: Expression[];
|
|
71
|
+
failureModes?: FailureMode[];
|
|
72
|
+
/** Declared destructive intent. Semantics may also be inferred from operations. */
|
|
73
|
+
destructive?: boolean;
|
|
74
|
+
requiresConfirmation?: boolean;
|
|
75
|
+
confirmationMessage?: string;
|
|
76
|
+
}
|
|
77
|
+
export type Operation = SetOperation | InsertOperation | RemoveOperation | InvokeOperation | NavigateOperation | NativeOperation;
|
|
78
|
+
/** Every mutation is a set, an insert or a remove against an addressed Location. */
|
|
79
|
+
export type MutationOperation = SetOperation | InsertOperation | RemoveOperation;
|
|
80
|
+
export interface SetOperation {
|
|
81
|
+
kind: 'set';
|
|
82
|
+
target: Location;
|
|
83
|
+
value: Expression;
|
|
84
|
+
}
|
|
85
|
+
export interface InsertOperation {
|
|
86
|
+
kind: 'insert';
|
|
87
|
+
/** A location addressing a collection. */
|
|
88
|
+
target: Location;
|
|
89
|
+
value: Expression;
|
|
90
|
+
position?: 'start' | 'end';
|
|
91
|
+
}
|
|
92
|
+
export interface RemoveOperation {
|
|
93
|
+
kind: 'remove';
|
|
94
|
+
target: CollectionItemLocation;
|
|
95
|
+
}
|
|
96
|
+
export declare function isMutationOperation(operation: Operation): operation is MutationOperation;
|
|
97
|
+
export interface InvokeOperation {
|
|
98
|
+
kind: 'invoke';
|
|
99
|
+
actionId: NodeId;
|
|
100
|
+
arguments?: Record<string, Expression>;
|
|
101
|
+
}
|
|
102
|
+
export interface NavigateOperation {
|
|
103
|
+
kind: 'navigate';
|
|
104
|
+
routeId?: NodeId;
|
|
105
|
+
path?: string;
|
|
106
|
+
/** Keyed by route parameter id. */
|
|
107
|
+
parameters?: Record<string, Expression>;
|
|
108
|
+
}
|
|
109
|
+
export type NativeEffect = {
|
|
110
|
+
kind: 'reads-state';
|
|
111
|
+
stateId: NodeId;
|
|
112
|
+
} | {
|
|
113
|
+
kind: 'writes-state';
|
|
114
|
+
stateId: NodeId;
|
|
115
|
+
} | {
|
|
116
|
+
kind: 'external';
|
|
117
|
+
description: string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Controlled boundary for behaviour the operation vocabulary cannot express. The graph
|
|
121
|
+
* declares an implementation id and its effects; the implementation itself is registered
|
|
122
|
+
* with the runtime and is never embedded in the graph as source text.
|
|
123
|
+
*/
|
|
124
|
+
export interface NativeOperation {
|
|
125
|
+
kind: 'native';
|
|
126
|
+
implementationId: string;
|
|
127
|
+
inputs?: Record<string, Expression>;
|
|
128
|
+
/**
|
|
129
|
+
* Where the implementation's return value is stored. Native code never writes Axiom
|
|
130
|
+
* state itself; it returns a value that the mutation engine then sets.
|
|
131
|
+
*/
|
|
132
|
+
resultTarget?: Location;
|
|
133
|
+
declaredEffects?: NativeEffect[];
|
|
134
|
+
}
|
|
135
|
+
export interface ConstraintDef extends NodeBase {
|
|
136
|
+
kind: 'constraint';
|
|
137
|
+
expression: Expression;
|
|
138
|
+
/** When set, the expression is evaluated once per instance of this entity. */
|
|
139
|
+
entityId?: NodeId;
|
|
140
|
+
severity?: 'error' | 'warning';
|
|
141
|
+
message?: string;
|
|
142
|
+
}
|
|
143
|
+
export interface RouteParameter {
|
|
144
|
+
id: NodeId;
|
|
145
|
+
/** Matches the `:name` placeholder in the route path. */
|
|
146
|
+
name: string;
|
|
147
|
+
valueType?: TypeRef;
|
|
148
|
+
}
|
|
149
|
+
export interface RouteDef extends NodeBase {
|
|
150
|
+
kind: 'route';
|
|
151
|
+
path: string;
|
|
152
|
+
viewId: NodeId;
|
|
153
|
+
parameters?: RouteParameter[];
|
|
154
|
+
}
|
|
155
|
+
export type EdgeKind = 'contains' | 'reads' | 'writes' | 'invokes' | 'renders' | 'binds' | 'depends-on' | 'derives-from' | 'constrains' | 'routes-to' | 'references';
|
|
156
|
+
export declare const EDGE_KINDS: readonly EdgeKind[];
|
|
157
|
+
export interface GraphEdge {
|
|
158
|
+
id: EdgeId;
|
|
159
|
+
from: NodeId;
|
|
160
|
+
to: NodeId;
|
|
161
|
+
kind: EdgeKind;
|
|
162
|
+
metadata?: Record<string, unknown>;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=nodes.d.ts.map
|
package/dist/nodes.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function isMutationOperation(operation) {
|
|
2
|
+
return operation.kind === 'set' || operation.kind === 'insert' || operation.kind === 'remove';
|
|
3
|
+
}
|
|
4
|
+
export const EDGE_KINDS = [
|
|
5
|
+
'contains',
|
|
6
|
+
'reads',
|
|
7
|
+
'writes',
|
|
8
|
+
'invokes',
|
|
9
|
+
'renders',
|
|
10
|
+
'binds',
|
|
11
|
+
'depends-on',
|
|
12
|
+
'derives-from',
|
|
13
|
+
'constrains',
|
|
14
|
+
'routes-to',
|
|
15
|
+
'references',
|
|
16
|
+
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { NodeId } from './ids.js';
|
|
2
|
+
/**
|
|
3
|
+
* Structured type references. Types are never encoded as strings such as
|
|
4
|
+
* "Collection<X>" — every type is a walkable structure.
|
|
5
|
+
*/
|
|
6
|
+
export type TypeRef = PrimitiveTypeRef | EntityTypeRef | CollectionTypeRef | OptionalTypeRef | EnumTypeRef;
|
|
7
|
+
export type PrimitiveKind = 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'binary';
|
|
8
|
+
export interface PrimitiveTypeRef {
|
|
9
|
+
kind: 'primitive';
|
|
10
|
+
primitive: PrimitiveKind;
|
|
11
|
+
}
|
|
12
|
+
export interface EntityTypeRef {
|
|
13
|
+
kind: 'entity';
|
|
14
|
+
entityId: NodeId;
|
|
15
|
+
}
|
|
16
|
+
export interface CollectionTypeRef {
|
|
17
|
+
kind: 'collection';
|
|
18
|
+
itemType: TypeRef;
|
|
19
|
+
}
|
|
20
|
+
export interface OptionalTypeRef {
|
|
21
|
+
kind: 'optional';
|
|
22
|
+
valueType: TypeRef;
|
|
23
|
+
}
|
|
24
|
+
export interface EnumTypeRef {
|
|
25
|
+
kind: 'enum';
|
|
26
|
+
values: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function primitiveType(primitive: PrimitiveKind): PrimitiveTypeRef;
|
|
29
|
+
export declare function entityType(entityId: NodeId): EntityTypeRef;
|
|
30
|
+
export declare function collectionType(itemType: TypeRef): CollectionTypeRef;
|
|
31
|
+
export declare function optionalType(valueType: TypeRef): OptionalTypeRef;
|
|
32
|
+
export declare function enumType(values: string[]): EnumTypeRef;
|
|
33
|
+
/** Removes an `optional` wrapper, if present. */
|
|
34
|
+
export declare function unwrapOptional(type: TypeRef): TypeRef;
|
|
35
|
+
//# sourceMappingURL=type-ref.d.ts.map
|
package/dist/type-ref.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function primitiveType(primitive) {
|
|
2
|
+
return { kind: 'primitive', primitive };
|
|
3
|
+
}
|
|
4
|
+
export function entityType(entityId) {
|
|
5
|
+
return { kind: 'entity', entityId };
|
|
6
|
+
}
|
|
7
|
+
export function collectionType(itemType) {
|
|
8
|
+
return { kind: 'collection', itemType };
|
|
9
|
+
}
|
|
10
|
+
export function optionalType(valueType) {
|
|
11
|
+
return { kind: 'optional', valueType };
|
|
12
|
+
}
|
|
13
|
+
export function enumType(values) {
|
|
14
|
+
return { kind: 'enum', values };
|
|
15
|
+
}
|
|
16
|
+
/** Removes an `optional` wrapper, if present. */
|
|
17
|
+
export function unwrapOptional(type) {
|
|
18
|
+
return type.kind === 'optional' ? unwrapOptional(type.valueType) : type;
|
|
19
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ActionDef, ConstraintDef, EntityDef, GraphEdge, RouteDef, StateDef } from './nodes.js';
|
|
2
|
+
import type { NodeId } from './ids.js';
|
|
3
|
+
import type { UINode, UINodeKind } from './ui.js';
|
|
4
|
+
export type SemanticNodeKind = 'entity' | 'state' | 'action' | 'constraint' | 'route';
|
|
5
|
+
export type NodeKind = SemanticNodeKind | UINodeKind;
|
|
6
|
+
export type AnyNode = EntityDef | StateDef | ActionDef | ConstraintDef | RouteDef | UINode;
|
|
7
|
+
export type NodeOfKind<K extends NodeKind> = Extract<AnyNode, {
|
|
8
|
+
kind: K;
|
|
9
|
+
}>;
|
|
10
|
+
/** A node as supplied by a caller: the id may be omitted and will be generated. */
|
|
11
|
+
export type NodeInput<T extends AnyNode = AnyNode> = T extends AnyNode ? Omit<T, 'id'> & {
|
|
12
|
+
id?: NodeId;
|
|
13
|
+
} : never;
|
|
14
|
+
export interface ApplicationGraphData {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
version: string;
|
|
18
|
+
nodes: Record<NodeId, AnyNode>;
|
|
19
|
+
edges: Record<string, GraphEdge>;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Expression } from './expressions.js';
|
|
2
|
+
import type { FieldId, NodeId } from './ids.js';
|
|
3
|
+
import type { Location } from './location.js';
|
|
4
|
+
import type { PresentationHints } from './nodes.js';
|
|
5
|
+
export type UINodeKind = 'view' | 'container' | 'text' | 'repeat' | 'field-display' | 'form' | 'input' | 'button' | 'conditional';
|
|
6
|
+
export declare const UI_NODE_KINDS: readonly UINodeKind[];
|
|
7
|
+
export interface UIBase {
|
|
8
|
+
id: NodeId;
|
|
9
|
+
kind: UINodeKind;
|
|
10
|
+
name?: string;
|
|
11
|
+
visibleWhen?: Expression;
|
|
12
|
+
presentation?: PresentationHints;
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** A routable or independently renderable UI root. */
|
|
16
|
+
export interface ViewNode extends UIBase {
|
|
17
|
+
kind: 'view';
|
|
18
|
+
children: NodeId[];
|
|
19
|
+
}
|
|
20
|
+
export interface ContainerNode extends UIBase {
|
|
21
|
+
kind: 'container';
|
|
22
|
+
layout?: 'vertical' | 'horizontal' | 'stack';
|
|
23
|
+
children: NodeId[];
|
|
24
|
+
}
|
|
25
|
+
export interface TextNode extends UIBase {
|
|
26
|
+
kind: 'text';
|
|
27
|
+
value: string | Expression;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Repeats `templateId` over `source`. The current item is bound to this node's own id,
|
|
31
|
+
* so templates reference it with `{ kind: 'ref', targetId: <repeat node id> }`.
|
|
32
|
+
*/
|
|
33
|
+
export interface RepeatNode extends UIBase {
|
|
34
|
+
kind: 'repeat';
|
|
35
|
+
source: Expression;
|
|
36
|
+
itemAlias?: string;
|
|
37
|
+
templateId: NodeId;
|
|
38
|
+
emptyTemplateId?: NodeId;
|
|
39
|
+
}
|
|
40
|
+
export interface FieldDisplayNode extends UIBase {
|
|
41
|
+
kind: 'field-display';
|
|
42
|
+
source: Expression;
|
|
43
|
+
fieldId: FieldId;
|
|
44
|
+
label?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface FormNode extends UIBase {
|
|
47
|
+
kind: 'form';
|
|
48
|
+
target: Expression;
|
|
49
|
+
children: NodeId[];
|
|
50
|
+
submitActionId?: NodeId;
|
|
51
|
+
submitLabel?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* An input writes to an addressed Location, never to whatever object an expression
|
|
55
|
+
* happened to evaluate to.
|
|
56
|
+
*/
|
|
57
|
+
export interface InputBinding {
|
|
58
|
+
location: Location;
|
|
59
|
+
}
|
|
60
|
+
export type InputHint = 'text' | 'email' | 'number' | 'password' | 'date' | 'checkbox' | 'multiline' | 'select';
|
|
61
|
+
/**
|
|
62
|
+
* Offers a choice drawn from application data rather than a fixed enum, which is how a
|
|
63
|
+
* value that identifies another record is entered. Each candidate is bound to `scopeId`
|
|
64
|
+
* while `valueFieldId` and `labelFieldId` are read.
|
|
65
|
+
*/
|
|
66
|
+
export interface InputOptionsSource {
|
|
67
|
+
source: Expression;
|
|
68
|
+
scopeId: NodeId;
|
|
69
|
+
valueFieldId: FieldId;
|
|
70
|
+
labelFieldId?: FieldId;
|
|
71
|
+
}
|
|
72
|
+
export interface InputNode extends UIBase {
|
|
73
|
+
kind: 'input';
|
|
74
|
+
binding: InputBinding;
|
|
75
|
+
/** Presentation hint only; the runtime infers a control from the field type otherwise. */
|
|
76
|
+
inputHint?: InputHint;
|
|
77
|
+
options?: InputOptionsSource;
|
|
78
|
+
label?: string;
|
|
79
|
+
placeholder?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface ButtonNode extends UIBase {
|
|
82
|
+
kind: 'button';
|
|
83
|
+
label: string | Expression;
|
|
84
|
+
actionId: NodeId;
|
|
85
|
+
/** Keyed by action parameter id. */
|
|
86
|
+
arguments?: Record<string, Expression>;
|
|
87
|
+
destructive?: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface ConditionalNode extends UIBase {
|
|
90
|
+
kind: 'conditional';
|
|
91
|
+
condition: Expression;
|
|
92
|
+
whenTrue: NodeId[];
|
|
93
|
+
whenFalse?: NodeId[];
|
|
94
|
+
}
|
|
95
|
+
export type UINode = ViewNode | ContainerNode | TextNode | RepeatNode | FieldDisplayNode | FormNode | InputNode | ButtonNode | ConditionalNode;
|
|
96
|
+
export declare function isUINode(node: {
|
|
97
|
+
kind: string;
|
|
98
|
+
}): node is UINode;
|
|
99
|
+
/** Child ids declared by a UI node, in render order. */
|
|
100
|
+
export declare function uiChildIds(node: UINode): NodeId[];
|
|
101
|
+
//# sourceMappingURL=ui.d.ts.map
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const UI_NODE_KINDS = [
|
|
2
|
+
'view',
|
|
3
|
+
'container',
|
|
4
|
+
'text',
|
|
5
|
+
'repeat',
|
|
6
|
+
'field-display',
|
|
7
|
+
'form',
|
|
8
|
+
'input',
|
|
9
|
+
'button',
|
|
10
|
+
'conditional',
|
|
11
|
+
];
|
|
12
|
+
export function isUINode(node) {
|
|
13
|
+
return UI_NODE_KINDS.includes(node.kind);
|
|
14
|
+
}
|
|
15
|
+
/** Child ids declared by a UI node, in render order. */
|
|
16
|
+
export function uiChildIds(node) {
|
|
17
|
+
switch (node.kind) {
|
|
18
|
+
case 'view':
|
|
19
|
+
case 'container':
|
|
20
|
+
case 'form':
|
|
21
|
+
return [...node.children];
|
|
22
|
+
case 'repeat':
|
|
23
|
+
return node.emptyTemplateId ? [node.templateId, node.emptyTemplateId] : [node.templateId];
|
|
24
|
+
case 'conditional':
|
|
25
|
+
return [...node.whenTrue, ...(node.whenFalse ?? [])];
|
|
26
|
+
default:
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ValidationIssue } from './diagnostics.js';
|
|
2
|
+
import type { NodeId } from './ids.js';
|
|
3
|
+
import type { SemanticContext } from './infer.js';
|
|
4
|
+
import type { Location } from './location.js';
|
|
5
|
+
export interface LocationValidationOptions {
|
|
6
|
+
/** Node the location belongs to, for diagnostics. */
|
|
7
|
+
ownerId?: NodeId;
|
|
8
|
+
/** Whether the location will be written to. Read-only uses skip writability checks. */
|
|
9
|
+
requireWritable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Structural validation of a location: does every state, field and selector it names
|
|
13
|
+
* exist, do they fit together, and may it be written to?
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateLocation(location: Location, context: SemanticContext, options?: LocationValidationOptions): ValidationIssue[];
|
|
16
|
+
//# sourceMappingURL=validate-location.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { VALIDATION_CODES } from './diagnostics.js';
|
|
2
|
+
import { inferLocationType, locationCapabilities } from './infer.js';
|
|
3
|
+
import { locationRootStateId } from './location.js';
|
|
4
|
+
/**
|
|
5
|
+
* Structural validation of a location: does every state, field and selector it names
|
|
6
|
+
* exist, do they fit together, and may it be written to?
|
|
7
|
+
*/
|
|
8
|
+
export function validateLocation(location, context, options = {}) {
|
|
9
|
+
const problems = [];
|
|
10
|
+
const ownerId = options.ownerId;
|
|
11
|
+
const report = (code, message, extra = {}) => {
|
|
12
|
+
problems.push({ code, message, ...(ownerId ? { nodeId: ownerId } : {}), ...extra });
|
|
13
|
+
};
|
|
14
|
+
walk(location, context, report);
|
|
15
|
+
if (problems.length === 0 && options.requireWritable) {
|
|
16
|
+
const capabilities = locationCapabilities(location, context);
|
|
17
|
+
if (!capabilities.writable) {
|
|
18
|
+
report(VALIDATION_CODES.derivedStateWrite, `${locationRootStateId(location)} is derived state and cannot be written to; address the state the value is stored in instead`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return problems;
|
|
22
|
+
}
|
|
23
|
+
function walk(location, context, report) {
|
|
24
|
+
switch (location.kind) {
|
|
25
|
+
case 'state': {
|
|
26
|
+
if (!context.getState(location.stateId)) {
|
|
27
|
+
report(VALIDATION_CODES.unknownStateRef, `Location references unknown state ${location.stateId}`);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
case 'field': {
|
|
32
|
+
walk(location.target, context, report);
|
|
33
|
+
const entry = context.getField(location.fieldId);
|
|
34
|
+
if (!entry) {
|
|
35
|
+
report(VALIDATION_CODES.danglingFieldRef, `Location references unknown field ${location.fieldId}`, { fieldId: location.fieldId });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const parent = inferLocationType(location.target, context);
|
|
39
|
+
const resolved = parent?.kind === 'optional' ? parent.valueType : parent;
|
|
40
|
+
if (!resolved) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (resolved.kind !== 'entity') {
|
|
44
|
+
report(VALIDATION_CODES.fieldOnNonEntity, `Field ${location.fieldId} was selected on a ${resolved.kind} value, which has no fields`, { fieldId: location.fieldId });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (entry.entityId !== resolved.entityId) {
|
|
48
|
+
report(VALIDATION_CODES.fieldNotOnEntity, `Field ${location.fieldId} belongs to ${entry.entityId}, not to ${resolved.entityId}`, { fieldId: location.fieldId });
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
case 'collection-item': {
|
|
53
|
+
walk(location.collection, context, report);
|
|
54
|
+
const parent = inferLocationType(location.collection, context);
|
|
55
|
+
const resolved = parent?.kind === 'optional' ? parent.valueType : parent;
|
|
56
|
+
if (resolved && resolved.kind !== 'collection') {
|
|
57
|
+
report(VALIDATION_CODES.selectorOnNonCollection, `An item selector was applied to a ${resolved.kind} value`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (location.selector.kind !== 'identity') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const entry = context.getField(location.selector.fieldId);
|
|
64
|
+
if (!entry) {
|
|
65
|
+
report(VALIDATION_CODES.danglingFieldRef, `Item selector references unknown field ${location.selector.fieldId}`, { fieldId: location.selector.fieldId });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const item = resolved?.kind === 'collection' ? resolved.itemType : undefined;
|
|
69
|
+
const itemType = item?.kind === 'optional' ? item.valueType : item;
|
|
70
|
+
if (itemType?.kind === 'entity' && entry.entityId !== itemType.entityId) {
|
|
71
|
+
report(VALIDATION_CODES.identityFieldMismatch, `Item selector uses field ${location.selector.fieldId} of ${entry.entityId}, but the collection holds ${itemType.entityId}`, { fieldId: location.selector.fieldId });
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
report(VALIDATION_CODES.unknownStateRef, `Unknown location kind "${location.kind}"`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ValidationResult } from './diagnostics.js';
|
|
2
|
+
import type { ApplicationGraph } from './graph.js';
|
|
3
|
+
/**
|
|
4
|
+
* Verifies referential integrity of an application graph. An invalid graph must never
|
|
5
|
+
* be executed, so every reference — nodes, fields, edges, expressions, UI children —
|
|
6
|
+
* is resolved here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateGraph(graph: ApplicationGraph): ValidationResult;
|
|
9
|
+
//# sourceMappingURL=validate.d.ts.map
|