@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/graph.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createEdgeId, createNodeId } from './ids.js';
|
|
2
|
+
function clone(value) {
|
|
3
|
+
return structuredClone(value);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The Application Graph is the canonical representation of an application. Reads return
|
|
7
|
+
* deep clones, so a node retrieved from the graph must be written back with
|
|
8
|
+
* `updateNode` for the change to take effect.
|
|
9
|
+
*/
|
|
10
|
+
export class ApplicationGraph {
|
|
11
|
+
data;
|
|
12
|
+
outgoing = new Map();
|
|
13
|
+
incoming = new Map();
|
|
14
|
+
fieldIndex = new Map();
|
|
15
|
+
constructor(id, name, version = '0.3.0') {
|
|
16
|
+
this.data = { id, name, version, nodes: {}, edges: {} };
|
|
17
|
+
}
|
|
18
|
+
get id() {
|
|
19
|
+
return this.data.id;
|
|
20
|
+
}
|
|
21
|
+
get name() {
|
|
22
|
+
return this.data.name;
|
|
23
|
+
}
|
|
24
|
+
get version() {
|
|
25
|
+
return this.data.version;
|
|
26
|
+
}
|
|
27
|
+
addNode(node) {
|
|
28
|
+
const id = (node.id ?? createNodeId(node.kind));
|
|
29
|
+
if (this.data.nodes[id]) {
|
|
30
|
+
throw new Error(`Node ${id} already exists`);
|
|
31
|
+
}
|
|
32
|
+
this.data.nodes[id] = clone({ ...node, id });
|
|
33
|
+
this.indexNodeFields(this.data.nodes[id]);
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
getNode(id) {
|
|
37
|
+
const node = this.data.nodes[id];
|
|
38
|
+
return node ? clone(node) : undefined;
|
|
39
|
+
}
|
|
40
|
+
hasNode(id) {
|
|
41
|
+
return Boolean(this.data.nodes[id]);
|
|
42
|
+
}
|
|
43
|
+
updateNode(node) {
|
|
44
|
+
if (!this.data.nodes[node.id]) {
|
|
45
|
+
throw new Error(`Node ${node.id} does not exist`);
|
|
46
|
+
}
|
|
47
|
+
this.data.nodes[node.id] = clone(node);
|
|
48
|
+
this.rebuildFieldIndex();
|
|
49
|
+
}
|
|
50
|
+
removeNode(id) {
|
|
51
|
+
if (!this.data.nodes[id]) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
delete this.data.nodes[id];
|
|
55
|
+
for (const [edgeKey, edge] of Object.entries(this.data.edges)) {
|
|
56
|
+
if (edge.from === id || edge.to === id) {
|
|
57
|
+
delete this.data.edges[edgeKey];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.rebuildIndexes();
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
getNodesByKind(kind) {
|
|
64
|
+
return Object.values(this.data.nodes)
|
|
65
|
+
.filter((node) => node.kind === kind)
|
|
66
|
+
.map((node) => clone(node));
|
|
67
|
+
}
|
|
68
|
+
listNodes() {
|
|
69
|
+
return Object.values(this.data.nodes).map((node) => clone(node));
|
|
70
|
+
}
|
|
71
|
+
/** Resolves a field id to its owning entity. Fields are globally identifiable. */
|
|
72
|
+
getField(id) {
|
|
73
|
+
const location = this.fieldIndex.get(id);
|
|
74
|
+
return location ? clone(location) : undefined;
|
|
75
|
+
}
|
|
76
|
+
listFields() {
|
|
77
|
+
return [...this.fieldIndex.values()].map((location) => clone(location));
|
|
78
|
+
}
|
|
79
|
+
addEdge(from, to, kind, options = {}) {
|
|
80
|
+
if (!this.data.nodes[from]) {
|
|
81
|
+
throw new Error(`Cannot add edge from missing node ${from}`);
|
|
82
|
+
}
|
|
83
|
+
if (!this.data.nodes[to]) {
|
|
84
|
+
throw new Error(`Cannot add edge to missing node ${to}`);
|
|
85
|
+
}
|
|
86
|
+
const existing = Object.values(this.data.edges).find((edge) => edge.from === from && edge.to === to && edge.kind === kind);
|
|
87
|
+
if (existing) {
|
|
88
|
+
return existing.id;
|
|
89
|
+
}
|
|
90
|
+
const id = options.id ?? createEdgeId();
|
|
91
|
+
const edge = { id, from, to, kind, ...(options.metadata ? { metadata: options.metadata } : {}) };
|
|
92
|
+
this.data.edges[id] = edge;
|
|
93
|
+
this.indexEdge(edge);
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
removeEdge(id) {
|
|
97
|
+
if (!this.data.edges[id]) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
delete this.data.edges[id];
|
|
101
|
+
this.rebuildEdgeIndexes();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
getEdge(id) {
|
|
105
|
+
const edge = this.data.edges[id];
|
|
106
|
+
return edge ? clone(edge) : undefined;
|
|
107
|
+
}
|
|
108
|
+
listEdges() {
|
|
109
|
+
return Object.values(this.data.edges).map((edge) => clone(edge));
|
|
110
|
+
}
|
|
111
|
+
getEdges(nodeId, query = {}) {
|
|
112
|
+
return [...this.getOutgoingEdges(nodeId, query), ...this.getIncomingEdges(nodeId, query)];
|
|
113
|
+
}
|
|
114
|
+
getOutgoingEdges(nodeId, query = {}) {
|
|
115
|
+
return filterEdges(this.outgoing.get(nodeId) ?? [], query).map((edge) => clone(edge));
|
|
116
|
+
}
|
|
117
|
+
getIncomingEdges(nodeId, query = {}) {
|
|
118
|
+
return filterEdges(this.incoming.get(nodeId) ?? [], query).map((edge) => clone(edge));
|
|
119
|
+
}
|
|
120
|
+
toJSON() {
|
|
121
|
+
return clone(this.data);
|
|
122
|
+
}
|
|
123
|
+
serialize() {
|
|
124
|
+
return JSON.stringify(this.data, null, 2);
|
|
125
|
+
}
|
|
126
|
+
restore(input) {
|
|
127
|
+
this.data = typeof input === 'string' ? JSON.parse(input) : clone(input);
|
|
128
|
+
this.rebuildIndexes();
|
|
129
|
+
}
|
|
130
|
+
static deserialize(input) {
|
|
131
|
+
const data = typeof input === 'string' ? JSON.parse(input) : clone(input);
|
|
132
|
+
const graph = new ApplicationGraph(data.id, data.name, data.version);
|
|
133
|
+
graph.restore(data);
|
|
134
|
+
return graph;
|
|
135
|
+
}
|
|
136
|
+
indexEdge(edge) {
|
|
137
|
+
const outgoing = this.outgoing.get(edge.from) ?? [];
|
|
138
|
+
outgoing.push(edge);
|
|
139
|
+
this.outgoing.set(edge.from, outgoing);
|
|
140
|
+
const incoming = this.incoming.get(edge.to) ?? [];
|
|
141
|
+
incoming.push(edge);
|
|
142
|
+
this.incoming.set(edge.to, incoming);
|
|
143
|
+
}
|
|
144
|
+
indexNodeFields(node) {
|
|
145
|
+
if (node.kind !== 'entity') {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const field of node.fields) {
|
|
149
|
+
this.fieldIndex.set(field.id, { entityId: node.id, field });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
rebuildFieldIndex() {
|
|
153
|
+
this.fieldIndex.clear();
|
|
154
|
+
for (const node of Object.values(this.data.nodes)) {
|
|
155
|
+
this.indexNodeFields(node);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
rebuildEdgeIndexes() {
|
|
159
|
+
this.outgoing.clear();
|
|
160
|
+
this.incoming.clear();
|
|
161
|
+
for (const edge of Object.values(this.data.edges)) {
|
|
162
|
+
this.indexEdge(edge);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
rebuildIndexes() {
|
|
166
|
+
this.rebuildEdgeIndexes();
|
|
167
|
+
this.rebuildFieldIndex();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function filterEdges(edges, query) {
|
|
171
|
+
if (!query.kinds) {
|
|
172
|
+
return edges;
|
|
173
|
+
}
|
|
174
|
+
const kinds = new Set(query.kinds);
|
|
175
|
+
return edges.filter((edge) => kinds.has(edge.kind));
|
|
176
|
+
}
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic identifiers are branded so that node, field and edge references cannot be
|
|
3
|
+
* mixed accidentally. The brands are erased at runtime; JSON round-trips are plain
|
|
4
|
+
* strings and must be re-branded through the helpers below.
|
|
5
|
+
*/
|
|
6
|
+
export type NodeId = string & {
|
|
7
|
+
readonly __brand: 'NodeId';
|
|
8
|
+
};
|
|
9
|
+
export type FieldId = string & {
|
|
10
|
+
readonly __brand: 'FieldId';
|
|
11
|
+
};
|
|
12
|
+
export type EdgeId = string & {
|
|
13
|
+
readonly __brand: 'EdgeId';
|
|
14
|
+
};
|
|
15
|
+
export declare function randomHex(bytes?: number): string;
|
|
16
|
+
export declare function nodeId(value: string): NodeId;
|
|
17
|
+
export declare function fieldId(value: string): FieldId;
|
|
18
|
+
export declare function edgeId(value: string): EdgeId;
|
|
19
|
+
export declare function createNodeId(prefix?: string): NodeId;
|
|
20
|
+
export declare function createFieldId(prefix?: string): FieldId;
|
|
21
|
+
export declare function createEdgeId(prefix?: string): EdgeId;
|
|
22
|
+
//# sourceMappingURL=ids.d.ts.map
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
export function randomHex(bytes = 4) {
|
|
3
|
+
return randomBytes(bytes).toString('hex');
|
|
4
|
+
}
|
|
5
|
+
export function nodeId(value) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function fieldId(value) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function edgeId(value) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function createNodeId(prefix = 'node') {
|
|
15
|
+
return `${prefix}_${randomHex(6)}`;
|
|
16
|
+
}
|
|
17
|
+
export function createFieldId(prefix = 'field') {
|
|
18
|
+
return `${prefix}_${randomHex(6)}`;
|
|
19
|
+
}
|
|
20
|
+
export function createEdgeId(prefix = 'edge') {
|
|
21
|
+
return `${prefix}_${randomHex(6)}`;
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './ids.js';
|
|
2
|
+
export * from './diagnostics.js';
|
|
3
|
+
export * from './location.js';
|
|
4
|
+
export * from './type-ref.js';
|
|
5
|
+
export * from './expressions.js';
|
|
6
|
+
export * from './nodes.js';
|
|
7
|
+
export * from './ui.js';
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './graph.js';
|
|
10
|
+
export * from './infer.js';
|
|
11
|
+
export * from './context.js';
|
|
12
|
+
export * from './validate-location.js';
|
|
13
|
+
export * from './validate.js';
|
|
14
|
+
export * from './derive-edges.js';
|
|
15
|
+
export * from './ir.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './ids.js';
|
|
2
|
+
export * from './diagnostics.js';
|
|
3
|
+
export * from './location.js';
|
|
4
|
+
export * from './type-ref.js';
|
|
5
|
+
export * from './expressions.js';
|
|
6
|
+
export * from './nodes.js';
|
|
7
|
+
export * from './ui.js';
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './graph.js';
|
|
10
|
+
export * from './infer.js';
|
|
11
|
+
export * from './context.js';
|
|
12
|
+
export * from './validate-location.js';
|
|
13
|
+
export * from './validate.js';
|
|
14
|
+
export * from './derive-edges.js';
|
|
15
|
+
export * from './ir.js';
|
package/dist/infer.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Expression } from './expressions.js';
|
|
2
|
+
import type { FieldId, NodeId } from './ids.js';
|
|
3
|
+
import type { EntityDef, StateDef } from './nodes.js';
|
|
4
|
+
import type { FieldIndexEntry } from './graph.js';
|
|
5
|
+
import type { Location } from './location.js';
|
|
6
|
+
import type { TypeRef } from './type-ref.js';
|
|
7
|
+
/**
|
|
8
|
+
* The lookups static analysis needs. Both an authoring graph and a compiled IR can
|
|
9
|
+
* provide them, so validation and the runtime reason about locations the same way.
|
|
10
|
+
*/
|
|
11
|
+
export interface SemanticContext {
|
|
12
|
+
getState(id: NodeId): StateDef | undefined;
|
|
13
|
+
getEntity(id: NodeId): EntityDef | undefined;
|
|
14
|
+
getField(id: FieldId): FieldIndexEntry | undefined;
|
|
15
|
+
/** Type of an action or route parameter, where declared. */
|
|
16
|
+
getParameterType?(id: NodeId): TypeRef | undefined;
|
|
17
|
+
/** Name of any node, for human-readable rendering only. */
|
|
18
|
+
getName?(id: NodeId): string | undefined;
|
|
19
|
+
}
|
|
20
|
+
export interface LocationCapabilities {
|
|
21
|
+
readable: boolean;
|
|
22
|
+
writable: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** The type of the value a location addresses, where it can be determined statically. */
|
|
25
|
+
export declare function inferLocationType(location: Location, context: SemanticContext): TypeRef | undefined;
|
|
26
|
+
/** Derived state is readable but never writable; everything else follows its root. */
|
|
27
|
+
export declare function locationCapabilities(location: Location, context: SemanticContext): LocationCapabilities;
|
|
28
|
+
/**
|
|
29
|
+
* A best-effort type for an expression. Returns undefined wherever the type depends on
|
|
30
|
+
* an iteration scope or another value only known at run time — 0.3 deliberately stops
|
|
31
|
+
* short of a complete static type checker.
|
|
32
|
+
*/
|
|
33
|
+
export declare function inferExpressionType(expression: Expression, context: SemanticContext): TypeRef | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* True when a value of `value` type clearly cannot be stored at a `target` type. Unknown
|
|
36
|
+
* types are never reported: this rejects obvious mistakes, not everything questionable.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isObviouslyIncompatible(target: TypeRef | undefined, value: TypeRef | undefined): boolean;
|
|
39
|
+
/** Renders a location for people. The stored representation stays id-based. */
|
|
40
|
+
export declare function formatLocation(location: Location, context: SemanticContext): string;
|
|
41
|
+
//# sourceMappingURL=infer.d.ts.map
|
package/dist/infer.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { entityType, optionalType, primitiveType } from './type-ref.js';
|
|
2
|
+
function unwrap(type) {
|
|
3
|
+
return type?.kind === 'optional' ? unwrap(type.valueType) : type;
|
|
4
|
+
}
|
|
5
|
+
/** The type of the value a location addresses, where it can be determined statically. */
|
|
6
|
+
export function inferLocationType(location, context) {
|
|
7
|
+
switch (location.kind) {
|
|
8
|
+
case 'state':
|
|
9
|
+
return context.getState(location.stateId)?.valueType;
|
|
10
|
+
case 'field': {
|
|
11
|
+
const parent = unwrap(inferLocationType(location.target, context));
|
|
12
|
+
if (parent?.kind === 'entity') {
|
|
13
|
+
const entity = context.getEntity(parent.entityId);
|
|
14
|
+
const field = entity?.fields.find((candidate) => candidate.id === location.fieldId);
|
|
15
|
+
if (field) {
|
|
16
|
+
return field.valueType;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return parent === undefined ? context.getField(location.fieldId)?.field.valueType : undefined;
|
|
21
|
+
}
|
|
22
|
+
case 'collection-item': {
|
|
23
|
+
const parent = unwrap(inferLocationType(location.collection, context));
|
|
24
|
+
return parent?.kind === 'collection' ? parent.itemType : undefined;
|
|
25
|
+
}
|
|
26
|
+
default:
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Derived state is readable but never writable; everything else follows its root. */
|
|
31
|
+
export function locationCapabilities(location, context) {
|
|
32
|
+
const root = rootState(location, context);
|
|
33
|
+
if (!root) {
|
|
34
|
+
return { readable: false, writable: false };
|
|
35
|
+
}
|
|
36
|
+
return { readable: true, writable: root.derivation === undefined };
|
|
37
|
+
}
|
|
38
|
+
function rootState(location, context) {
|
|
39
|
+
switch (location.kind) {
|
|
40
|
+
case 'state':
|
|
41
|
+
return context.getState(location.stateId);
|
|
42
|
+
case 'field':
|
|
43
|
+
return rootState(location.target, context);
|
|
44
|
+
case 'collection-item':
|
|
45
|
+
return rootState(location.collection, context);
|
|
46
|
+
default:
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A best-effort type for an expression. Returns undefined wherever the type depends on
|
|
52
|
+
* an iteration scope or another value only known at run time — 0.3 deliberately stops
|
|
53
|
+
* short of a complete static type checker.
|
|
54
|
+
*/
|
|
55
|
+
export function inferExpressionType(expression, context) {
|
|
56
|
+
switch (expression.kind) {
|
|
57
|
+
case 'literal': {
|
|
58
|
+
const value = expression.value;
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
return primitiveType('string');
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'number') {
|
|
63
|
+
return primitiveType('number');
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'boolean') {
|
|
66
|
+
return primitiveType('boolean');
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
case 'ref': {
|
|
71
|
+
const state = context.getState(expression.targetId);
|
|
72
|
+
if (state) {
|
|
73
|
+
return state.valueType;
|
|
74
|
+
}
|
|
75
|
+
const entity = context.getEntity(expression.targetId);
|
|
76
|
+
if (entity) {
|
|
77
|
+
return entityType(entity.id);
|
|
78
|
+
}
|
|
79
|
+
return context.getParameterType?.(expression.targetId);
|
|
80
|
+
}
|
|
81
|
+
case 'field': {
|
|
82
|
+
const source = unwrap(inferExpressionType(expression.source, context));
|
|
83
|
+
if (source?.kind === 'entity') {
|
|
84
|
+
const entity = context.getEntity(source.entityId);
|
|
85
|
+
return entity?.fields.find((candidate) => candidate.id === expression.fieldId)?.valueType;
|
|
86
|
+
}
|
|
87
|
+
return source === undefined ? context.getField(expression.fieldId)?.field.valueType : undefined;
|
|
88
|
+
}
|
|
89
|
+
case 'object':
|
|
90
|
+
return expression.entityId ? entityType(expression.entityId) : undefined;
|
|
91
|
+
case 'binary':
|
|
92
|
+
return ['add', 'subtract', 'multiply', 'divide'].includes(expression.operator)
|
|
93
|
+
? primitiveType('number')
|
|
94
|
+
: primitiveType('boolean');
|
|
95
|
+
case 'unary':
|
|
96
|
+
return expression.operator === 'not' ? primitiveType('boolean') : primitiveType('number');
|
|
97
|
+
case 'call':
|
|
98
|
+
switch (expression.function) {
|
|
99
|
+
case 'required':
|
|
100
|
+
case 'is-empty':
|
|
101
|
+
case 'contains':
|
|
102
|
+
case 'one-of':
|
|
103
|
+
return primitiveType('boolean');
|
|
104
|
+
case 'length':
|
|
105
|
+
case 'count':
|
|
106
|
+
return primitiveType('number');
|
|
107
|
+
case 'concat':
|
|
108
|
+
case 'lowercase':
|
|
109
|
+
case 'to-string':
|
|
110
|
+
case 'uuid':
|
|
111
|
+
return primitiveType('string');
|
|
112
|
+
case 'now':
|
|
113
|
+
return primitiveType('datetime');
|
|
114
|
+
default:
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
case 'filter':
|
|
118
|
+
return inferExpressionType(expression.source, context);
|
|
119
|
+
case 'find': {
|
|
120
|
+
const source = unwrap(inferExpressionType(expression.source, context));
|
|
121
|
+
return source?.kind === 'collection' ? optionalType(source.itemType) : undefined;
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* True when a value of `value` type clearly cannot be stored at a `target` type. Unknown
|
|
129
|
+
* types are never reported: this rejects obvious mistakes, not everything questionable.
|
|
130
|
+
*/
|
|
131
|
+
export function isObviouslyIncompatible(target, value) {
|
|
132
|
+
const wanted = unwrap(target);
|
|
133
|
+
const given = unwrap(value);
|
|
134
|
+
if (!wanted || !given) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (wanted.kind !== given.kind) {
|
|
138
|
+
// An enum accepts strings, and a date is carried as a string.
|
|
139
|
+
if (wanted.kind === 'enum' && given.kind === 'primitive' && given.primitive === 'string') {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (given.kind === 'enum' && wanted.kind === 'primitive' && wanted.primitive === 'string') {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (wanted.kind === 'primitive' && given.kind === 'primitive') {
|
|
148
|
+
const interchangeable = new Set(['string', 'date', 'datetime']);
|
|
149
|
+
if (interchangeable.has(wanted.primitive) && interchangeable.has(given.primitive)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return wanted.primitive !== given.primitive;
|
|
153
|
+
}
|
|
154
|
+
if (wanted.kind === 'entity' && given.kind === 'entity') {
|
|
155
|
+
return wanted.entityId !== given.entityId;
|
|
156
|
+
}
|
|
157
|
+
if (wanted.kind === 'collection' && given.kind === 'collection') {
|
|
158
|
+
return isObviouslyIncompatible(wanted.itemType, given.itemType);
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
/** Renders a location for people. The stored representation stays id-based. */
|
|
163
|
+
export function formatLocation(location, context) {
|
|
164
|
+
const name = (id) => context.getName?.(id) ?? id;
|
|
165
|
+
const fieldName = (id) => context.getField(id)?.field.name ?? id;
|
|
166
|
+
switch (location.kind) {
|
|
167
|
+
case 'state':
|
|
168
|
+
return name(location.stateId);
|
|
169
|
+
case 'field':
|
|
170
|
+
return `${formatLocation(location.target, context)} → ${fieldName(location.fieldId)}`;
|
|
171
|
+
case 'collection-item': {
|
|
172
|
+
const parent = formatLocation(location.collection, context);
|
|
173
|
+
if (location.selector.kind === 'identity') {
|
|
174
|
+
const value = location.selector.value.kind === 'ref'
|
|
175
|
+
? name(location.selector.value.targetId)
|
|
176
|
+
: location.selector.value.kind === 'literal'
|
|
177
|
+
? JSON.stringify(location.selector.value.value)
|
|
178
|
+
: 'expression';
|
|
179
|
+
return `${parent} → [${fieldName(location.selector.fieldId)} = ${value}]`;
|
|
180
|
+
}
|
|
181
|
+
return `${parent} → [index]`;
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
return 'unknown location';
|
|
185
|
+
}
|
|
186
|
+
}
|
package/dist/ir.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FieldId, NodeId } from './ids.js';
|
|
2
|
+
import type { ActionDef, ConstraintDef, EntityDef, GraphEdge, RouteParameter, StateDef } from './nodes.js';
|
|
3
|
+
import type { FieldIndexEntry } from './graph.js';
|
|
4
|
+
import type { UINode } from './ui.js';
|
|
5
|
+
import type { AnyNode } from './types.js';
|
|
6
|
+
import type { TypeRef } from './type-ref.js';
|
|
7
|
+
export interface RouteSegment {
|
|
8
|
+
kind: 'static' | 'parameter';
|
|
9
|
+
value: string;
|
|
10
|
+
parameterId?: NodeId;
|
|
11
|
+
}
|
|
12
|
+
export interface CompiledRoute {
|
|
13
|
+
id: NodeId;
|
|
14
|
+
path: string;
|
|
15
|
+
viewId: NodeId;
|
|
16
|
+
segments: RouteSegment[];
|
|
17
|
+
parameters: RouteParameter[];
|
|
18
|
+
/** Number of dynamic segments; routes are matched most-specific first. */
|
|
19
|
+
specificity: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The normalized form a compiler hands to a runtime: the same semantics as the graph,
|
|
23
|
+
* with references resolved and lookups pre-indexed. It is the shared contract between
|
|
24
|
+
* `@cynodia/axiom-compiler` and `@cynodia/axiom-runtime`, which is why it lives in core.
|
|
25
|
+
*/
|
|
26
|
+
export interface ApplicationIR {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
version: string;
|
|
30
|
+
nodes: Record<NodeId, AnyNode>;
|
|
31
|
+
fields: Record<FieldId, FieldIndexEntry>;
|
|
32
|
+
entities: EntityDef[];
|
|
33
|
+
states: StateDef[];
|
|
34
|
+
actions: Record<NodeId, ActionDef>;
|
|
35
|
+
uiNodes: Record<NodeId, UINode>;
|
|
36
|
+
constraints: ConstraintDef[];
|
|
37
|
+
routes: CompiledRoute[];
|
|
38
|
+
edges: GraphEdge[];
|
|
39
|
+
/**
|
|
40
|
+
* The type each input's bound location addresses, resolved during normalization so a
|
|
41
|
+
* runtime never has to re-derive it.
|
|
42
|
+
*/
|
|
43
|
+
locationTypes: Record<NodeId, TypeRef>;
|
|
44
|
+
/**
|
|
45
|
+
* The state each input's bound location is rooted in. A runtime uses it to tell a write
|
|
46
|
+
* to canonical application state from a write to a draft.
|
|
47
|
+
*/
|
|
48
|
+
locationRoots: Record<NodeId, NodeId>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=ir.d.ts.map
|
package/dist/ir.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Expression } from './expressions.js';
|
|
2
|
+
import type { FieldId, NodeId } from './ids.js';
|
|
3
|
+
/**
|
|
4
|
+
* A Location is an addressable position in application state: where a value lives, as
|
|
5
|
+
* opposed to an Expression, which says what a value is. Nothing writable is ever
|
|
6
|
+
* expressed as an Expression, so no mutation depends on JavaScript object identity.
|
|
7
|
+
*/
|
|
8
|
+
export type Location = StateLocation | FieldLocation | CollectionItemLocation;
|
|
9
|
+
export interface StateLocation {
|
|
10
|
+
kind: 'state';
|
|
11
|
+
stateId: NodeId;
|
|
12
|
+
}
|
|
13
|
+
export interface FieldLocation {
|
|
14
|
+
kind: 'field';
|
|
15
|
+
target: Location;
|
|
16
|
+
fieldId: FieldId;
|
|
17
|
+
}
|
|
18
|
+
export interface CollectionItemLocation {
|
|
19
|
+
kind: 'collection-item';
|
|
20
|
+
collection: Location;
|
|
21
|
+
selector: CollectionSelector;
|
|
22
|
+
}
|
|
23
|
+
export type CollectionSelector = IdentitySelector | IndexSelector;
|
|
24
|
+
/** Addresses an item by the value of its identity field. Preferred over an index. */
|
|
25
|
+
export interface IdentitySelector {
|
|
26
|
+
kind: 'identity';
|
|
27
|
+
fieldId: FieldId;
|
|
28
|
+
value: Expression;
|
|
29
|
+
}
|
|
30
|
+
export interface IndexSelector {
|
|
31
|
+
kind: 'index';
|
|
32
|
+
index: Expression;
|
|
33
|
+
}
|
|
34
|
+
export declare function stateLocation(stateId: NodeId): StateLocation;
|
|
35
|
+
export declare function fieldLocation(target: Location, fieldId: FieldId): FieldLocation;
|
|
36
|
+
export declare function itemLocation(collection: Location, selector: CollectionSelector): CollectionItemLocation;
|
|
37
|
+
export declare function identitySelector(fieldId: FieldId, value: Expression): IdentitySelector;
|
|
38
|
+
export declare function indexSelector(index: Expression): IndexSelector;
|
|
39
|
+
/** Convenience for the common shape: one field of one item of a collection state. */
|
|
40
|
+
export declare function itemFieldLocation(stateId: NodeId, identityFieldId: FieldId, identityValue: Expression, fieldId: FieldId): FieldLocation;
|
|
41
|
+
/** The state node every location is ultimately rooted in. */
|
|
42
|
+
export declare function locationRootStateId(location: Location): NodeId;
|
|
43
|
+
/**
|
|
44
|
+
* Expressions embedded in a location — selector values and indexes. These are read
|
|
45
|
+
* dependencies of whatever uses the location.
|
|
46
|
+
*/
|
|
47
|
+
export declare function locationExpressions(location: Location): Expression[];
|
|
48
|
+
/** Fields a write through this location touches, outermost first. */
|
|
49
|
+
export declare function locationFieldIds(location: Location): FieldId[];
|
|
50
|
+
/** Fields a location reads in order to address itself, such as identity selectors. */
|
|
51
|
+
export declare function locationSelectorFieldIds(location: Location): FieldId[];
|
|
52
|
+
/**
|
|
53
|
+
* Structural equality. Two locations that address the same position are equal even
|
|
54
|
+
* though the value a selector expression produces is only known at run time.
|
|
55
|
+
*/
|
|
56
|
+
export declare function locationsEqual(left: Location, right: Location): boolean;
|
|
57
|
+
//# sourceMappingURL=location.d.ts.map
|
package/dist/location.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function stateLocation(stateId) {
|
|
2
|
+
return { kind: 'state', stateId };
|
|
3
|
+
}
|
|
4
|
+
export function fieldLocation(target, fieldId) {
|
|
5
|
+
return { kind: 'field', target, fieldId };
|
|
6
|
+
}
|
|
7
|
+
export function itemLocation(collection, selector) {
|
|
8
|
+
return { kind: 'collection-item', collection, selector };
|
|
9
|
+
}
|
|
10
|
+
export function identitySelector(fieldId, value) {
|
|
11
|
+
return { kind: 'identity', fieldId, value };
|
|
12
|
+
}
|
|
13
|
+
export function indexSelector(index) {
|
|
14
|
+
return { kind: 'index', index };
|
|
15
|
+
}
|
|
16
|
+
/** Convenience for the common shape: one field of one item of a collection state. */
|
|
17
|
+
export function itemFieldLocation(stateId, identityFieldId, identityValue, fieldId) {
|
|
18
|
+
return fieldLocation(itemLocation(stateLocation(stateId), identitySelector(identityFieldId, identityValue)), fieldId);
|
|
19
|
+
}
|
|
20
|
+
/** The state node every location is ultimately rooted in. */
|
|
21
|
+
export function locationRootStateId(location) {
|
|
22
|
+
switch (location.kind) {
|
|
23
|
+
case 'state':
|
|
24
|
+
return location.stateId;
|
|
25
|
+
case 'field':
|
|
26
|
+
return locationRootStateId(location.target);
|
|
27
|
+
case 'collection-item':
|
|
28
|
+
return locationRootStateId(location.collection);
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`Unknown location kind "${location.kind}"`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Expressions embedded in a location — selector values and indexes. These are read
|
|
35
|
+
* dependencies of whatever uses the location.
|
|
36
|
+
*/
|
|
37
|
+
export function locationExpressions(location) {
|
|
38
|
+
switch (location.kind) {
|
|
39
|
+
case 'state':
|
|
40
|
+
return [];
|
|
41
|
+
case 'field':
|
|
42
|
+
return locationExpressions(location.target);
|
|
43
|
+
case 'collection-item': {
|
|
44
|
+
const own = location.selector.kind === 'identity' ? [location.selector.value] : [location.selector.index];
|
|
45
|
+
return [...locationExpressions(location.collection), ...own];
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Fields a write through this location touches, outermost first. */
|
|
52
|
+
export function locationFieldIds(location) {
|
|
53
|
+
return location.kind === 'field' ? [location.fieldId, ...locationFieldIds(location.target)] : [];
|
|
54
|
+
}
|
|
55
|
+
/** Fields a location reads in order to address itself, such as identity selectors. */
|
|
56
|
+
export function locationSelectorFieldIds(location) {
|
|
57
|
+
switch (location.kind) {
|
|
58
|
+
case 'state':
|
|
59
|
+
return [];
|
|
60
|
+
case 'field':
|
|
61
|
+
return locationSelectorFieldIds(location.target);
|
|
62
|
+
case 'collection-item':
|
|
63
|
+
return [
|
|
64
|
+
...locationSelectorFieldIds(location.collection),
|
|
65
|
+
...(location.selector.kind === 'identity' ? [location.selector.fieldId] : []),
|
|
66
|
+
];
|
|
67
|
+
default:
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Structural equality. Two locations that address the same position are equal even
|
|
73
|
+
* though the value a selector expression produces is only known at run time.
|
|
74
|
+
*/
|
|
75
|
+
export function locationsEqual(left, right) {
|
|
76
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
77
|
+
}
|