@astrale-os/kernel-dsl 1.0.0-beta.7 → 1.0.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/v1/builder/class/authoring.d.ts +29 -4
- package/dist/v1/builder/class/authoring.js +15 -5
- package/dist/v1/builder/index.d.ts +1 -1
- package/dist/v1/builder/lowering/class.js +11 -1
- package/dist/v1/builder/lowering/property-keys.d.ts +27 -0
- package/dist/v1/builder/lowering/property-keys.js +108 -0
- package/dist/v1/builder/lowering/schema/compile.js +8 -3
- package/dist/v1/builder/lowering/schema/lifetime.d.ts +11 -0
- package/dist/v1/builder/lowering/schema/lifetime.js +55 -0
- package/dist/v1/builder/schema/diagnostic.d.ts +1 -1
- package/dist/v1/compiled/admission/semantics/cardinality.d.ts +1 -1
- package/dist/v1/compiled/admission/semantics/cardinality.js +16 -1
- package/dist/v1/compiled/admission/semantics/class.js +23 -0
- package/dist/v1/compiled/admission/semantics/core.js +16 -0
- package/dist/v1/compiled/admission/wire/class.js +24 -4
- package/dist/v1/language/class/lifetime.d.ts +48 -11
- package/dist/v1/language/class/lifetime.js +86 -20
- package/dist/v1/language/class/model/class.d.ts +16 -1
- package/dist/v1/language/class/model/edge.d.ts +22 -6
- package/dist/v1/language/class/unique.d.ts +123 -13
- package/dist/v1/language/class/unique.js +238 -27
- package/dist/v1/language/index.d.ts +4 -4
- package/dist/v1/language/index.js +2 -2
- package/dist/v1/schema/diagnostic.d.ts +1 -1
- package/dist/v1/schema/normalization/domain.js +15 -4
- package/dist/v1/schema/validation/class.js +70 -1
- package/dist/v1/schema/validation/core.js +50 -0
- package/dist/v1/schema/validation/domain.structure.js +18 -4
- package/package.json +1 -1
- package/dist/v1/builder/lowering/core-properties.d.ts +0 -10
- package/dist/v1/builder/lowering/core-properties.js +0 -76
|
@@ -1,46 +1,257 @@
|
|
|
1
|
+
import { json } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import { ClassKey } from '../../addressing/class-key.js';
|
|
3
|
+
import { compareUnicode } from '../../addressing/ordering.js';
|
|
4
|
+
import { PropertyKey } from '../../addressing/property-key.js';
|
|
5
|
+
/** The kinds of key fields, in canonical order: the endpoints, the slug, then the Properties. */
|
|
6
|
+
const KINDS = Object.freeze([
|
|
7
|
+
'source',
|
|
8
|
+
'target',
|
|
9
|
+
'slug',
|
|
10
|
+
'property',
|
|
11
|
+
]);
|
|
12
|
+
/** The canonical order of forms: kind by kind, a form before every form it prefixes. */
|
|
13
|
+
const compareForms = compareSequences(compareKinds);
|
|
1
14
|
/**
|
|
2
|
-
* Every
|
|
3
|
-
*
|
|
15
|
+
* Every form a key tuple may take, in canonical order: each nonempty set of field kinds. A Property
|
|
16
|
+
* kind stands for one Property or more, whatever their keys.
|
|
4
17
|
*/
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
18
|
+
export const EDGE_UNIQUE_FORMS = Object.freeze(Array.from({ length: (1 << KINDS.length) - 1 }, (_, index) => Object.freeze(KINDS.filter((_, kind) => ((index + 1) & (1 << kind)) !== 0))).sort(compareForms));
|
|
19
|
+
/** The one form a Node key tuple takes: its fields are Properties, whatever their keys. */
|
|
20
|
+
export const NODE_UNIQUE_FORMS = Object.freeze([
|
|
21
|
+
Object.freeze(['property']),
|
|
8
22
|
]);
|
|
9
|
-
/**
|
|
10
|
-
const
|
|
11
|
-
|
|
23
|
+
/** Every form of every kind, which a backend enforcing any key tuple announces. */
|
|
24
|
+
export const UNIQUE_FORMS = Object.freeze({
|
|
25
|
+
edge: EDGE_UNIQUE_FORMS,
|
|
26
|
+
node: NODE_UNIQUE_FORMS,
|
|
27
|
+
});
|
|
28
|
+
/** No form of either kind: a backend that announces none enforces no key tuple. */
|
|
29
|
+
export const NO_UNIQUE_FORMS = Object.freeze({
|
|
30
|
+
edge: Object.freeze([]),
|
|
31
|
+
node: Object.freeze([]),
|
|
32
|
+
});
|
|
33
|
+
/** The kind of `field`. */
|
|
34
|
+
function kindOf(field) {
|
|
35
|
+
return typeof field === 'string' ? field : 'property';
|
|
36
|
+
}
|
|
37
|
+
function compareKinds(left, right) {
|
|
38
|
+
return KINDS.indexOf(left) - KINDS.indexOf(right);
|
|
39
|
+
}
|
|
40
|
+
/** The canonical order of fields: source, target, slug, then Properties by key. */
|
|
41
|
+
export function compareEdgeUniqueFields(left, right) {
|
|
42
|
+
return (compareKinds(kindOf(left), kindOf(right)) ||
|
|
43
|
+
(typeof left === 'string' || typeof right === 'string'
|
|
44
|
+
? 0
|
|
45
|
+
: compareUnicode(left.property, right.property)));
|
|
46
|
+
}
|
|
47
|
+
/** The canonical order of tuples: field by field, a tuple before every tuple it prefixes. */
|
|
48
|
+
export const compareEdgeUniqueTuples = compareSequences(compareEdgeUniqueFields);
|
|
49
|
+
/** The field `value` spells, detached, or undefined when it spells none. */
|
|
50
|
+
function edgeUniqueField(value) {
|
|
51
|
+
if (value === 'source' || value === 'target' || value === 'slug')
|
|
52
|
+
return value;
|
|
53
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
54
|
+
return undefined;
|
|
55
|
+
const keys = Object.keys(value);
|
|
56
|
+
const property = value.property;
|
|
57
|
+
return keys.length === 1 && keys[0] === 'property' && PropertyKey.is(property)
|
|
58
|
+
? Object.freeze({ property })
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The tuple `value` spells, detached, or undefined when it spells none: a nonempty list of fields,
|
|
63
|
+
* distinct and in canonical order, so one set of fields has one spelling.
|
|
64
|
+
*/
|
|
12
65
|
export function edgeUniqueTuple(value) {
|
|
66
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const fields = [];
|
|
69
|
+
for (const item of value) {
|
|
70
|
+
const field = edgeUniqueField(item);
|
|
71
|
+
if (field === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (fields.length > 0 && compareEdgeUniqueFields(fields.at(-1), field) >= 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
fields.push(field);
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze(fields);
|
|
78
|
+
}
|
|
79
|
+
/** The canonical order of Node tuples: Property by Property, a tuple before every one it prefixes. */
|
|
80
|
+
export const compareNodeUniqueTuples = compareSequences(compareUnicode);
|
|
81
|
+
/**
|
|
82
|
+
* The Node tuple `value` spells, detached, or undefined when it spells none: a nonempty list of
|
|
83
|
+
* Property keys, distinct and in code-point order, so one set of Properties has one spelling.
|
|
84
|
+
*/
|
|
85
|
+
export function nodeUniqueTuple(value) {
|
|
86
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const keys = [];
|
|
89
|
+
for (const key of value) {
|
|
90
|
+
if (!PropertyKey.is(key))
|
|
91
|
+
return undefined;
|
|
92
|
+
if (keys.length > 0 && compareUnicode(keys.at(-1), key) >= 0)
|
|
93
|
+
return undefined;
|
|
94
|
+
keys.push(key);
|
|
95
|
+
}
|
|
96
|
+
return Object.freeze(keys);
|
|
97
|
+
}
|
|
98
|
+
/** The form of `NODE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
|
|
99
|
+
export function nodeUniqueForm(value) {
|
|
100
|
+
return Array.isArray(value) && value.length === 1 && value[0] === 'property'
|
|
101
|
+
? NODE_UNIQUE_FORMS[0]
|
|
102
|
+
: undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether a backend announcing `forms` for Nodes enforces the Node tuple `value` spells: every
|
|
106
|
+
* Node tuple has the one Node form. False when `value` spells no tuple.
|
|
107
|
+
*/
|
|
108
|
+
export function enforcesNodeUniqueTuple(forms, value) {
|
|
109
|
+
return forms.length > 0 && nodeUniqueTuple(value) !== undefined;
|
|
110
|
+
}
|
|
111
|
+
/** The form of `tuple`: the kinds of its fields, each once. */
|
|
112
|
+
export function edgeUniqueFormOf(tuple) {
|
|
113
|
+
const kinds = new Set(tuple.map(kindOf));
|
|
114
|
+
return EDGE_UNIQUE_FORMS.find((form) => form.length === kinds.size && form.every((kind) => kinds.has(kind)));
|
|
115
|
+
}
|
|
116
|
+
/** The form of `EDGE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
|
|
117
|
+
export function edgeUniqueForm(value) {
|
|
13
118
|
if (!Array.isArray(value))
|
|
14
119
|
return undefined;
|
|
15
|
-
return
|
|
120
|
+
return EDGE_UNIQUE_FORMS.find((form) => form.length === value.length && form.every((kind, index) => kind === value[index]));
|
|
16
121
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Whether a backend announcing `forms` enforces the tuple `value` spells: its form is one of them.
|
|
124
|
+
* False when `value` spells no tuple, so a tuple is never taken as enforced by mistake.
|
|
125
|
+
*/
|
|
126
|
+
export function enforcesEdgeUniqueTuple(forms, value) {
|
|
19
127
|
const tuple = edgeUniqueTuple(value);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
128
|
+
if (tuple === undefined)
|
|
129
|
+
return false;
|
|
130
|
+
const form = edgeUniqueFormOf(tuple);
|
|
131
|
+
return forms.some((candidate) => compareForms(candidate, form) === 0);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether a Property may be a key field (D-S1): a single-kind scalar by its ordering facet, a
|
|
135
|
+
* string, number, boolean or instant, that no automatic program writes.
|
|
136
|
+
*/
|
|
137
|
+
export function keyableProperty(ordering, auto) {
|
|
138
|
+
return ordering !== undefined && auto === undefined;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The values of the fields of `tuple` on `edge`, in order, or undefined when one of them is absent,
|
|
142
|
+
* so that an Edge without a slug or without a Property never collides.
|
|
143
|
+
*/
|
|
144
|
+
export function edgeUniqueValues(tuple, edge) {
|
|
145
|
+
return keyValues(tuple, (field) => typeof field === 'string' ? edge[field] : propertyOf(edge.props, field.property));
|
|
31
146
|
}
|
|
32
147
|
/**
|
|
33
|
-
* The value `edge` takes under `tuple`,
|
|
34
|
-
*
|
|
35
|
-
* exactly when they take one value.
|
|
148
|
+
* The value `edge` takes under `tuple`, the canonical JSON of the values of its fields, or
|
|
149
|
+
* undefined when one of them is absent: two distinct Edges of the Classes a tuple covers collide
|
|
150
|
+
* exactly when they take one value. Endpoints compare by NodeId, slugs by exact text and
|
|
151
|
+
* Properties by canonical Value.
|
|
36
152
|
*/
|
|
37
153
|
export function edgeUniqueValue(tuple, edge) {
|
|
154
|
+
return serialized(edgeUniqueValues(tuple, edge));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The values of the Properties of `tuple` on `node`, in order, or undefined when one of them is
|
|
158
|
+
* absent, so that a Node without one of them never collides, like NULL in an SQL UNIQUE.
|
|
159
|
+
*/
|
|
160
|
+
export function nodeUniqueValues(tuple, node) {
|
|
161
|
+
return keyValues(tuple, (key) => propertyOf(node.props, key));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The value `node` takes under `tuple`, the canonical JSON of the values of its Properties, or
|
|
165
|
+
* undefined when one of them is absent: two distinct Nodes of the Classes a tuple covers collide
|
|
166
|
+
* exactly when they take one value. Properties compare by canonical Value, nothing normalized.
|
|
167
|
+
*/
|
|
168
|
+
export function nodeUniqueValue(tuple, node) {
|
|
169
|
+
return serialized(nodeUniqueValues(tuple, node));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The tuples covering `candidate`: those it and each of its ancestors declare, each with the Class
|
|
173
|
+
* declaring it, ancestors found by `resolve`. A descendant never restates them.
|
|
174
|
+
*/
|
|
175
|
+
export function coveringEdgeUniqueTuples(candidate, resolve) {
|
|
176
|
+
return coveringTuples(candidate, resolve);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The Node tuples covering `candidate`: those it and each of its ancestors declare, each with the
|
|
180
|
+
* Class declaring it, ancestors found by `resolve`. A descendant never restates them.
|
|
181
|
+
*/
|
|
182
|
+
export function coveringNodeUniqueTuples(candidate, resolve) {
|
|
183
|
+
return coveringTuples(candidate, resolve);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The keys `edge` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
|
|
187
|
+
* tuple, so two Edges collide exactly when they hold one key.
|
|
188
|
+
*/
|
|
189
|
+
export function edgeUniqueKeys(tuples, edge) {
|
|
190
|
+
return heldKeys(tuples, (fields) => edgeUniqueValue(fields, edge));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The keys `node` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
|
|
194
|
+
* tuple, so two Nodes collide exactly when they hold one key.
|
|
195
|
+
*/
|
|
196
|
+
export function nodeUniqueKeys(tuples, node) {
|
|
197
|
+
return heldKeys(tuples, (fields) => nodeUniqueValue(fields, node));
|
|
198
|
+
}
|
|
199
|
+
/** The values `read` gives the fields of `tuple`, in order, or undefined when one is absent. */
|
|
200
|
+
function keyValues(tuple, read) {
|
|
38
201
|
const values = [];
|
|
39
202
|
for (const field of tuple) {
|
|
40
|
-
const value =
|
|
203
|
+
const value = read(field);
|
|
41
204
|
if (value === undefined)
|
|
42
205
|
return undefined;
|
|
43
206
|
values.push(value);
|
|
44
207
|
}
|
|
45
|
-
return
|
|
208
|
+
return Object.freeze(values);
|
|
209
|
+
}
|
|
210
|
+
/** The value of the Property `key` in `props`, or undefined when it is absent. */
|
|
211
|
+
function propertyOf(props, key) {
|
|
212
|
+
return Object.hasOwn(props, key) ? props[key] : undefined;
|
|
213
|
+
}
|
|
214
|
+
/** The canonical JSON of a key's values, or undefined when it has none. */
|
|
215
|
+
function serialized(values) {
|
|
216
|
+
return values === undefined ? undefined : json.serializeAdmitted(values);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The tuples `candidate` and its ancestors declare, each with its declarer. Only ancestors of the
|
|
220
|
+
* candidate's kind are visited, so every tuple is of that kind.
|
|
221
|
+
*/
|
|
222
|
+
function coveringTuples(candidate, resolve) {
|
|
223
|
+
const tuples = [];
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
const visit = (value) => {
|
|
226
|
+
if (seen.has(value) || value.kind !== candidate.kind)
|
|
227
|
+
return;
|
|
228
|
+
seen.add(value);
|
|
229
|
+
for (const fields of value.unique ?? [])
|
|
230
|
+
tuples.push(Object.freeze({ class: ClassKey.of(value.ref), fields }));
|
|
231
|
+
for (const ref of value.extends) {
|
|
232
|
+
const parent = resolve(ref);
|
|
233
|
+
if (parent !== undefined)
|
|
234
|
+
visit(parent);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
visit(candidate);
|
|
238
|
+
return Object.freeze(tuples);
|
|
239
|
+
}
|
|
240
|
+
/** The keys a fact holds under `tuples`, its value under each tagged by the declaring Class. */
|
|
241
|
+
function heldKeys(tuples, valueOf) {
|
|
242
|
+
return tuples.flatMap(({ class: declaring, fields }) => {
|
|
243
|
+
const value = valueOf(fields);
|
|
244
|
+
return value === undefined ? [] : [JSON.stringify([declaring, fields, value])];
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** The lexicographic order of sequences by `compare`, a sequence before every one it prefixes. */
|
|
248
|
+
function compareSequences(compare) {
|
|
249
|
+
return (left, right) => {
|
|
250
|
+
for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
|
|
251
|
+
const order = compare(left[index], right[index]);
|
|
252
|
+
if (order !== 0)
|
|
253
|
+
return order;
|
|
254
|
+
}
|
|
255
|
+
return left.length - right.length;
|
|
256
|
+
};
|
|
46
257
|
}
|
|
@@ -15,10 +15,10 @@ export type * from './class/model/class.js';
|
|
|
15
15
|
export type { DataDefinition, DataFacet, MediaType } from './class/model/data.js';
|
|
16
16
|
export type * from './class/model/edge.js';
|
|
17
17
|
export { inheritanceOf } from './class/inheritance.js';
|
|
18
|
-
export { edgeLifetimes } from './class/lifetime.js';
|
|
19
|
-
export type {
|
|
20
|
-
export {
|
|
21
|
-
export type { EdgeUniqueFacts } from './class/unique.js';
|
|
18
|
+
export { edgeLifetimes, lifetimeScope } from './class/lifetime.js';
|
|
19
|
+
export type { LifetimeScope } from './class/lifetime.js';
|
|
20
|
+
export { EDGE_UNIQUE_FORMS, NODE_UNIQUE_FORMS, NO_UNIQUE_FORMS, UNIQUE_FORMS, compareEdgeUniqueFields, compareEdgeUniqueTuples, compareNodeUniqueTuples, coveringEdgeUniqueTuples, coveringNodeUniqueTuples, edgeUniqueForm, edgeUniqueFormOf, edgeUniqueKeys, edgeUniqueTuple, edgeUniqueValue, edgeUniqueValues, enforcesEdgeUniqueTuple, enforcesNodeUniqueTuple, keyableProperty, nodeUniqueForm, nodeUniqueKeys, nodeUniqueTuple, nodeUniqueValue, nodeUniqueValues, } from './class/unique.js';
|
|
21
|
+
export type { DeclaredEdgeUniqueTuple, DeclaredNodeUniqueTuple, EdgeUniqueFacts, NodeUniqueFacts, UniqueForms, } from './class/unique.js';
|
|
22
22
|
export type * from './class/model/property.js';
|
|
23
23
|
export type * from './class/resolved.js';
|
|
24
24
|
export type * from './class/facets.js';
|
|
@@ -4,7 +4,7 @@ export { isBinaryInput } from './callable/model/callable.js';
|
|
|
4
4
|
export { valueProgramIncludes } from './value/inclusion/includes.js';
|
|
5
5
|
export { stateMachine } from './state/index.js';
|
|
6
6
|
export { inheritanceOf } from './class/inheritance.js';
|
|
7
|
-
export { edgeLifetimes } from './class/lifetime.js';
|
|
8
|
-
export {
|
|
7
|
+
export { edgeLifetimes, lifetimeScope } from './class/lifetime.js';
|
|
8
|
+
export { EDGE_UNIQUE_FORMS, NODE_UNIQUE_FORMS, NO_UNIQUE_FORMS, UNIQUE_FORMS, compareEdgeUniqueFields, compareEdgeUniqueTuples, compareNodeUniqueTuples, coveringEdgeUniqueTuples, coveringNodeUniqueTuples, edgeUniqueForm, edgeUniqueFormOf, edgeUniqueKeys, edgeUniqueTuple, edgeUniqueValue, edgeUniqueValues, enforcesEdgeUniqueTuple, enforcesNodeUniqueTuple, keyableProperty, nodeUniqueForm, nodeUniqueKeys, nodeUniqueTuple, nodeUniqueValue, nodeUniqueValues, } from './class/unique.js';
|
|
9
9
|
export { PropertyValues } from './class/property-values.js';
|
|
10
10
|
export { isClassRuleChecks, policyVariableClass, policyVariableSelector, policyObjectKey, policyObjectOrigin, projectedMemberClass, } from './policy/model/policy.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SchemaDiagnosticCode = 'DM_CAPABILITY_INVALID' | 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_ADMISSION' | 'DV_CALLABLE_AUTH' | 'DV_CALLABLE_IO' | 'DV_CALLABLE_POLICY_AUTH' | 'DV_CLASS_ABSTRACT' | 'DV_CLASS_ACYCLIC' | 'DV_CLASS_INHERITANCE' | 'DV_CLASS_KIND' | 'DV_CORE_EDGE_CLASS' | 'DV_CORE_EDGE_CONSTRAINTS' | 'DV_CORE_EDGE_KEY' | 'DV_CORE_EDGE_PROPERTIES' | 'DV_CORE_NODE_CLASS' | 'DV_CORE_NODE_PROPERTIES' | 'DV_CORE_REF' | 'DV_DATA_COMPOSITION' | 'DV_DEPENDENCY_ACYCLIC' | 'DV_DEPENDENCY_DIRECT' | 'DV_DEPENDENCY_EXACT' | 'DV_DEPENDENCY_LIMITS' | 'DV_DEPENDENCY_REVISION_COHERENCE' | 'DV_DEPENDENCY_UNIQUE' | 'DV_EDGE_ACCEPTS' | 'DV_EDGE_CLASS_COMPOSITION' | 'DV_EDGE_CONSTRAINT' | 'DV_EDGE_FOREIGN_MINIMUM' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_ENTRYPOINT' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_PROPERTY_IMMUTABLE' | 'DV_PROPERTY_STATE' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | 'PL_BUDGET' | 'PL_COMPOSITION' | 'PL_CONNECTIVITY' | 'PL_REF' | 'PL_REPEAT' | 'PL_TARGET' | 'PL_TYPING' | 'PL_USE' | 'PL_VARIABLE_SCOPE' | 'STRUCTURE_ABSTRACT_POLICY' | 'STRUCTURE_CALLABLE_POLICY_AUTH' | 'STRUCTURE_DUPLICATE_ITEM' | 'STRUCTURE_EDGE_ORIENTATION' | 'STRUCTURE_FORMAT' | 'STRUCTURE_INVALID' | 'STRUCTURE_KEY' | 'STRUCTURE_MAXIMUM' | 'STRUCTURE_MINIMUM' | 'STRUCTURE_MULTIPLE' | 'STRUCTURE_REQUIRED' | 'STRUCTURE_TYPE' | 'STRUCTURE_UNION' | 'STRUCTURE_UNKNOWN_FIELD' | 'STRUCTURE_VALUE' | 'UNSUPPORTED_DOCUMENT_FORMAT' | 'UNSUPPORTED_DSL_VERSION';
|
|
1
|
+
export type SchemaDiagnosticCode = 'DM_CAPABILITY_INVALID' | 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_ADMISSION' | 'DV_CALLABLE_AUTH' | 'DV_CALLABLE_IO' | 'DV_CALLABLE_POLICY_AUTH' | 'DV_CLASS_ABSTRACT' | 'DV_CLASS_ACYCLIC' | 'DV_CLASS_INHERITANCE' | 'DV_CLASS_KIND' | 'DV_CORE_EDGE_CLASS' | 'DV_CORE_EDGE_CONSTRAINTS' | 'DV_CORE_EDGE_KEY' | 'DV_CORE_EDGE_PROPERTIES' | 'DV_CORE_EDGE_UNIQUE' | 'DV_CORE_NODE_CLASS' | 'DV_CORE_NODE_PROPERTIES' | 'DV_CORE_NODE_UNIQUE' | 'DV_CORE_REF' | 'DV_DATA_COMPOSITION' | 'DV_DEPENDENCY_ACYCLIC' | 'DV_DEPENDENCY_DIRECT' | 'DV_DEPENDENCY_EXACT' | 'DV_DEPENDENCY_LIMITS' | 'DV_DEPENDENCY_REVISION_COHERENCE' | 'DV_DEPENDENCY_UNIQUE' | 'DV_EDGE_ACCEPTS' | 'DV_EDGE_CLASS_COMPOSITION' | 'DV_EDGE_CONSTRAINT' | 'DV_EDGE_FOREIGN_MINIMUM' | 'DV_EDGE_LIFETIME' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_EDGE_UNIQUE' | 'DV_ENTRYPOINT' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_NODE_UNIQUE' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_PROPERTY_IMMUTABLE' | 'DV_PROPERTY_STATE' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | 'PL_BUDGET' | 'PL_COMPOSITION' | 'PL_CONNECTIVITY' | 'PL_REF' | 'PL_REPEAT' | 'PL_TARGET' | 'PL_TYPING' | 'PL_USE' | 'PL_VARIABLE_SCOPE' | 'STRUCTURE_ABSTRACT_POLICY' | 'STRUCTURE_CALLABLE_POLICY_AUTH' | 'STRUCTURE_DUPLICATE_ITEM' | 'STRUCTURE_EDGE_ORIENTATION' | 'STRUCTURE_FORMAT' | 'STRUCTURE_INVALID' | 'STRUCTURE_KEY' | 'STRUCTURE_MAXIMUM' | 'STRUCTURE_MINIMUM' | 'STRUCTURE_MULTIPLE' | 'STRUCTURE_REQUIRED' | 'STRUCTURE_TYPE' | 'STRUCTURE_UNION' | 'STRUCTURE_UNKNOWN_FIELD' | 'STRUCTURE_VALUE' | 'UNSUPPORTED_DOCUMENT_FORMAT' | 'UNSUPPORTED_DSL_VERSION';
|
|
2
2
|
export interface SchemaDiagnostic<out Code extends SchemaDiagnosticCode = SchemaDiagnosticCode> {
|
|
3
3
|
readonly code: Code;
|
|
4
4
|
readonly pointer: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { json } from '@astrale-os/kernel-dsl/value';
|
|
2
2
|
import { compareDefinitionRefs, compareUnicode } from '../../addressing/ordering.js';
|
|
3
3
|
import { isBinaryInput } from '../../language/callable/model/callable.js';
|
|
4
|
-
import { compareEdgeUniqueTuples } from '../../language/class/unique.js';
|
|
4
|
+
import { compareEdgeUniqueFields, compareEdgeUniqueTuples, compareNodeUniqueTuples, } from '../../language/class/unique.js';
|
|
5
5
|
import { normalizeClassRule, normalizePolicyChecks, normalizePolicyExpression, } from '../../language/policy/normalization/normalize.js';
|
|
6
6
|
import { admitStateTopology } from '../../language/state/topology.js';
|
|
7
7
|
import { canonicalizeValueSchema } from '../../language/value/profile/canonicalize.js';
|
|
@@ -73,6 +73,7 @@ function canonicalizeClass(value) {
|
|
|
73
73
|
...(value.data === undefined ? {} : { data: { mediaType: value.data.mediaType } }),
|
|
74
74
|
...(value.icon === undefined ? {} : { icon: value.icon }),
|
|
75
75
|
policies: canonicalizeNodePolicies(value.policies),
|
|
76
|
+
...(value.unique === undefined ? {} : { unique: canonicalizeNodeUnique(value.unique) }),
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
return value.orientation === 'directed'
|
|
@@ -82,9 +83,7 @@ function canonicalizeClass(value) {
|
|
|
82
83
|
endpoints: canonicalizeDirectedEndpoints(value.endpoints),
|
|
83
84
|
policies: value.policies,
|
|
84
85
|
constraints: value.constraints,
|
|
85
|
-
...(value.unique === undefined
|
|
86
|
-
? {}
|
|
87
|
-
: { unique: [...value.unique].sort(compareEdgeUniqueTuples) }),
|
|
86
|
+
...(value.unique === undefined ? {} : { unique: canonicalizeUnique(value.unique) }),
|
|
88
87
|
}
|
|
89
88
|
: {
|
|
90
89
|
...base,
|
|
@@ -94,6 +93,18 @@ function canonicalizeClass(value) {
|
|
|
94
93
|
constraints: value.constraints,
|
|
95
94
|
};
|
|
96
95
|
}
|
|
96
|
+
/** A set of Node key tuples, each a set of Property keys, both in their canonical order. */
|
|
97
|
+
function canonicalizeNodeUnique(tuples) {
|
|
98
|
+
return tuples
|
|
99
|
+
.map((tuple) => [...tuple].sort(compareUnicode))
|
|
100
|
+
.sort(compareNodeUniqueTuples);
|
|
101
|
+
}
|
|
102
|
+
/** A set of key tuples, each a set of fields: fields and tuples take their canonical order. */
|
|
103
|
+
function canonicalizeUnique(tuples) {
|
|
104
|
+
return tuples
|
|
105
|
+
.map((tuple) => [...tuple].sort(compareEdgeUniqueFields))
|
|
106
|
+
.sort(compareEdgeUniqueTuples);
|
|
107
|
+
}
|
|
97
108
|
/** A rule keeps one spelling: normalized checks, or the Policy a lone `self` check names. */
|
|
98
109
|
function canonicalizeNodePolicies(value) {
|
|
99
110
|
return {
|
|
@@ -2,16 +2,19 @@ import { Key } from '../../addressing/key.js';
|
|
|
2
2
|
import { definitionRefIndex } from '../../addressing/ordering.js';
|
|
3
3
|
import { isMethodExecutable } from '../../language/callable/executable.js';
|
|
4
4
|
import { classSatisfies as satisfiesClass, isGraphRoot } from '../../language/class/inheritance.js';
|
|
5
|
+
import { lifetimeBreaches, lifetimeScope } from '../../language/class/lifetime.js';
|
|
6
|
+
import { keyableProperty } from '../../language/class/unique.js';
|
|
5
7
|
import { admitStateTopology } from '../../language/state/topology.js';
|
|
6
8
|
import { isBlobValueSchema } from '../../language/value/model/blob.js';
|
|
7
9
|
import { classifyOrdering } from '../../language/value/ordering.js';
|
|
8
10
|
import { diagnostic } from '../diagnostic.js';
|
|
9
|
-
import { createValueSchemaRegistry } from '../resolution/value.registry.js';
|
|
11
|
+
import { createDomainValueSchemaRegistry, createValueSchemaRegistry, } from '../resolution/value.registry.js';
|
|
10
12
|
const formatDefinitionRef = Key.of;
|
|
11
13
|
/** Validate the semantic contracts owned by Class declarations. */
|
|
12
14
|
export function validateDefinitions(environment, analysis) {
|
|
13
15
|
const diagnostics = [...analysis.diagnostics];
|
|
14
16
|
const schema = environment.candidate;
|
|
17
|
+
let scope;
|
|
15
18
|
for (const [name, declaration] of Object.entries(schema.classes)) {
|
|
16
19
|
const ref = localRef(schema.origin, name);
|
|
17
20
|
const pointer = `/classes/${escapePointer(name)}`;
|
|
@@ -26,7 +29,10 @@ export function validateDefinitions(environment, analysis) {
|
|
|
26
29
|
validateParents(environment, ref, declaration.kind, declaration.extends, diagnostics);
|
|
27
30
|
if (declaration.kind === 'edge') {
|
|
28
31
|
validateEdgeContract(environment, ref, declaration, diagnostics);
|
|
32
|
+
scope ??= lifetimeScope([...environment.definitions.values()].map(({ declaration }) => declaration));
|
|
33
|
+
validateEdgeLifetimes(scope, ref, declaration, pointer, diagnostics);
|
|
29
34
|
}
|
|
35
|
+
validateUniqueProperties(environment, analysis, ref, declaration, pointer, diagnostics);
|
|
30
36
|
}
|
|
31
37
|
validateEdgeClassComposition(environment, diagnostics);
|
|
32
38
|
return Object.freeze(diagnostics);
|
|
@@ -132,6 +138,56 @@ function validateEdgeContract(environment, owner, declaration, diagnostics) {
|
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Each Property a key tuple names is one of the Class's own or inherited Properties, and keyable:
|
|
143
|
+
* a single-kind scalar by its ordering facet that no automatic program writes (D-S1). Properties
|
|
144
|
+
* inherit without redefinition, so the key reads one meaning on every descendant it covers. A Node
|
|
145
|
+
* tuple names Properties only; an Edge tuple names them as `{ property }` beside its other fields.
|
|
146
|
+
*/
|
|
147
|
+
function validateUniqueProperties(environment, analysis, owner, declaration, pointer, diagnostics) {
|
|
148
|
+
if (declaration.kind === 'edge' && declaration.orientation !== 'directed')
|
|
149
|
+
return;
|
|
150
|
+
if (declaration.unique === undefined)
|
|
151
|
+
return;
|
|
152
|
+
const properties = analysis.definitions.get(definitionRefIndex(owner))?.properties;
|
|
153
|
+
if (properties === undefined)
|
|
154
|
+
return;
|
|
155
|
+
const code = declaration.kind === 'node' ? 'DV_NODE_UNIQUE' : 'DV_EDGE_UNIQUE';
|
|
156
|
+
const registries = new Map();
|
|
157
|
+
const registryOf = (origin) => {
|
|
158
|
+
if (!registries.has(origin)) {
|
|
159
|
+
const schema = environment.dependencies.get(origin);
|
|
160
|
+
const registry = origin === environment.candidate.origin
|
|
161
|
+
? createValueSchemaRegistry(environment)
|
|
162
|
+
: schema === undefined
|
|
163
|
+
? undefined
|
|
164
|
+
: createDomainValueSchemaRegistry(schema);
|
|
165
|
+
if (registry !== undefined)
|
|
166
|
+
registries.set(origin, registry);
|
|
167
|
+
}
|
|
168
|
+
return registries.get(origin);
|
|
169
|
+
};
|
|
170
|
+
const check = (key, at) => {
|
|
171
|
+
const property = properties.get(key);
|
|
172
|
+
if (property === undefined) {
|
|
173
|
+
diagnostics.push(diagnostic(code, at, `${key} is not a Property of the Class.`));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const registry = registryOf(property.owner.origin);
|
|
177
|
+
const ordering = registry === undefined ? undefined : classifyOrdering(property.schema, registry);
|
|
178
|
+
if (!keyableProperty(ordering, property.auto)) {
|
|
179
|
+
diagnostics.push(diagnostic(code, at, 'A key Property is a string, number, boolean or instant that no automatic program writes.'));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
if (declaration.kind === 'node') {
|
|
183
|
+
declaration.unique.forEach((tuple, t) => tuple.forEach((key, f) => check(key, `${pointer}/unique/${t}/${f}`)));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
declaration.unique.forEach((tuple, t) => tuple.forEach((field, f) => {
|
|
187
|
+
if (typeof field !== 'string')
|
|
188
|
+
check(field.property, `${pointer}/unique/${t}/${f}/property`);
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
135
191
|
function validateEndpoint(environment, owner, endpoint, count, side, diagnostics) {
|
|
136
192
|
// An Edge Class binds only the Nodes of its Domain and its dependents: it cannot oblige the
|
|
137
193
|
// Nodes of a Class another Domain owns to carry its Edges.
|
|
@@ -154,6 +210,19 @@ function validateEndpoint(environment, owner, endpoint, count, side, diagnostics
|
|
|
154
210
|
}
|
|
155
211
|
}
|
|
156
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* The lifetime laws over the lifetimes a Schema declares. What an endpoint derives where it
|
|
215
|
+
* declares nothing is never refused here: a stored Registry decoded again keeps it.
|
|
216
|
+
*/
|
|
217
|
+
function validateEdgeLifetimes(scope, owner, declaration, pointer, diagnostics) {
|
|
218
|
+
const sides = declaration.orientation === 'directed'
|
|
219
|
+
? ['source', 'target']
|
|
220
|
+
: ['first endpoint', 'second endpoint'];
|
|
221
|
+
const keys = declaration.orientation === 'directed' ? ['source', 'target'] : ['0', '1'];
|
|
222
|
+
for (const { index, reason } of lifetimeBreaches(declaration, scope)) {
|
|
223
|
+
diagnostics.push(diagnostic('DV_EDGE_LIFETIME', `${pointer}/endpoints/${keys[index]}/onDelete`, `${formatDefinitionRef(owner)} ${sides[index]} ${reason}`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
157
226
|
/** Edge inheritance is refinement, not a second interface composition mechanism. */
|
|
158
227
|
function validateEdgeClassComposition(environment, diagnostics) {
|
|
159
228
|
for (const resolved of environment.definitions.values()) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Key } from '../../addressing/key.js';
|
|
2
2
|
import { compareUnicode, definitionRefIndex } from '../../addressing/ordering.js';
|
|
3
3
|
import { isMethodExecutable } from '../../language/callable/executable.js';
|
|
4
|
+
import { coveringEdgeUniqueTuples, coveringNodeUniqueTuples, edgeUniqueKeys, nodeUniqueKeys, } from '../../language/class/unique.js';
|
|
4
5
|
import { policyObjectKey, policyObjectOrigin, projectedMemberClass, } from '../../language/policy/model/policy.js';
|
|
5
6
|
import { validateJsonInstance } from '../../language/value/evaluate/validate.js';
|
|
6
7
|
import { diagnostic } from '../diagnostic.js';
|
|
@@ -10,9 +11,34 @@ export function validateCore(environment, analysis, registry) {
|
|
|
10
11
|
const diagnostics = [];
|
|
11
12
|
const nodesByRef = resolveCoreNodes(environment, analysis, registry, diagnostics);
|
|
12
13
|
const edges = resolveCoreEdges(environment, analysis, registry, nodesByRef, diagnostics);
|
|
14
|
+
validateCoreNodeKeys(environment, nodesByRef, diagnostics);
|
|
13
15
|
validateCoreEdgeSet(environment, edges, nodesByRef, diagnostics);
|
|
14
16
|
return Object.freeze(diagnostics);
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* No two Core Nodes hold one key under a tuple covering their Classes, whichever Class or ancestor
|
|
20
|
+
* declares it (DV-NODE-UNIQUE); Activation checks them against stored data.
|
|
21
|
+
*/
|
|
22
|
+
function validateCoreNodeKeys(environment, nodesByRef, diagnostics) {
|
|
23
|
+
const resolve = (ref) => environment.resolveAny(ref)?.declaration;
|
|
24
|
+
const holders = new Map();
|
|
25
|
+
for (const node of nodesByRef.values()) {
|
|
26
|
+
const declaration = resolve(node.classRef);
|
|
27
|
+
if (declaration === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
const pointer = `/core/nodes/${escapePointer(node.slug)}`;
|
|
30
|
+
const keys = nodeUniqueKeys(coveringNodeUniqueTuples(declaration, resolve), {
|
|
31
|
+
props: environment.candidate.core.nodes[node.slug].properties,
|
|
32
|
+
});
|
|
33
|
+
const previous = keys.map((key) => holders.get(key)).find((at) => at !== undefined);
|
|
34
|
+
if (previous !== undefined) {
|
|
35
|
+
diagnostics.push(diagnostic('DV_CORE_NODE_UNIQUE', pointer, `Core Node holds a key the Core Node at ${previous} holds.`));
|
|
36
|
+
}
|
|
37
|
+
for (const key of keys)
|
|
38
|
+
if (!holders.has(key))
|
|
39
|
+
holders.set(key, pointer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
16
42
|
function resolveCoreNodes(environment, analysis, registry, diagnostics) {
|
|
17
43
|
const nodes = new Map();
|
|
18
44
|
for (const [slug, node] of Object.entries(environment.candidate.core.nodes)) {
|
|
@@ -175,6 +201,7 @@ function validateCoreEdgeSet(environment, edges, nodesByRef, diagnostics) {
|
|
|
175
201
|
diagnostics.push(diagnostic('DV_CORE_EDGE_CONSTRAINTS', edge.pointer, 'Core Edge violates noSelf.'));
|
|
176
202
|
}
|
|
177
203
|
}
|
|
204
|
+
validateCoreEdgeKeys(environment, edges, diagnostics);
|
|
178
205
|
for (const { declaration, edges: classEdges } of edgeSlices.values()) {
|
|
179
206
|
if (declaration.constraints.acyclic === true && hasDirectedCycle(classEdges)) {
|
|
180
207
|
diagnostics.push(diagnostic('DV_CORE_EDGE_CONSTRAINTS', '/core/edges', 'Core Edge set violates exact-Class acyclicity.'));
|
|
@@ -182,6 +209,29 @@ function validateCoreEdgeSet(environment, edges, nodesByRef, diagnostics) {
|
|
|
182
209
|
validateCardinality(environment, declaration, classEdges, nodesByRef, diagnostics);
|
|
183
210
|
}
|
|
184
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* No two Core Edges hold one key under a tuple covering their Classes, whichever Class or ancestor
|
|
214
|
+
* declares it (DV-EDGE-UNIQUE). Core Edges have no slug, so only tuples of endpoints and
|
|
215
|
+
* Properties can bind them; Activation checks them against stored data.
|
|
216
|
+
*/
|
|
217
|
+
function validateCoreEdgeKeys(environment, edges, diagnostics) {
|
|
218
|
+
const resolve = (ref) => environment.resolveAny(ref)?.declaration;
|
|
219
|
+
const holders = new Map();
|
|
220
|
+
for (const edge of edges) {
|
|
221
|
+
const keys = edgeUniqueKeys(coveringEdgeUniqueTuples(edge.declaration, resolve), {
|
|
222
|
+
source: edge.source.key,
|
|
223
|
+
target: edge.target.key,
|
|
224
|
+
props: edge.value.properties,
|
|
225
|
+
});
|
|
226
|
+
const previous = keys.map((key) => holders.get(key)).find((pointer) => pointer !== undefined);
|
|
227
|
+
if (previous !== undefined) {
|
|
228
|
+
diagnostics.push(diagnostic('DV_CORE_EDGE_UNIQUE', edge.pointer, `Core Edge holds a key the Core Edge at ${previous} holds.`));
|
|
229
|
+
}
|
|
230
|
+
for (const key of keys)
|
|
231
|
+
if (!holders.has(key))
|
|
232
|
+
holders.set(key, edge.pointer);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
185
235
|
function validateCardinality(environment, declaration, edges, nodesByRef, diagnostics) {
|
|
186
236
|
const subjects = cardinalitySubjects(edges, nodesByRef);
|
|
187
237
|
if (declaration.orientation === 'undirected') {
|
|
@@ -83,6 +83,7 @@ const coreEndpointRefSchema = discriminatedUnion('kind', [
|
|
|
83
83
|
const edgeEndpointBaseShape = {
|
|
84
84
|
role: memberNameSchema,
|
|
85
85
|
accepts: uniqueArray(classRefSchema).min(1),
|
|
86
|
+
onDelete: enumeration(['restrict', 'detach']).optional(),
|
|
86
87
|
};
|
|
87
88
|
const edgeCountSchema = enumeration(['0..*', '1..*', '0..1', '1']);
|
|
88
89
|
const directedEdgeEndpointsSchema = strictObject({
|
|
@@ -98,9 +99,21 @@ const edgeConstraintsSchema = strictObject({
|
|
|
98
99
|
noSelf: literal(true).optional(),
|
|
99
100
|
acyclic: literal(true).optional(),
|
|
100
101
|
});
|
|
101
|
-
// A tuple names
|
|
102
|
+
// A tuple names distinct fields: endpoints, the slug and Properties by key. Normalization puts
|
|
103
|
+
// them, and the tuples, in canonical order (`compareEdgeUniqueFields`). No tuple is spelled by the
|
|
102
104
|
// absence of the field, never by an empty list: one spelling per meaning, one revision.
|
|
103
|
-
const
|
|
105
|
+
const edgeUniqueFieldSchema = union([
|
|
106
|
+
literal('source'),
|
|
107
|
+
literal('target'),
|
|
108
|
+
literal('slug'),
|
|
109
|
+
strictObject({ property: propertyKeySchema }),
|
|
110
|
+
]);
|
|
111
|
+
const edgeUniqueSchema = uniqueArray(uniqueArray(edgeUniqueFieldSchema).min(1), (tuple) =>
|
|
112
|
+
// Two spellings of one set of fields are one tuple, repeated.
|
|
113
|
+
JSON.stringify(tuple.map(canonicalJson).sort())).min(1);
|
|
114
|
+
// A Node tuple names distinct Properties by key, which normalization puts in code-point order, as
|
|
115
|
+
// it does the tuples (`compareNodeUniqueTuples`). Two orders of one set of keys are one tuple.
|
|
116
|
+
const nodeUniqueSchema = uniqueArray(uniqueArray(propertyKeySchema).min(1), (tuple) => JSON.stringify([...tuple].sort())).min(1);
|
|
104
117
|
const policyPatternSubjectSchema = strictObject({ kind: literal('subject') });
|
|
105
118
|
const policyPatternObjectSchema = strictObject({ kind: literal('object') });
|
|
106
119
|
const policyPatternSourceSchema = strictObject({ kind: literal('source') });
|
|
@@ -250,6 +263,7 @@ const nodeClassSchema = strictObject({
|
|
|
250
263
|
data: dataDeclarationSchema.optional(),
|
|
251
264
|
icon: iconSchema.optional(),
|
|
252
265
|
policies: nodeClassPoliciesSchema,
|
|
266
|
+
unique: nodeUniqueSchema.optional(),
|
|
253
267
|
});
|
|
254
268
|
const edgeClassShape = {
|
|
255
269
|
ref: classRefSchema,
|
|
@@ -389,11 +403,11 @@ function isStateTransitions(input) {
|
|
|
389
403
|
function isRecord(input) {
|
|
390
404
|
return input !== null && typeof input === 'object' && !Array.isArray(input);
|
|
391
405
|
}
|
|
392
|
-
function uniqueArray(element) {
|
|
406
|
+
function uniqueArray(element, keyOf = canonicalJson) {
|
|
393
407
|
return array(element).superRefine((values, context) => {
|
|
394
408
|
const firstIndexByValue = new Map();
|
|
395
409
|
values.forEach((value, index) => {
|
|
396
|
-
const key =
|
|
410
|
+
const key = keyOf(value);
|
|
397
411
|
const firstIndex = firstIndexByValue.get(key);
|
|
398
412
|
if (firstIndex === undefined) {
|
|
399
413
|
firstIndexByValue.set(key, index);
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { CoreContent } from '../../language/core/model/core.js';
|
|
2
|
-
import type { DomainSchemaLookup } from '../../schema/dependency/lookup.js';
|
|
3
|
-
import type { DomainSchemaIR } from '../../schema/model/schema.js';
|
|
4
|
-
import type { UnqualifiedCoreContent } from './core.js';
|
|
5
|
-
type UnqualifiedDomainSchema = Omit<DomainSchemaIR, 'core'> & {
|
|
6
|
-
readonly core: UnqualifiedCoreContent;
|
|
7
|
-
};
|
|
8
|
-
/** Lower rich builder shorthand to the canonical full-key Core representation. */
|
|
9
|
-
export declare function canonicalizeCorePropertyKeys(candidate: UnqualifiedDomainSchema, lookup: DomainSchemaLookup): CoreContent;
|
|
10
|
-
export {};
|