@astrale-os/kernel-dsl 1.0.0-beta.12 → 1.0.0-beta.14

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.
Files changed (152) hide show
  1. package/dist/v1/addressing/definition-ref.js +21 -37
  2. package/dist/v1/addressing/key.js +2 -1
  3. package/dist/v1/addressing/ordering.d.ts +2 -0
  4. package/dist/v1/addressing/ordering.js +4 -0
  5. package/dist/v1/addressing/patterns.d.ts +1 -1
  6. package/dist/v1/addressing/patterns.js +2 -6
  7. package/dist/v1/builder/class/authoring.js +5 -28
  8. package/dist/v1/builder/class/property.d.ts +3 -9
  9. package/dist/v1/builder/class/property.js +1 -0
  10. package/dist/v1/builder/lowering/class.js +5 -1
  11. package/dist/v1/builder/lowering/policy.js +1 -0
  12. package/dist/v1/builder/lowering/schema/context.js +2 -4
  13. package/dist/v1/builder/lowering/value/path.js +2 -4
  14. package/dist/v1/builder/lowering/value/zod.js +1 -3
  15. package/dist/v1/builder/policy/authoring.js +3 -2
  16. package/dist/v1/builder/schema/define.d.ts +0 -1
  17. package/dist/v1/builder/schema/diagnostic.d.ts +2 -1
  18. package/dist/v1/builder/schema/diagnostic.js +5 -10
  19. package/dist/v1/builder/schema/projection.d.ts +1 -1
  20. package/dist/v1/builder/value/full-match.d.ts +0 -1
  21. package/dist/v1/builder/value/full-match.js +0 -1
  22. package/dist/v1/bundle/codec.js +4 -6
  23. package/dist/v1/bundle/diagnostic.d.ts +2 -0
  24. package/dist/v1/bundle/diagnostic.js +9 -7
  25. package/dist/v1/bundle/structure.js +4 -6
  26. package/dist/v1/compiled/admission/diagnostic.d.ts +1 -1
  27. package/dist/v1/compiled/admission/diagnostic.js +1 -3
  28. package/dist/v1/compiled/admission/revision.d.ts +9 -1
  29. package/dist/v1/compiled/admission/revision.js +6 -3
  30. package/dist/v1/compiled/admission/semantics/class.d.ts +4 -1
  31. package/dist/v1/compiled/admission/semantics/class.js +4 -8
  32. package/dist/v1/compiled/admission/semantics/core.d.ts +1 -24
  33. package/dist/v1/compiled/admission/semantics/core.js +54 -34
  34. package/dist/v1/compiled/admission/semantics/edge.d.ts +2 -4
  35. package/dist/v1/compiled/admission/semantics/edge.js +6 -65
  36. package/dist/v1/compiled/admission/semantics/policy/callable.js +0 -2
  37. package/dist/v1/compiled/admission/semantics/policy/contracts.d.ts +5 -20
  38. package/dist/v1/compiled/admission/semantics/policy/contracts.js +8 -21
  39. package/dist/v1/compiled/admission/semantics/policy/parity.js +13 -22
  40. package/dist/v1/compiled/admission/semantics/policy/pattern.d.ts +4 -1
  41. package/dist/v1/compiled/admission/semantics/policy/pattern.js +4 -16
  42. package/dist/v1/compiled/admission/semantics/policy/typing.d.ts +2 -24
  43. package/dist/v1/compiled/admission/semantics/policy/typing.js +19 -201
  44. package/dist/v1/compiled/compiler/compile.js +6 -8
  45. package/dist/v1/compiled/diagnostic.js +2 -3
  46. package/dist/v1/compiled/loader/load.d.ts +3 -2
  47. package/dist/v1/compiled/loader/load.js +10 -44
  48. package/dist/v1/domain/create.js +3 -5
  49. package/dist/v1/domain/namespace/member-collection.js +4 -6
  50. package/dist/v1/domain/validation/create.d.ts +4 -2
  51. package/dist/v1/domain/validation/create.js +21 -69
  52. package/dist/v1/language/.spec/schemas/value-schema.schema.json +22 -0
  53. package/dist/v1/language/class/inheritance.d.ts +18 -0
  54. package/dist/v1/language/class/inheritance.js +73 -0
  55. package/dist/v1/language/class/model/class.d.ts +4 -4
  56. package/dist/v1/language/class/model/edge.d.ts +2 -2
  57. package/dist/v1/language/class/model/property.d.ts +16 -7
  58. package/dist/v1/language/class/property-values.js +2 -4
  59. package/dist/v1/language/core/facts.d.ts +72 -0
  60. package/dist/v1/language/core/facts.js +95 -0
  61. package/dist/v1/language/definition/metadata.d.ts +5 -0
  62. package/dist/v1/language/definition/metadata.js +7 -0
  63. package/dist/v1/language/policy/analysis/branches.d.ts +8 -0
  64. package/dist/v1/language/policy/analysis/branches.js +2 -1
  65. package/dist/v1/language/policy/analysis/budgets.d.ts +13 -1
  66. package/dist/v1/language/policy/analysis/budgets.js +104 -0
  67. package/dist/v1/language/policy/analysis/typing.d.ts +48 -0
  68. package/dist/v1/language/policy/analysis/typing.js +338 -0
  69. package/dist/v1/language/policy/model/policy.d.ts +5 -5
  70. package/dist/v1/language/policy/normalization/normalize.js +17 -16
  71. package/dist/v1/language/validation/diagnostic.d.ts +15 -1
  72. package/dist/v1/language/validation/diagnostic.js +21 -0
  73. package/dist/v1/language/validation/rule.d.ts +26 -0
  74. package/dist/v1/language/validation/rule.js +116 -0
  75. package/dist/v1/language/value/compile/fields.js +4 -8
  76. package/dist/v1/language/value/profile/meta-schema.compiled.js +4 -1
  77. package/dist/v1/language/value/profile/rules.js +17 -43
  78. package/dist/v1/language/value/profile/validate.js +1 -10
  79. package/dist/v1/language/value/profile/vocabulary.d.ts +35 -2
  80. package/dist/v1/language/value/profile/vocabulary.js +52 -5
  81. package/dist/v1/language/value/reference/recursion.js +2 -22
  82. package/dist/v1/language/value/reference/registry.index.js +2 -1
  83. package/dist/v1/language/value/syntax/keywords.d.ts +3 -3
  84. package/dist/v1/language/value/syntax/keywords.js +4 -20
  85. package/dist/v1/language/value/syntax/node-path/validate.js +2 -3
  86. package/dist/v1/language/value/syntax/pointer.d.ts +2 -0
  87. package/dist/v1/language/value/syntax/pointer.js +5 -2
  88. package/dist/v1/language/value/syntax/walk.d.ts +2 -0
  89. package/dist/v1/language/value/syntax/walk.js +19 -20
  90. package/dist/v1/schema/.spec/schemas/domain-schema.schema.json +2336 -0
  91. package/dist/v1/schema/analysis/class.js +33 -15
  92. package/dist/v1/schema/compatibility/api.d.ts +1 -1
  93. package/dist/v1/schema/compatibility/coordinate.js +3 -3
  94. package/dist/v1/schema/compatibility/diff.js +2 -4
  95. package/dist/v1/schema/compatibility/meaning/footprint.js +16 -50
  96. package/dist/v1/schema/compatibility/model.d.ts +5 -0
  97. package/dist/v1/schema/compatibility/structure/requirements.js +26 -58
  98. package/dist/v1/schema/compatibility/walk.d.ts +17 -0
  99. package/dist/v1/schema/compatibility/walk.js +41 -0
  100. package/dist/v1/schema/diagnostic.d.ts +17 -3
  101. package/dist/v1/schema/diagnostic.js +14 -11
  102. package/dist/v1/schema/index.d.ts +3 -1
  103. package/dist/v1/schema/index.js +1 -0
  104. package/dist/v1/schema/model/schema.d.ts +19 -1
  105. package/dist/v1/schema/normalization/domain.js +1 -0
  106. package/dist/v1/schema/resolution/closure.js +2 -1
  107. package/dist/v1/schema/resolution/context.js +6 -6
  108. package/dist/v1/schema/rule.d.ts +29 -0
  109. package/dist/v1/schema/rule.js +415 -0
  110. package/dist/v1/schema/validation/callable.d.ts +4 -1
  111. package/dist/v1/schema/validation/callable.js +13 -6
  112. package/dist/v1/schema/validation/class.js +46 -96
  113. package/dist/v1/schema/validation/core.js +68 -202
  114. package/dist/v1/schema/validation/domain.js +1 -10
  115. package/dist/v1/schema/validation/domain.json-schema.d.ts +15 -0
  116. package/dist/v1/schema/validation/domain.json-schema.js +183 -0
  117. package/dist/v1/schema/validation/domain.structure.d.ts +4018 -0
  118. package/dist/v1/schema/validation/domain.structure.js +343 -155
  119. package/dist/v1/schema/validation/json.json-schema.d.ts +24 -0
  120. package/dist/v1/schema/validation/json.json-schema.js +36 -0
  121. package/dist/v1/schema/validation/policy/budgets.js +5 -112
  122. package/dist/v1/schema/validation/policy/context.d.ts +5 -3
  123. package/dist/v1/schema/validation/policy/context.js +5 -3
  124. package/dist/v1/schema/validation/policy/objects.d.ts +1 -1
  125. package/dist/v1/schema/validation/policy/objects.js +6 -14
  126. package/dist/v1/schema/validation/policy/references.js +10 -27
  127. package/dist/v1/schema/validation/policy/typing.d.ts +3 -18
  128. package/dist/v1/schema/validation/policy/typing.js +22 -356
  129. package/dist/v1/schema/validation/policy/uses.d.ts +1 -1
  130. package/dist/v1/schema/validation/policy/uses.js +9 -25
  131. package/dist/v1/schema/validation/policy/validate.js +3 -2
  132. package/dist/v1/schema/validation/value-api.d.ts +12 -1
  133. package/dist/v1/schema/validation/value-api.js +12 -3
  134. package/dist/v1/schema/validation/value.js +8 -10
  135. package/dist/v1/schema/validation/value.json-schema.d.ts +51 -0
  136. package/dist/v1/schema/validation/value.json-schema.js +126 -0
  137. package/dist/v1/schema/validation/view.js +4 -8
  138. package/package.json +3 -1
  139. package/dist/v1/compiled/admission/semantics/cardinality.d.ts +0 -5
  140. package/dist/v1/compiled/admission/semantics/cardinality.js +0 -128
  141. package/dist/v1/compiled/admission/semantics/policy/equality.d.ts +0 -8
  142. package/dist/v1/compiled/admission/semantics/policy/equality.js +0 -27
  143. package/dist/v1/compiled/admission/semantics/policy/measure.d.ts +0 -9
  144. package/dist/v1/compiled/admission/semantics/policy/measure.js +0 -81
  145. package/dist/v1/compiled/admission/semantics/policy/variants.d.ts +0 -10
  146. package/dist/v1/compiled/admission/semantics/policy/variants.js +0 -65
  147. package/dist/v1/language/class/index.d.ts +0 -8
  148. package/dist/v1/language/class/index.js +0 -1
  149. package/dist/v1/language/core/index.d.ts +0 -2
  150. package/dist/v1/language/core/index.js +0 -1
  151. package/dist/v1/language/value/index.d.ts +0 -11
  152. package/dist/v1/language/value/index.js +0 -8
@@ -0,0 +1,338 @@
1
+ import { Key } from '../../../addressing/index.js';
2
+ import { definitionRefIndex } from '../../../addressing/ordering.js';
3
+ import { branchesOf, branchProduct, capBranches, targetMode, termKey, UnionFind, } from './branches.js';
4
+ import { MAX_REPEAT } from './budgets.js';
5
+ import { admittedCheckClasses } from './checks.js';
6
+ export class PolicyTyping {
7
+ scope;
8
+ // Expanded branches of each named Policy, cached by node identity. A Policy's expansion is reused
9
+ // across every composition path that reaches it (and across every use site and object contract,
10
+ // none of which the expansion depends on), so a shared-child composition DAG is expanded in work
11
+ // linear in the number of Policies instead of enumerating every path. The expansion result is
12
+ // independent of the reaching path up to the opaque term-namespace labels, which only ever
13
+ // disambiguate variables within one branch and leave targetMode and branch typing unchanged.
14
+ // Two occurrences of one Policy in a branch share labels only when a Policy reaches it by two
15
+ // paths; for that to merge distinct variables, the Policy expands deeper than MAX_PATTERN_DEPTH.
16
+ #expansions = new Map();
17
+ constructor(scope) {
18
+ this.scope = scope;
19
+ }
20
+ /** The normalized branches of a named Policy, `undefined` when it does not expand. */
21
+ expandedBranches(name, namespace) {
22
+ return this.#expandedBranches(name, new Set(), namespace);
23
+ }
24
+ policyAccepts(name, object) {
25
+ return this.#policyBranchTyping(name, object)?.every(Boolean) ?? false;
26
+ }
27
+ /** Whether the Policy accepts every concrete source and target pair the Edge Class accepts. */
28
+ policyAcceptsEdgeClass(name, declaration) {
29
+ const endpoints = declaration.orientation === 'directed'
30
+ ? [declaration.endpoints.source, declaration.endpoints.target]
31
+ : declaration.endpoints;
32
+ const sources = this.#concreteEndpointClasses(endpoints[0].accepts);
33
+ const targets = this.#concreteEndpointClasses(endpoints[1].accepts);
34
+ return sources.every((source) => targets.every((target) => this.#policyAcceptsEdge(name, { kind: 'exact', ref: source }, { kind: 'exact', ref: target })));
35
+ }
36
+ /**
37
+ * How a check of the Policy types against each contract of its object, `undefined` when the
38
+ * Policy does not expand or one of its branches is typed for no contract.
39
+ */
40
+ checkLeafClasses(name, contracts) {
41
+ const typings = new Map(contracts.map((contract) => [
42
+ contract.kind === 'exact' ? Key.of(contract.ref) : 'default',
43
+ this.#policyBranchTyping(name, contract),
44
+ ]));
45
+ const admitted = admittedCheckClasses(typings);
46
+ return admitted === undefined ? undefined : { accepted: [...typings.keys()], admitted };
47
+ }
48
+ endpointsOverlap(left, right) {
49
+ const seed = [{ kind: 'open', requirements: [] }];
50
+ const afterLeft = this.#applyEndpoint(seed, left);
51
+ return this.#applyEndpoint(afterLeft, right).length > 0;
52
+ }
53
+ branchIsTyped(branch, use) {
54
+ if (targetMode(branch) !== use.kind)
55
+ return false;
56
+ const alternatives = edgeAlternatives(branch.edges, this);
57
+ return alternatives.some((variants) => this.#constraintsAreSatisfiable(branch, variants, use));
58
+ }
59
+ /** Whether each normalized branch of the Policy is typed for an object of this contract. */
60
+ #policyBranchTyping(name, object) {
61
+ const branches = this.#expandedBranches(name, new Set(), `$use:${name}`);
62
+ const use = { kind: 'object', object };
63
+ return branches?.map((branch) => this.branchIsTyped(branch, use));
64
+ }
65
+ #policyAcceptsEdge(name, source, target) {
66
+ const branches = this.#expandedBranches(name, new Set(), `$edge-use:${name}`);
67
+ const use = { kind: 'edge', source, target };
68
+ return branches !== undefined && branches.every((branch) => this.branchIsTyped(branch, use));
69
+ }
70
+ #concreteEndpointClasses(accepts) {
71
+ return this.scope.concreteNodeClasses.filter((candidate) => this.#classSatisfiesEndpoint(candidate, accepts));
72
+ }
73
+ #expandedBranches(name, visiting, namespace) {
74
+ if (visiting.has(name))
75
+ return undefined;
76
+ const cached = this.#expansions.get(name);
77
+ if (cached !== undefined || this.#expansions.has(name))
78
+ return cached;
79
+ const expanded = this.#expandDeclaration(name, visiting, namespace);
80
+ this.#expansions.set(name, expanded);
81
+ return expanded;
82
+ }
83
+ #expandDeclaration(name, visiting, namespace) {
84
+ const declaration = this.scope.normalized.get(name);
85
+ if (declaration === undefined)
86
+ return undefined;
87
+ if ('match' in declaration)
88
+ return branchesOf(declaration.match, namespace);
89
+ const nested = new Set(visiting);
90
+ nested.add(name);
91
+ const refs = 'allOf' in declaration ? declaration.allOf : declaration.anyOf;
92
+ const children = [];
93
+ for (const [index, ref] of refs.entries()) {
94
+ if (!this.scope.isLocalPolicy(ref))
95
+ return undefined;
96
+ const expanded = this.#expandedBranches(ref.name, nested, `${namespace}/${index}:${ref.name}`);
97
+ if (expanded === undefined)
98
+ return undefined;
99
+ children.push(expanded);
100
+ }
101
+ return 'allOf' in declaration ? branchProduct(children) : capBranches(children.flat());
102
+ }
103
+ #constraintsAreSatisfiable(branch, variants, use) {
104
+ const terms = new Set(use.kind === 'object' ? ['subject', 'object'] : ['subject', 'source', 'target']);
105
+ for (const key of branch.variables.keys())
106
+ terms.add(key);
107
+ for (const key of branch.terms.keys())
108
+ terms.add(key);
109
+ for (const edge of branch.edges) {
110
+ terms.add(termKey(edge.pattern.source, edge.namespace));
111
+ terms.add(termKey(edge.pattern.target, edge.namespace));
112
+ }
113
+ const union = new UnionFind(terms);
114
+ for (const [left, right] of branch.equalities)
115
+ union.join(left, right);
116
+ for (const variant of variants) {
117
+ for (const [left, right] of variant.equalities)
118
+ union.join(left, right);
119
+ }
120
+ const constraints = new Map();
121
+ for (const variant of variants) {
122
+ for (const [term, accepts] of variant.constraints) {
123
+ const root = union.find(term);
124
+ const values = constraints.get(root) ?? [];
125
+ values.push(accepts);
126
+ constraints.set(root, values);
127
+ }
128
+ }
129
+ const groups = new Map();
130
+ for (const term of terms) {
131
+ const root = union.find(term);
132
+ const members = groups.get(root) ?? [];
133
+ members.push(term);
134
+ groups.set(root, members);
135
+ }
136
+ for (const [root, members] of groups) {
137
+ const endpointConstraints = constraints.get(root) ?? [];
138
+ let candidates = [{ kind: 'open', requirements: [] }];
139
+ for (const member of members) {
140
+ candidates = this.#applyInitial(candidates, member, branch, use);
141
+ if (candidates.length === 0)
142
+ return false;
143
+ }
144
+ for (const endpoint of endpointConstraints) {
145
+ candidates = this.#applyEndpoint(candidates, endpoint);
146
+ if (candidates.length === 0)
147
+ return false;
148
+ }
149
+ }
150
+ return true;
151
+ }
152
+ #applyInitial(candidates, term, branch, use) {
153
+ if (term === 'subject')
154
+ return this.#applyAnyNodeClass(candidates);
155
+ const contract = targetContract(term, use);
156
+ if (contract !== undefined) {
157
+ if (contract.kind === 'exact')
158
+ return this.#applyExact(candidates, contract.ref);
159
+ if (contract.kind === 'default') {
160
+ return uniqueCandidates(this.scope.allNodeClasses.flatMap((ref) => this.#applyExact(candidates, ref)));
161
+ }
162
+ return candidates;
163
+ }
164
+ const referenced = branch.terms.get(term);
165
+ if (referenced?.kind === 'ref') {
166
+ const contract = this.scope.referenceContract(referenced.ref);
167
+ return contract?.kind === 'exact' ? this.#applyExact(candidates, contract.ref) : [];
168
+ }
169
+ const selector = branch.variables.get(term);
170
+ if (selector === undefined)
171
+ return [];
172
+ if (selector.kind === 'exact')
173
+ return this.#applyExact(candidates, selector.class);
174
+ if (selector.kind === 'any')
175
+ return candidates;
176
+ return this.#applyRequirement(candidates, selector.class);
177
+ }
178
+ #applyExact(candidates, exact) {
179
+ const result = [];
180
+ for (const candidate of candidates) {
181
+ if (candidate.kind === 'exact') {
182
+ if (definitionRefIndex(candidate.ref) === definitionRefIndex(exact))
183
+ result.push(candidate);
184
+ }
185
+ else if (candidate.requirements.every((required) => this.scope.classExtends(exact, required))) {
186
+ result.push({ kind: 'exact', ref: exact });
187
+ }
188
+ }
189
+ return uniqueCandidates(result);
190
+ }
191
+ #applyAnyNodeClass(candidates) {
192
+ return uniqueCandidates(candidates.flatMap((candidate) => candidate.kind === 'exact'
193
+ ? [candidate]
194
+ : this.scope.allNodeClasses
195
+ .filter((ref) => candidate.requirements.every((required) => this.scope.classExtends(ref, required)))
196
+ .map((ref) => ({ kind: 'exact', ref }))));
197
+ }
198
+ #applyRequirement(candidates, required) {
199
+ const result = [];
200
+ for (const candidate of candidates) {
201
+ if (candidate.kind === 'exact') {
202
+ if (this.scope.classExtends(candidate.ref, required))
203
+ result.push(candidate);
204
+ }
205
+ else {
206
+ result.push({
207
+ kind: 'open',
208
+ requirements: uniqueClasses([...candidate.requirements, required]),
209
+ });
210
+ }
211
+ }
212
+ return uniqueCandidates(result);
213
+ }
214
+ #applyEndpoint(candidates, accepts) {
215
+ const result = [];
216
+ for (const candidate of candidates) {
217
+ if (candidate.kind === 'exact') {
218
+ if (this.#classSatisfiesEndpoint(candidate.ref, accepts))
219
+ result.push(candidate);
220
+ continue;
221
+ }
222
+ for (const accept of accepts) {
223
+ result.push({
224
+ kind: 'open',
225
+ requirements: uniqueClasses([...candidate.requirements, accept]),
226
+ });
227
+ }
228
+ }
229
+ return uniqueCandidates(result);
230
+ }
231
+ #classSatisfiesEndpoint(candidate, accepts) {
232
+ return accepts.some((accept) => this.scope.classExtends(candidate, accept));
233
+ }
234
+ }
235
+ function targetContract(term, use) {
236
+ if (use.kind === 'object')
237
+ return term === 'object' ? use.object : undefined;
238
+ if (term === 'source')
239
+ return use.source;
240
+ if (term === 'target')
241
+ return use.target;
242
+ return undefined;
243
+ }
244
+ function edgeAlternatives(edges, typing) {
245
+ let result = [[]];
246
+ for (const edge of edges) {
247
+ const variants = variantsForEdge(edge, typing);
248
+ if (variants.length === 0)
249
+ return [];
250
+ const next = [];
251
+ for (const prefix of result) {
252
+ for (const variant of variants)
253
+ next.push([...prefix, variant]);
254
+ }
255
+ result = next;
256
+ }
257
+ return result;
258
+ }
259
+ function variantsForEdge(use, typing) {
260
+ const edge = use.pattern;
261
+ const declaration = typing.scope.edgeDeclaration(edge.class);
262
+ if (declaration === undefined)
263
+ return [];
264
+ const endpoints = declaration.orientation === 'directed'
265
+ ? [declaration.endpoints.source, declaration.endpoints.target]
266
+ : declaration.endpoints;
267
+ const bounds = edge.repeat ?? { min: 1, max: 1 };
268
+ if (!Number.isInteger(bounds.min) ||
269
+ !Number.isInteger(bounds.max) ||
270
+ bounds.min < 0 ||
271
+ bounds.max < bounds.min ||
272
+ bounds.max > MAX_REPEAT) {
273
+ return [];
274
+ }
275
+ const sourceKey = termKey(edge.source, use.namespace);
276
+ const targetKey = termKey(edge.target, use.namespace);
277
+ const variants = [];
278
+ for (let length = bounds.min; length <= bounds.max; length++) {
279
+ if (length === 0) {
280
+ variants.push({ constraints: [], equalities: [[sourceKey, targetKey]] });
281
+ continue;
282
+ }
283
+ const orientationCount = declaration.orientation === 'undirected' ? 2 ** length : 1;
284
+ for (let mask = 0; mask < orientationCount; mask++) {
285
+ const orientations = Array.from({ length }, (_, index) => declaration.orientation === 'undirected' && (mask & (1 << index)) !== 0);
286
+ let transitionsValid = true;
287
+ for (let index = 0; index + 1 < length; index++) {
288
+ const incoming = endEndpoint(endpoints, orientations[index] ?? false);
289
+ const outgoing = startEndpoint(endpoints, orientations[index + 1] ?? false);
290
+ if (!typing.endpointsOverlap(incoming.accepts, outgoing.accepts)) {
291
+ transitionsValid = false;
292
+ break;
293
+ }
294
+ }
295
+ if (!transitionsValid)
296
+ continue;
297
+ const first = startEndpoint(endpoints, orientations[0] ?? false);
298
+ const last = endEndpoint(endpoints, orientations[length - 1] ?? false);
299
+ variants.push({
300
+ constraints: [
301
+ [sourceKey, first.accepts],
302
+ [targetKey, last.accepts],
303
+ ],
304
+ equalities: [],
305
+ });
306
+ }
307
+ }
308
+ return uniqueEdgeVariants(variants);
309
+ }
310
+ function startEndpoint(endpoints, reversed) {
311
+ return reversed ? endpoints[1] : endpoints[0];
312
+ }
313
+ function endEndpoint(endpoints, reversed) {
314
+ return reversed ? endpoints[0] : endpoints[1];
315
+ }
316
+ function uniqueEdgeVariants(values) {
317
+ const result = new Map();
318
+ for (const value of values) {
319
+ const key = `${value.constraints
320
+ .map(([term, accepts]) => `${term}:${accepts.map(definitionRefIndex).sort().join(',')}`)
321
+ .join('|')}=${value.equalities.map(([left, right]) => `${left}:${right}`).join('|')}`;
322
+ result.set(key, value);
323
+ }
324
+ return [...result.values()];
325
+ }
326
+ function uniqueCandidates(values) {
327
+ const result = new Map();
328
+ for (const value of values) {
329
+ const key = value.kind === 'exact'
330
+ ? `exact:${definitionRefIndex(value.ref)}`
331
+ : `open:${value.requirements.map(definitionRefIndex).sort().join('|')}`;
332
+ result.set(key, value);
333
+ }
334
+ return [...result.values()];
335
+ }
336
+ function uniqueClasses(values) {
337
+ return [...new Map(values.map((value) => [definitionRefIndex(value), value])).values()];
338
+ }
@@ -65,20 +65,20 @@ export interface PolicyEdgePattern {
65
65
  }
66
66
  export type PolicyPatternExpression = PolicyEdgePattern | PolicySameNode<PolicyPatternTerm> | {
67
67
  readonly exists: {
68
- readonly nodes: readonly PolicyVariable[];
68
+ readonly nodes: readonly [PolicyVariable, ...PolicyVariable[]];
69
69
  readonly where: PolicyPatternExpression;
70
70
  };
71
71
  } | {
72
- readonly allOf: readonly PolicyPatternExpression[];
72
+ readonly allOf: readonly [PolicyPatternExpression, ...PolicyPatternExpression[]];
73
73
  } | {
74
- readonly anyOf: readonly PolicyPatternExpression[];
74
+ readonly anyOf: readonly [PolicyPatternExpression, ...PolicyPatternExpression[]];
75
75
  };
76
76
  export type PolicyExpression = {
77
77
  readonly match: PolicyPatternExpression;
78
78
  } | {
79
- readonly allOf: readonly PolicyRef[];
79
+ readonly allOf: readonly [PolicyRef, ...PolicyRef[]];
80
80
  } | {
81
- readonly anyOf: readonly PolicyRef[];
81
+ readonly anyOf: readonly [PolicyRef, ...PolicyRef[]];
82
82
  };
83
83
  export interface PolicyDefinition extends DefinitionBase<PolicyRef> {
84
84
  readonly expression: PolicyExpression;
@@ -6,9 +6,9 @@ export function normalizePolicyExpression(value) {
6
6
  return { match: normalizePolicyPattern(value.match) };
7
7
  }
8
8
  if ('allOf' in value) {
9
- return { allOf: uniqueRefs(value.allOf) };
9
+ return { allOf: operands(uniqueRefs(value.allOf)) };
10
10
  }
11
- return { anyOf: uniqueRefs(value.anyOf) };
11
+ return { anyOf: operands(uniqueRefs(value.anyOf)) };
12
12
  }
13
13
  /**
14
14
  * Passes after the first, while one still changes the Pattern. A pass numbers variables by the
@@ -66,8 +66,7 @@ function unwrapSingleOperands(value) {
66
66
  }));
67
67
  if (children.length === 1)
68
68
  return children[0];
69
- const operands = policyCheckComposition(children);
70
- return all ? { allOf: operands } : { anyOf: operands };
69
+ return all ? { allOf: operands(children) } : { anyOf: operands(children) };
71
70
  }
72
71
  export function normalizePolicyChecks(value) {
73
72
  if ('check' in value)
@@ -79,17 +78,19 @@ export function normalizePolicyChecks(value) {
79
78
  const normalized = normalizePolicyChecks(child);
80
79
  return 'allOf' in normalized ? normalized.allOf : [normalized];
81
80
  });
82
- return { allOf: policyCheckComposition(uniqueJson(children)) };
81
+ return { allOf: operands(uniqueJson(children)) };
83
82
  }
84
83
  const children = value.anyOf.flatMap((child) => {
85
84
  const normalized = normalizePolicyChecks(child);
86
85
  return 'anyOf' in normalized ? normalized.anyOf : [normalized];
87
86
  });
88
- return { anyOf: policyCheckComposition(uniqueJson(children)) };
87
+ return { anyOf: operands(uniqueJson(children)) };
89
88
  }
90
- function policyCheckComposition(values) {
91
- // Structural routing may carry an invalid empty array into semantic validation;
92
- // validateChecks reports it before the DomainSchema can be accepted.
89
+ /**
90
+ * The operands of a non-empty list, mapped, flattened, deduplicated or sorted one by one: still at
91
+ * least one. The structure and compiled admission refuse an empty list.
92
+ */
93
+ function operands(values) {
93
94
  return values;
94
95
  }
95
96
  function normalizePatternNode(value) {
@@ -106,7 +107,7 @@ function normalizePatternNode(value) {
106
107
  if ('exists' in value) {
107
108
  return {
108
109
  exists: {
109
- nodes: [...value.exists.nodes].sort((left, right) => left.variable.id - right.variable.id),
110
+ nodes: operands([...value.exists.nodes].sort((left, right) => left.variable.id - right.variable.id)),
110
111
  where: normalizePatternNode(value.exists.where),
111
112
  },
112
113
  };
@@ -116,13 +117,13 @@ function normalizePatternNode(value) {
116
117
  const normalized = normalizePatternNode(child);
117
118
  return 'allOf' in normalized ? normalized.allOf : [normalized];
118
119
  });
119
- return { allOf: uniqueJson(children) };
120
+ return { allOf: operands(uniqueJson(children)) };
120
121
  }
121
122
  const children = value.anyOf.flatMap((child) => {
122
123
  const normalized = normalizePatternNode(child);
123
124
  return 'anyOf' in normalized ? normalized.anyOf : [normalized];
124
125
  });
125
- return { anyOf: uniqueJson(children) };
126
+ return { anyOf: operands(uniqueJson(children)) };
126
127
  }
127
128
  function canonicalVariableIds(value) {
128
129
  const declarations = new Map();
@@ -297,7 +298,7 @@ function rewriteVariables(value, ids) {
297
298
  if ('exists' in value) {
298
299
  return {
299
300
  exists: {
300
- nodes: value.exists.nodes.map((node) => {
301
+ nodes: operands(value.exists.nodes.map((node) => {
301
302
  const variable = {
302
303
  kind: 'variable',
303
304
  id: ids.get(node.variable.id) ?? node.variable.id,
@@ -305,14 +306,14 @@ function rewriteVariables(value, ids) {
305
306
  return 'class' in node
306
307
  ? { variable, class: node.class }
307
308
  : { variable, selector: node.selector };
308
- }),
309
+ })),
309
310
  where: rewriteVariables(value.exists.where, ids),
310
311
  },
311
312
  };
312
313
  }
313
314
  if ('allOf' in value)
314
- return { allOf: value.allOf.map((child) => rewriteVariables(child, ids)) };
315
- return { anyOf: value.anyOf.map((child) => rewriteVariables(child, ids)) };
315
+ return { allOf: operands(value.allOf.map((child) => rewriteVariables(child, ids))) };
316
+ return { anyOf: operands(value.anyOf.map((child) => rewriteVariables(child, ids))) };
316
317
  }
317
318
  function selectorSignature(node) {
318
319
  const selector = policyVariableSelector(node);
@@ -1,4 +1,4 @@
1
- export type DiagnosticCode = 'CALLABLE_INPUT_FIELD_UNKNOWN' | 'DOMAIN_CLASS_ABSTRACT' | 'DOMAIN_PROPERTY_INPUT_INVALID' | 'DOMAIN_PROPERTY_REQUIRED' | 'DOMAIN_PROPERTY_UNKNOWN' | 'DOMAIN_VALIDATION_TARGET_UNKNOWN' | 'VALUE_SCHEMA_INSTANCE_INVALID' | 'VS_DIALECT' | 'VS_DYNAMIC_FORBIDDEN' | 'VS_KEYWORD_CLOSED' | 'VS_META_SCHEMA' | 'VS_NUMBER_DOMAIN' | 'VS_PATH_EXTENSION' | 'VS_RECURSION_PRODUCTIVE' | 'VS_REFERENCE_FORM' | 'VS_REFERENCE_RESOLUTION' | 'VS_REGEX_PORTABLE' | 'VS_RESOURCE_ID' | 'VS_VALUE_KIND' | 'VS_VALUE_KIND_JSON';
1
+ export type DiagnosticCode = 'CALLABLE_INPUT_FIELD_UNKNOWN' | 'DOMAIN_CLASS_ABSTRACT' | 'DOMAIN_PROPERTY_INPUT_INVALID' | 'DOMAIN_PROPERTY_REQUIRED' | 'DOMAIN_PROPERTY_UNKNOWN' | 'VALUE_SCHEMA_INSTANCE_INVALID' | 'VS_DIALECT' | 'VS_DYNAMIC_FORBIDDEN' | 'VS_KEYWORD_CLOSED' | 'VS_META_SCHEMA' | 'VS_NUMBER_DOMAIN' | 'VS_PATH_EXTENSION' | 'VS_RECURSION_PRODUCTIVE' | 'VS_REFERENCE_FORM' | 'VS_REFERENCE_RESOLUTION' | 'VS_REGEX_PORTABLE' | 'VS_RESOURCE_ID' | 'VS_VALUE_KIND' | 'VS_VALUE_KIND_JSON';
2
2
  export interface Diagnostic<out Code extends DiagnosticCode = DiagnosticCode> {
3
3
  readonly code: Code;
4
4
  readonly pointer: string;
@@ -6,3 +6,17 @@ export interface Diagnostic<out Code extends DiagnosticCode = DiagnosticCode> {
6
6
  readonly ref?: string;
7
7
  }
8
8
  export declare function diagnostic<const Code extends DiagnosticCode>(code: Code, pointer: string, message: string, ref?: string): Diagnostic<Code>;
9
+ /** The fields every diagnostic family shares. */
10
+ interface DiagnosticFields {
11
+ readonly code: string;
12
+ readonly pointer: string;
13
+ readonly message: string;
14
+ readonly ref?: string;
15
+ }
16
+ /** Order diagnostics by pointer, then code, then message. */
17
+ export declare function orderDiagnostics<Value extends DiagnosticFields>(diagnostics: readonly Value[]): Value[];
18
+ /** Keep one of each pointer, code, ref and message, then order them. */
19
+ export declare function orderAndDeduplicate<Value extends DiagnosticFields>(diagnostics: readonly Value[]): Value[];
20
+ /** One line per diagnostic: its pointer (`/` for the root), its code and its message. */
21
+ export declare function formatDiagnostics(diagnostics: readonly DiagnosticFields[]): string;
22
+ export {};
@@ -1,3 +1,24 @@
1
+ import { compareUnicode } from '../../addressing/ordering.js';
1
2
  export function diagnostic(code, pointer, message, ref) {
2
3
  return Object.freeze({ code, pointer, message, ...(ref === undefined ? {} : { ref }) });
3
4
  }
5
+ /** Order diagnostics by pointer, then code, then message. */
6
+ export function orderDiagnostics(diagnostics) {
7
+ return [...diagnostics].sort((left, right) => compareUnicode(left.pointer, right.pointer) ||
8
+ compareUnicode(left.code, right.code) ||
9
+ compareUnicode(left.message, right.message));
10
+ }
11
+ /** Keep one of each pointer, code, ref and message, then order them. */
12
+ export function orderAndDeduplicate(diagnostics) {
13
+ const unique = new Map();
14
+ for (const value of diagnostics) {
15
+ unique.set(`${value.pointer}\u0000${value.code}\u0000${value.ref ?? ''}\u0000${value.message}`, value);
16
+ }
17
+ return orderDiagnostics([...unique.values()]);
18
+ }
19
+ /** One line per diagnostic: its pointer (`/` for the root), its code and its message. */
20
+ export function formatDiagnostics(diagnostics) {
21
+ return diagnostics
22
+ .map(({ code, pointer, message }) => `${pointer || '/'} [${code}] ${message}`)
23
+ .join('\n');
24
+ }
@@ -0,0 +1,26 @@
1
+ import type { DiagnosticCode } from './diagnostic.js';
2
+ /**
3
+ * What a rule reads to decide. `schema`: a value schema alone, with its place in the declaration
4
+ * that holds it (a Property, a callable input or output). `context`: a value schema and what its
5
+ * Domain can reach, the Node Classes of the Domain and of its direct dependencies and the exact
6
+ * dependency closure. `value`: a value, under an accepted schema or a contract of the Domain.
7
+ * `warning`: a value schema, which the rule reports without refusing it.
8
+ */
9
+ export type ValueRuleStage = 'schema' | 'context' | 'value' | 'warning';
10
+ /** The rule one value diagnostic code reports. */
11
+ export interface ValueRule {
12
+ readonly stage: ValueRuleStage;
13
+ /**
14
+ * The positions the rule constrains, which its diagnostics point at, relative to the value
15
+ * schema, the value or the Domain input it checks: JSON Pointer templates in which a `{name}`
16
+ * token stands for one token of the kind it names and `{/path*}` for any number of tokens.
17
+ */
18
+ readonly positions: readonly [string, ...string[]];
19
+ /** The rule, in one plain sentence. */
20
+ readonly statement: string;
21
+ }
22
+ /**
23
+ * Every value diagnostic code with the rule it reports. A code added to the language does not
24
+ * compile until it is described here.
25
+ */
26
+ export declare const VALUE_RULES: Readonly<Record<DiagnosticCode, ValueRule>>;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Every value diagnostic code with the rule it reports. A code added to the language does not
3
+ * compile until it is described here.
4
+ */
5
+ export const VALUE_RULES = frozen({
6
+ CALLABLE_INPUT_FIELD_UNKNOWN: {
7
+ stage: 'value',
8
+ positions: [''],
9
+ statement: 'An input field validated on its own is a field of the Callable input, which has fields only when it is a closed object.',
10
+ },
11
+ DOMAIN_CLASS_ABSTRACT: {
12
+ stage: 'value',
13
+ positions: ['/class'],
14
+ statement: 'Properties are validated for a concrete Class only.',
15
+ },
16
+ DOMAIN_PROPERTY_INPUT_INVALID: {
17
+ stage: 'value',
18
+ positions: ['/properties'],
19
+ statement: 'The Properties of a Class are a canonical map of Property keys to portable values.',
20
+ },
21
+ DOMAIN_PROPERTY_REQUIRED: {
22
+ stage: 'value',
23
+ positions: ['/properties/{key}'],
24
+ statement: 'Every required Property of the Class has a value.',
25
+ },
26
+ DOMAIN_PROPERTY_UNKNOWN: {
27
+ stage: 'value',
28
+ positions: ['/properties/{key}'],
29
+ statement: 'Every Property given a value is a Property of the Class, its own or inherited.',
30
+ },
31
+ VALUE_SCHEMA_INSTANCE_INVALID: {
32
+ stage: 'value',
33
+ positions: ['{/path*}'],
34
+ statement: 'A value satisfies its complete value schema.',
35
+ },
36
+ VS_DIALECT: {
37
+ stage: 'schema',
38
+ positions: ['{/path*}/$schema'],
39
+ statement: 'A value schema names $schema at its root only, and there names the dialect https://schemas.astrale.ai/dsl/v1/value-schema.',
40
+ },
41
+ VS_DYNAMIC_FORBIDDEN: {
42
+ stage: 'schema',
43
+ positions: [
44
+ '{/path*}/$anchor',
45
+ '{/path*}/$dynamicAnchor',
46
+ '{/path*}/$dynamicRef',
47
+ '{/path*}/$vocabulary',
48
+ ],
49
+ statement: 'A value schema names none of $anchor, $dynamicAnchor, $dynamicRef and $vocabulary.',
50
+ },
51
+ VS_KEYWORD_CLOSED: {
52
+ stage: 'schema',
53
+ positions: ['{/path*}/{keyword}'],
54
+ statement: 'Every keyword of a value schema is one of the closed V1 vocabulary.',
55
+ },
56
+ VS_META_SCHEMA: {
57
+ stage: 'schema',
58
+ positions: ['{/path*}'],
59
+ statement: 'A value schema is JSON valid under the meta-schema of the dialect, with fullMatch a string and no two equal values in an enum, at every location.',
60
+ },
61
+ VS_NUMBER_DOMAIN: {
62
+ stage: 'value',
63
+ positions: ['{/path*}'],
64
+ statement: 'A value, like a value schema, is portable JSON: its numbers are finite and not negative zero, its strings well formed, and its size within the value limits.',
65
+ },
66
+ VS_PATH_EXTENSION: {
67
+ stage: 'schema',
68
+ positions: ['{/path*}/x-astrale-path{/path*}', '{/path*}'],
69
+ statement: 'x-astrale-path annotates only a direct Property of a callable input object, beside exactly the $ref of the graph NodePath schema, and reads target node, cardinality one and a non-empty set of Node Class refs it accepts.',
70
+ },
71
+ VS_RECURSION_PRODUCTIVE: {
72
+ stage: 'schema',
73
+ positions: ['{/path*}'],
74
+ statement: 'Every reference cycle of a value schema crosses a keyword that steps into the value: to a member, a member name or an item.',
75
+ },
76
+ VS_REFERENCE_FORM: {
77
+ stage: 'schema',
78
+ positions: ['{/path*}/$ref'],
79
+ statement: 'A $ref is a JSON Pointer fragment into its own value schema, or the id of the graph NodePath schema beside x-astrale-path.',
80
+ },
81
+ VS_REFERENCE_RESOLUTION: {
82
+ stage: 'context',
83
+ positions: [
84
+ '{/path*}/$ref',
85
+ '{/path*}/x-astrale-path/accepts/{index}',
86
+ '',
87
+ '/registry{/path*}',
88
+ ],
89
+ statement: 'A $ref resolves to a location of its own value schema, x-astrale-path accepts Node Classes of the Domain or of a direct dependency, and the schema is checked with the exact dependency closure of its Domain.',
90
+ },
91
+ VS_REGEX_PORTABLE: {
92
+ stage: 'schema',
93
+ positions: ['{/path*}/pattern', '{/path*}/fullMatch', '{/path*}/patternProperties/{pattern}'],
94
+ statement: 'Every pattern, fullMatch and patternProperties name is an RFC 9485 I-Regexp within the regular-expression limits.',
95
+ },
96
+ VS_RESOURCE_ID: {
97
+ stage: 'schema',
98
+ positions: ['{/path*}/$id'],
99
+ statement: 'A value schema declares no $id: it is embedded, not a resource of its own.',
100
+ },
101
+ VS_VALUE_KIND: {
102
+ stage: 'schema',
103
+ positions: ['{/path*}/x-astrale-value-kind'],
104
+ statement: 'x-astrale-value-kind declares instant beside type string at any location, or blob as the whole schema of a Property, exactly as values.blob writes it.',
105
+ },
106
+ VS_VALUE_KIND_JSON: {
107
+ stage: 'warning',
108
+ positions: ['/classes/{class}/properties/{property}'],
109
+ statement: 'A Property of value kind json is filtered by equality only, so its author is warned.',
110
+ },
111
+ });
112
+ function frozen(rules) {
113
+ for (const rule of Object.values(rules))
114
+ Object.freeze(Object.freeze(rule).positions);
115
+ return Object.freeze(rules);
116
+ }