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

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 (106) hide show
  1. package/dist/v1/addressing/definition-ref.js +21 -37
  2. package/dist/v1/addressing/ordering.d.ts +2 -0
  3. package/dist/v1/addressing/ordering.js +4 -0
  4. package/dist/v1/addressing/patterns.d.ts +0 -1
  5. package/dist/v1/addressing/patterns.js +0 -5
  6. package/dist/v1/builder/class/authoring.js +5 -28
  7. package/dist/v1/builder/lowering/schema/context.js +2 -4
  8. package/dist/v1/builder/lowering/value/path.js +2 -4
  9. package/dist/v1/builder/lowering/value/zod.js +1 -3
  10. package/dist/v1/builder/schema/define.d.ts +0 -1
  11. package/dist/v1/builder/schema/diagnostic.d.ts +2 -1
  12. package/dist/v1/builder/schema/diagnostic.js +5 -10
  13. package/dist/v1/builder/schema/projection.d.ts +1 -1
  14. package/dist/v1/builder/value/full-match.d.ts +0 -1
  15. package/dist/v1/builder/value/full-match.js +0 -1
  16. package/dist/v1/bundle/codec.js +2 -4
  17. package/dist/v1/bundle/diagnostic.js +3 -7
  18. package/dist/v1/bundle/structure.js +2 -4
  19. package/dist/v1/compiled/admission/diagnostic.d.ts +1 -1
  20. package/dist/v1/compiled/admission/diagnostic.js +1 -3
  21. package/dist/v1/compiled/admission/revision.d.ts +9 -1
  22. package/dist/v1/compiled/admission/revision.js +6 -3
  23. package/dist/v1/compiled/admission/semantics/class.d.ts +4 -1
  24. package/dist/v1/compiled/admission/semantics/class.js +4 -8
  25. package/dist/v1/compiled/admission/semantics/core.d.ts +1 -24
  26. package/dist/v1/compiled/admission/semantics/core.js +54 -34
  27. package/dist/v1/compiled/admission/semantics/edge.d.ts +2 -4
  28. package/dist/v1/compiled/admission/semantics/edge.js +6 -65
  29. package/dist/v1/compiled/admission/semantics/policy/callable.js +0 -2
  30. package/dist/v1/compiled/admission/semantics/policy/contracts.d.ts +5 -20
  31. package/dist/v1/compiled/admission/semantics/policy/contracts.js +8 -21
  32. package/dist/v1/compiled/admission/semantics/policy/parity.js +13 -22
  33. package/dist/v1/compiled/admission/semantics/policy/pattern.d.ts +4 -1
  34. package/dist/v1/compiled/admission/semantics/policy/pattern.js +4 -16
  35. package/dist/v1/compiled/admission/semantics/policy/typing.d.ts +2 -24
  36. package/dist/v1/compiled/admission/semantics/policy/typing.js +19 -201
  37. package/dist/v1/compiled/compiler/compile.js +6 -8
  38. package/dist/v1/compiled/diagnostic.js +2 -3
  39. package/dist/v1/compiled/loader/load.d.ts +3 -2
  40. package/dist/v1/compiled/loader/load.js +10 -44
  41. package/dist/v1/domain/create.js +3 -5
  42. package/dist/v1/domain/namespace/member-collection.js +4 -6
  43. package/dist/v1/domain/validation/create.d.ts +0 -1
  44. package/dist/v1/domain/validation/create.js +0 -35
  45. package/dist/v1/language/class/inheritance.d.ts +18 -0
  46. package/dist/v1/language/class/inheritance.js +73 -0
  47. package/dist/v1/language/class/property-values.js +2 -4
  48. package/dist/v1/language/core/facts.d.ts +72 -0
  49. package/dist/v1/language/core/facts.js +95 -0
  50. package/dist/v1/language/policy/analysis/branches.d.ts +8 -0
  51. package/dist/v1/language/policy/analysis/branches.js +2 -1
  52. package/dist/v1/language/policy/analysis/budgets.d.ts +13 -1
  53. package/dist/v1/language/policy/analysis/budgets.js +104 -0
  54. package/dist/v1/language/policy/analysis/typing.d.ts +48 -0
  55. package/dist/v1/language/policy/analysis/typing.js +338 -0
  56. package/dist/v1/language/validation/diagnostic.d.ts +14 -0
  57. package/dist/v1/language/validation/diagnostic.js +21 -0
  58. package/dist/v1/language/value/compile/fields.js +4 -8
  59. package/dist/v1/language/value/profile/rules.js +5 -7
  60. package/dist/v1/language/value/profile/validate.js +1 -10
  61. package/dist/v1/language/value/reference/recursion.js +2 -22
  62. package/dist/v1/language/value/syntax/pointer.d.ts +2 -0
  63. package/dist/v1/language/value/syntax/pointer.js +5 -2
  64. package/dist/v1/language/value/syntax/walk.d.ts +2 -0
  65. package/dist/v1/language/value/syntax/walk.js +19 -20
  66. package/dist/v1/schema/compatibility/diff.js +2 -4
  67. package/dist/v1/schema/compatibility/meaning/footprint.js +16 -50
  68. package/dist/v1/schema/compatibility/structure/requirements.js +26 -58
  69. package/dist/v1/schema/compatibility/walk.d.ts +17 -0
  70. package/dist/v1/schema/compatibility/walk.js +41 -0
  71. package/dist/v1/schema/diagnostic.js +3 -11
  72. package/dist/v1/schema/resolution/context.js +3 -5
  73. package/dist/v1/schema/validation/callable.js +3 -5
  74. package/dist/v1/schema/validation/class.js +10 -52
  75. package/dist/v1/schema/validation/core.js +65 -199
  76. package/dist/v1/schema/validation/domain.js +1 -10
  77. package/dist/v1/schema/validation/domain.structure.js +2 -4
  78. package/dist/v1/schema/validation/policy/budgets.js +5 -112
  79. package/dist/v1/schema/validation/policy/context.d.ts +5 -3
  80. package/dist/v1/schema/validation/policy/context.js +5 -3
  81. package/dist/v1/schema/validation/policy/objects.d.ts +1 -1
  82. package/dist/v1/schema/validation/policy/objects.js +6 -14
  83. package/dist/v1/schema/validation/policy/references.js +5 -6
  84. package/dist/v1/schema/validation/policy/typing.d.ts +3 -18
  85. package/dist/v1/schema/validation/policy/typing.js +22 -356
  86. package/dist/v1/schema/validation/policy/uses.d.ts +1 -1
  87. package/dist/v1/schema/validation/policy/uses.js +9 -22
  88. package/dist/v1/schema/validation/policy/validate.js +3 -2
  89. package/dist/v1/schema/validation/value-api.js +2 -2
  90. package/dist/v1/schema/validation/value.js +5 -7
  91. package/dist/v1/schema/validation/view.js +4 -8
  92. package/package.json +1 -1
  93. package/dist/v1/compiled/admission/semantics/cardinality.d.ts +0 -5
  94. package/dist/v1/compiled/admission/semantics/cardinality.js +0 -128
  95. package/dist/v1/compiled/admission/semantics/policy/equality.d.ts +0 -8
  96. package/dist/v1/compiled/admission/semantics/policy/equality.js +0 -27
  97. package/dist/v1/compiled/admission/semantics/policy/measure.d.ts +0 -9
  98. package/dist/v1/compiled/admission/semantics/policy/measure.js +0 -81
  99. package/dist/v1/compiled/admission/semantics/policy/variants.d.ts +0 -10
  100. package/dist/v1/compiled/admission/semantics/policy/variants.js +0 -65
  101. package/dist/v1/language/class/index.d.ts +0 -8
  102. package/dist/v1/language/class/index.js +0 -1
  103. package/dist/v1/language/core/index.d.ts +0 -2
  104. package/dist/v1/language/core/index.js +0 -1
  105. package/dist/v1/language/value/index.d.ts +0 -11
  106. 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
+ }
@@ -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
+ }
@@ -1,3 +1,5 @@
1
+ import { compareCodeUnits } from '../../../addressing/ordering.js';
2
+ import { escapePointerToken } from '../syntax/pointer.js';
1
3
  /** Exact field constraints available for independent scalar route validation. */
2
4
  export function inputFieldNodes(program) {
3
5
  const fields = new Map();
@@ -34,7 +36,7 @@ export function inputFieldNodes(program) {
34
36
  Array.isArray(schema.properties))
35
37
  return;
36
38
  for (const name of Object.keys(schema.properties)) {
37
- const child = node.children[`properties/${escapePointer(name)}`];
39
+ const child = node.children[`properties/${escapePointerToken(name)}`];
38
40
  if (child === undefined)
39
41
  throw new TypeError(`Compiled input field ${name} is absent.`);
40
42
  const constraints = fields.get(name) ?? new Set();
@@ -46,15 +48,9 @@ export function inputFieldNodes(program) {
46
48
  if (!closed || unsupported)
47
49
  return undefined;
48
50
  return Object.freeze(Object.fromEntries([...fields]
49
- .sort(([left], [right]) => compare(left, right))
51
+ .sort(([left], [right]) => compareCodeUnits(left, right))
50
52
  .map(([name, nodes]) => [
51
53
  name,
52
54
  Object.freeze([...nodes].sort((left, right) => left - right)),
53
55
  ])));
54
56
  }
55
- function escapePointer(value) {
56
- return value.replaceAll('~', '~0').replaceAll('/', '~1');
57
- }
58
- function compare(left, right) {
59
- return left < right ? -1 : left > right ? 1 : 0;
60
- }
@@ -4,6 +4,7 @@ import { blob, BLOB_VALUE_KIND } from '../model/blob.js';
4
4
  import { isValueObject as isJsonObject } from '../model/object.js';
5
5
  import { VALUE_SCHEMA_DIALECT } from '../model/schema.js';
6
6
  import { refuseIRegexp, stateBudgetMessage } from '../regexp/syntax.js';
7
+ import { escapePointerToken } from '../syntax/pointer.js';
7
8
  import { walkSchema } from '../syntax/walk.js';
8
9
  import { FORBIDDEN_DYNAMIC_KEYWORDS, STANDARD_SCHEMA_KEYWORDS } from './vocabulary.js';
9
10
  export function validateSchemaProfile(schema, context, pointer) {
@@ -14,10 +15,10 @@ export function validateSchemaProfile(schema, context, pointer) {
14
15
  const currentPointer = `${pointer}${location.pointer}`;
15
16
  for (const keyword of Object.keys(location.schema)) {
16
17
  if (!STANDARD_SCHEMA_KEYWORDS.has(keyword)) {
17
- diagnostics.push(diagnostic('VS_KEYWORD_CLOSED', `${currentPointer}/${escapeToken(keyword)}`, `Keyword ${keyword} is outside the closed V1 Draft 2020-12 vocabulary.`));
18
+ diagnostics.push(diagnostic('VS_KEYWORD_CLOSED', `${currentPointer}/${escapePointerToken(keyword)}`, `Keyword ${keyword} is outside the closed V1 Draft 2020-12 vocabulary.`));
18
19
  }
19
20
  if (FORBIDDEN_DYNAMIC_KEYWORDS.has(keyword)) {
20
- diagnostics.push(diagnostic('VS_DYNAMIC_FORBIDDEN', `${currentPointer}/${escapeToken(keyword)}`, `Keyword ${keyword} is forbidden by V1.`));
21
+ diagnostics.push(diagnostic('VS_DYNAMIC_FORBIDDEN', `${currentPointer}/${escapePointerToken(keyword)}`, `Keyword ${keyword} is forbidden by V1.`));
21
22
  }
22
23
  }
23
24
  if (Object.hasOwn(location.schema, '$schema')) {
@@ -86,7 +87,7 @@ function validateSemanticSets(schema, pointer, diagnostics) {
86
87
  if (isJsonObject(schema.dependentRequired)) {
87
88
  for (const [name, dependencies] of Object.entries(schema.dependentRequired)) {
88
89
  if (Array.isArray(dependencies)) {
89
- rejectPrimitiveDuplicates(dependencies, `${pointer}/dependentRequired/${escapeToken(name)}`, 'dependentRequired', diagnostics);
90
+ rejectPrimitiveDuplicates(dependencies, `${pointer}/dependentRequired/${escapePointerToken(name)}`, 'dependentRequired', diagnostics);
90
91
  }
91
92
  }
92
93
  }
@@ -103,7 +104,7 @@ function validateRegexes(schema, pointer, diagnostics) {
103
104
  }
104
105
  if (isJsonObject(schema.patternProperties)) {
105
106
  for (const pattern of Object.keys(schema.patternProperties)) {
106
- portableRegex(pattern, `${pointer}/patternProperties/${escapeToken(pattern)}`, 'A patternProperties key', 'patternProperties keys must satisfy RFC 9485 I-Regexp syntax.', diagnostics);
107
+ portableRegex(pattern, `${pointer}/patternProperties/${escapePointerToken(pattern)}`, 'A patternProperties key', 'patternProperties keys must satisfy RFC 9485 I-Regexp syntax.', diagnostics);
107
108
  }
108
109
  }
109
110
  }
@@ -140,6 +141,3 @@ function rejectCanonicalDuplicates(values, pointer, name, diagnostics) {
140
141
  seen.add(key);
141
142
  }
142
143
  }
143
- function escapeToken(value) {
144
- return value.replaceAll('~', '~0').replaceAll('/', '~1');
145
- }
@@ -1,4 +1,4 @@
1
- import { compareUnicode } from '../../../addressing/ordering.js';
1
+ import { orderAndDeduplicate } from '../../validation/diagnostic.js';
2
2
  import { collectResourceClosure } from '../reference/closure.js';
3
3
  import { validateProductiveRecursion } from '../reference/recursion.js';
4
4
  import { hasNodeDefinition, indexRegistry } from '../reference/registry.index.js';
@@ -28,12 +28,3 @@ export function validateValueSchema(schema, context, registry, pointer = '') {
28
28
  diagnostics.push(...validateProductiveRecursion(closure.resources, closure.resolutions));
29
29
  return orderAndDeduplicate(diagnostics);
30
30
  }
31
- function orderAndDeduplicate(diagnostics) {
32
- const unique = new Map();
33
- for (const value of diagnostics) {
34
- unique.set(`${value.pointer}\u0000${value.code}\u0000${value.ref ?? ''}\u0000${value.message}`, value);
35
- }
36
- return [...unique.values()].sort((left, right) => compareUnicode(left.pointer, right.pointer) ||
37
- compareUnicode(left.code, right.code) ||
38
- compareUnicode(left.message, right.message));
39
- }
@@ -1,8 +1,7 @@
1
1
  import { diagnostic } from '../../validation/diagnostic.js';
2
2
  import { isValueObject as isJsonObject } from '../model/object.js';
3
- import { SCHEMA_ARRAY_KEYWORDS, SCHEMA_MAP_KEYWORDS, SCHEMA_SINGLE_KEYWORDS, } from '../syntax/keywords.js';
4
3
  import { appendPointer } from '../syntax/pointer.js';
5
- import { isJsonSchema, walkSchema } from '../syntax/walk.js';
4
+ import { forEachSubschema, walkSchema } from '../syntax/walk.js';
6
5
  import { locationKey } from './closure.js';
7
6
  export const CONSUMING_SCHEMA_KEYWORDS = new Set([
8
7
  'additionalProperties',
@@ -43,26 +42,7 @@ export function validateProductiveRecursion(resources, resolutions) {
43
42
  for (const [keyword, value] of Object.entries(location.schema)) {
44
43
  if (!NON_CONSUMING_SCHEMA_KEYWORDS.has(keyword))
45
44
  continue;
46
- const keywordPointer = appendPointer(location.pointer, keyword);
47
- if (SCHEMA_SINGLE_KEYWORDS.has(keyword) && isJsonSchema(value)) {
48
- outgoing.add(locationKey(resource.key, keywordPointer));
49
- continue;
50
- }
51
- if (SCHEMA_ARRAY_KEYWORDS.has(keyword) && Array.isArray(value)) {
52
- value.forEach((entry, index) => {
53
- if (isJsonSchema(entry)) {
54
- outgoing.add(locationKey(resource.key, appendPointer(keywordPointer, index)));
55
- }
56
- });
57
- continue;
58
- }
59
- if (SCHEMA_MAP_KEYWORDS.has(keyword) && isJsonObject(value)) {
60
- for (const [name, entry] of Object.entries(value)) {
61
- if (isJsonSchema(entry)) {
62
- outgoing.add(locationKey(resource.key, appendPointer(keywordPointer, name)));
63
- }
64
- }
65
- }
45
+ forEachSubschema(keyword, value, appendPointer(location.pointer, keyword), (_, at) => outgoing.add(locationKey(resource.key, at)));
66
46
  }
67
47
  });
68
48
  }
@@ -1,3 +1,5 @@
1
+ /** Escape one JSON Pointer reference token: `~` as `~0`, then `/` as `~1`. */
2
+ export declare function escapePointerToken(token: string): string;
1
3
  export declare function appendPointer(pointer: string, token: string | number): string;
2
4
  export declare function parseJsonPointerFragment(fragment: string): readonly string[] | undefined;
3
5
  export declare function tokensToPointer(tokens: readonly string[]): string;
@@ -1,6 +1,9 @@
1
+ /** Escape one JSON Pointer reference token: `~` as `~0`, then `/` as `~1`. */
2
+ export function escapePointerToken(token) {
3
+ return token.replaceAll('~', '~0').replaceAll('/', '~1');
4
+ }
1
5
  export function appendPointer(pointer, token) {
2
- const encoded = String(token).replaceAll('~', '~0').replaceAll('/', '~1');
3
- return `${pointer}/${encoded}`;
6
+ return `${pointer}/${escapePointerToken(String(token))}`;
4
7
  }
5
8
  export function parseJsonPointerFragment(fragment) {
6
9
  if (!fragment.startsWith('#'))
@@ -6,5 +6,7 @@ export interface SchemaLocation {
6
6
  readonly parentKeyword?: string;
7
7
  }
8
8
  export declare function walkSchema(schema: JsonSchema, visit: (location: SchemaLocation) => void, pointer?: string): void;
9
+ /** Visit each subschema `keyword` holds directly, with its pointer under `pointer`. */
10
+ export declare function forEachSubschema(keyword: string, value: unknown, pointer: string, visit: (schema: JsonSchema, pointer: string) => void): void;
9
11
  export declare function collectSchemaPointers(schema: JsonSchema): ReadonlySet<string>;
10
12
  export declare function isJsonSchema(value: unknown): value is JsonSchema;
@@ -14,26 +14,7 @@ export function walkSchema(schema, visit, pointer = '') {
14
14
  if (!isJsonObject(current))
15
15
  return;
16
16
  for (const [keyword, value] of Object.entries(current)) {
17
- const keywordPointer = appendPointer(currentPointer, keyword);
18
- if (SCHEMA_SINGLE_KEYWORDS.has(keyword) && isJsonSchema(value)) {
19
- walk(value, keywordPointer, depth + 1, keyword);
20
- continue;
21
- }
22
- if (SCHEMA_ARRAY_KEYWORDS.has(keyword) && Array.isArray(value)) {
23
- value.forEach((entry, index) => {
24
- if (isJsonSchema(entry)) {
25
- walk(entry, appendPointer(keywordPointer, index), depth + 1, keyword);
26
- }
27
- });
28
- continue;
29
- }
30
- if (SCHEMA_MAP_KEYWORDS.has(keyword) && isJsonObject(value)) {
31
- for (const [name, entry] of Object.entries(value)) {
32
- if (isJsonSchema(entry)) {
33
- walk(entry, appendPointer(keywordPointer, name), depth + 1, keyword);
34
- }
35
- }
36
- }
17
+ forEachSubschema(keyword, value, appendPointer(currentPointer, keyword), (entry, at) => walk(entry, at, depth + 1, keyword));
37
18
  }
38
19
  }
39
20
  finally {
@@ -43,6 +24,24 @@ export function walkSchema(schema, visit, pointer = '') {
43
24
  }
44
25
  walk(schema, pointer, 0);
45
26
  }
27
+ /** Visit each subschema `keyword` holds directly, with its pointer under `pointer`. */
28
+ export function forEachSubschema(keyword, value, pointer, visit) {
29
+ if (SCHEMA_SINGLE_KEYWORDS.has(keyword) && isJsonSchema(value)) {
30
+ visit(value, pointer);
31
+ }
32
+ else if (SCHEMA_ARRAY_KEYWORDS.has(keyword) && Array.isArray(value)) {
33
+ value.forEach((entry, index) => {
34
+ if (isJsonSchema(entry))
35
+ visit(entry, appendPointer(pointer, index));
36
+ });
37
+ }
38
+ else if (SCHEMA_MAP_KEYWORDS.has(keyword) && isJsonObject(value)) {
39
+ for (const [name, entry] of Object.entries(value)) {
40
+ if (isJsonSchema(entry))
41
+ visit(entry, appendPointer(pointer, name));
42
+ }
43
+ }
44
+ }
46
45
  export function collectSchemaPointers(schema) {
47
46
  const pointers = new Set();
48
47
  walkSchema(schema, ({ pointer }) => pointers.add(pointer));
@@ -1,5 +1,6 @@
1
1
  import { compareUnicode } from '../../addressing/ordering.js';
2
2
  import { isValueObject as isJsonObject } from '../../language/value/model/object.js';
3
+ import { escapePointerToken } from '../../language/value/syntax/pointer.js';
3
4
  /** Structural differences between two portable values, leaf by leaf, in JSON Pointer order. */
4
5
  export function differences(before, after) {
5
6
  const found = [];
@@ -17,7 +18,7 @@ function collect(before, after, path, found) {
17
18
  if (isJsonObject(before) && isJsonObject(after)) {
18
19
  const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort(compareUnicode);
19
20
  for (const key of keys) {
20
- collect(before[key], after[key], `${path}/${escape(key)}`, found);
21
+ collect(before[key], after[key], `${path}/${escapePointerToken(key)}`, found);
21
22
  }
22
23
  return;
23
24
  }
@@ -34,6 +35,3 @@ function collect(before, after, path, found) {
34
35
  ...(after === undefined ? {} : { after }),
35
36
  }));
36
37
  }
37
- function escape(segment) {
38
- return segment.replaceAll('~', '~0').replaceAll('/', '~1');
39
- }