@astrale-os/kernel-dsl 1.0.0-beta.15 → 1.0.0-beta.16
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/compiled/admission/semantics/policy/typing.js +2 -2
- package/dist/v1/language/.spec/schemas/draft-2020-12.schema.json +462 -0
- package/dist/v1/language/policy/analysis/typing.d.ts +4 -2
- package/dist/v1/language/policy/analysis/typing.js +132 -11
- package/dist/v1/language/validation/rule.d.ts +6 -0
- package/dist/v1/language/validation/rule.js +4 -6
- package/dist/v1/language/value/ordering.d.ts +5 -0
- package/dist/v1/language/value/ordering.js +14 -0
- package/dist/v1/language/value/reference/closure.js +3 -4
- package/dist/v1/language/value/reference/registry.index.d.ts +2 -0
- package/dist/v1/language/value/reference/registry.index.js +31 -15
- package/dist/v1/schema/.spec/schemas/domain-schema.schema.json +30 -2
- package/dist/v1/schema/rule.d.ts +6 -0
- package/dist/v1/schema/rule.js +13 -1
- package/dist/v1/schema/validation/class.d.ts +6 -2
- package/dist/v1/schema/validation/class.js +18 -22
- package/dist/v1/schema/validation/domain.js +1 -1
- package/dist/v1/schema/validation/policy/context.d.ts +1 -2
- package/dist/v1/schema/validation/policy/context.js +2 -7
- package/dist/v1/schema/validation/value-api.d.ts +2 -0
- package/dist/v1/schema/validation/value-api.js +2 -0
- package/dist/v1/schema/validation/value.json-schema.d.ts +3 -3
- package/dist/v1/schema/validation/value.json-schema.js +22 -4
- package/package.json +2 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Key } from '../../../addressing/index.js';
|
|
2
2
|
import { definitionRefIndex } from '../../../addressing/ordering.js';
|
|
3
|
+
import { classSatisfies, isGraphRoot } from '../../class/inheritance.js';
|
|
3
4
|
import { limits } from '../../limits.js';
|
|
4
5
|
import { branchesOf, branchProduct, capBranches, targetMode, termKey, UnionFind, } from './branches.js';
|
|
5
6
|
import { admittedCheckClasses } from './checks.js';
|
|
@@ -15,6 +16,9 @@ export class PolicyTyping {
|
|
|
15
16
|
// paths; for that to merge distinct variables, the Policy expands deeper than
|
|
16
17
|
// `limits.policy.patternDepth`.
|
|
17
18
|
#expansions = new Map();
|
|
19
|
+
#nodeClasses;
|
|
20
|
+
#concreteClasses;
|
|
21
|
+
#satisfaction;
|
|
18
22
|
constructor(scope) {
|
|
19
23
|
this.scope = scope;
|
|
20
24
|
}
|
|
@@ -69,7 +73,9 @@ export class PolicyTyping {
|
|
|
69
73
|
return branches !== undefined && branches.every((branch) => this.branchIsTyped(branch, use));
|
|
70
74
|
}
|
|
71
75
|
#concreteEndpointClasses(accepts) {
|
|
72
|
-
|
|
76
|
+
this.#concreteClasses ??= new Set(this.scope.concreteNodeClasses.map(definitionRefIndex));
|
|
77
|
+
const concrete = this.#concreteClasses;
|
|
78
|
+
return this.#satisfyingOneOf(accepts).filter((ref) => concrete.has(definitionRefIndex(ref)));
|
|
73
79
|
}
|
|
74
80
|
#expandedBranches(name, visiting, namespace) {
|
|
75
81
|
if (visiting.has(name))
|
|
@@ -158,7 +164,11 @@ export class PolicyTyping {
|
|
|
158
164
|
if (contract.kind === 'exact')
|
|
159
165
|
return this.#applyExact(candidates, contract.ref);
|
|
160
166
|
if (contract.kind === 'default') {
|
|
161
|
-
return uniqueCandidates(
|
|
167
|
+
return uniqueCandidates(candidates.flatMap((candidate) => candidate.kind === 'exact'
|
|
168
|
+
? this.#isNodeClass(candidate.ref)
|
|
169
|
+
? [candidate]
|
|
170
|
+
: []
|
|
171
|
+
: this.#nodeCandidates(candidate)));
|
|
162
172
|
}
|
|
163
173
|
return candidates;
|
|
164
174
|
}
|
|
@@ -183,26 +193,30 @@ export class PolicyTyping {
|
|
|
183
193
|
if (definitionRefIndex(candidate.ref) === definitionRefIndex(exact))
|
|
184
194
|
result.push(candidate);
|
|
185
195
|
}
|
|
186
|
-
else if (candidate.
|
|
196
|
+
else if (candidate.kind === 'node') {
|
|
197
|
+
if (this.#isNodeClass(exact) && this.#satisfiesLists(exact, candidate.lists)) {
|
|
198
|
+
result.push({ kind: 'exact', ref: exact });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (candidate.requirements.every((required) => this.#classExtends(exact, required))) {
|
|
187
202
|
result.push({ kind: 'exact', ref: exact });
|
|
188
203
|
}
|
|
189
204
|
}
|
|
190
205
|
return uniqueCandidates(result);
|
|
191
206
|
}
|
|
192
207
|
#applyAnyNodeClass(candidates) {
|
|
193
|
-
return uniqueCandidates(candidates.flatMap((candidate) => candidate.kind === 'exact'
|
|
194
|
-
? [candidate]
|
|
195
|
-
: this.scope.allNodeClasses
|
|
196
|
-
.filter((ref) => candidate.requirements.every((required) => this.scope.classExtends(ref, required)))
|
|
197
|
-
.map((ref) => ({ kind: 'exact', ref }))));
|
|
208
|
+
return uniqueCandidates(candidates.flatMap((candidate) => candidate.kind === 'exact' ? [candidate] : this.#nodeCandidates(candidate)));
|
|
198
209
|
}
|
|
199
210
|
#applyRequirement(candidates, required) {
|
|
200
211
|
const result = [];
|
|
201
212
|
for (const candidate of candidates) {
|
|
202
213
|
if (candidate.kind === 'exact') {
|
|
203
|
-
if (this
|
|
214
|
+
if (this.#classExtends(candidate.ref, required))
|
|
204
215
|
result.push(candidate);
|
|
205
216
|
}
|
|
217
|
+
else if (candidate.kind === 'node') {
|
|
218
|
+
result.push(...this.#inhabited([...candidate.lists, [required]]));
|
|
219
|
+
}
|
|
206
220
|
else {
|
|
207
221
|
result.push({
|
|
208
222
|
kind: 'open',
|
|
@@ -220,6 +234,11 @@ export class PolicyTyping {
|
|
|
220
234
|
result.push(candidate);
|
|
221
235
|
continue;
|
|
222
236
|
}
|
|
237
|
+
if (candidate.kind === 'node') {
|
|
238
|
+
if (accepts.length > 0)
|
|
239
|
+
result.push(...this.#inhabited([...candidate.lists, accepts]));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
223
242
|
for (const accept of accepts) {
|
|
224
243
|
result.push({
|
|
225
244
|
kind: 'open',
|
|
@@ -230,8 +249,105 @@ export class PolicyTyping {
|
|
|
230
249
|
return uniqueCandidates(result);
|
|
231
250
|
}
|
|
232
251
|
#classSatisfiesEndpoint(candidate, accepts) {
|
|
233
|
-
return accepts.some((accept) => this
|
|
252
|
+
return accepts.some((accept) => this.#classExtends(candidate, accept));
|
|
253
|
+
}
|
|
254
|
+
/** Whether `candidate` is the `required` Class or a same-kind descendant. */
|
|
255
|
+
#classExtends(candidate, required) {
|
|
256
|
+
const resolve = (ref) => this.scope.resolveClass(ref);
|
|
257
|
+
const actual = resolve(candidate);
|
|
258
|
+
const target = resolve(required);
|
|
259
|
+
return actual !== undefined && target !== undefined && classSatisfies(actual, target, resolve);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The Node Classes of the closure that satisfy one Class of `accepts`, in closure order: those
|
|
263
|
+
* `#classSatisfiesEndpoint` accepts, read from the index rather than tested one by one.
|
|
264
|
+
*/
|
|
265
|
+
#satisfyingOneOf(accepts) {
|
|
266
|
+
const index = (this.#satisfaction ??= satisfactionIndex(this.scope));
|
|
267
|
+
const positions = new Set();
|
|
268
|
+
for (const accept of accepts) {
|
|
269
|
+
const required = this.scope.resolveClass(accept);
|
|
270
|
+
if (required?.kind !== 'node')
|
|
271
|
+
continue;
|
|
272
|
+
if (isGraphRoot(required))
|
|
273
|
+
return index.nodeClasses;
|
|
274
|
+
for (const position of index.satisfying.get(ancestryKey(required.ref)) ?? []) {
|
|
275
|
+
positions.add(position);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return [...positions].sort((left, right) => left - right).map((at) => index.nodeClasses[at]);
|
|
279
|
+
}
|
|
280
|
+
#satisfiesLists(ref, lists) {
|
|
281
|
+
return lists.every((accepts) => this.#classSatisfiesEndpoint(ref, accepts));
|
|
282
|
+
}
|
|
283
|
+
#isNodeClass(ref) {
|
|
284
|
+
this.#nodeClasses ??= new Set(this.scope.allNodeClasses.map(definitionRefIndex));
|
|
285
|
+
return this.#nodeClasses.has(definitionRefIndex(ref));
|
|
286
|
+
}
|
|
287
|
+
/** The Node Classes an open or `node` candidate can be. */
|
|
288
|
+
#nodeCandidates(candidate) {
|
|
289
|
+
return candidate.kind === 'node'
|
|
290
|
+
? [candidate]
|
|
291
|
+
: this.#inhabited(candidate.requirements.map((required) => [required]));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The `node` candidate of these lists when some Node Class of the closure satisfies them, so that
|
|
295
|
+
* an empty one is dropped where its expansion would have been empty. A witness is almost always a
|
|
296
|
+
* Class the lists name; only when none is does the search read the Classes that satisfy the
|
|
297
|
+
* narrowest list, every witness being one of them.
|
|
298
|
+
*/
|
|
299
|
+
#inhabited(lists) {
|
|
300
|
+
const fits = (ref) => this.#isNodeClass(ref) && this.#satisfiesLists(ref, lists);
|
|
301
|
+
const inhabited = lists.some((accepts) => accepts.some(fits)) ||
|
|
302
|
+
(lists.length === 0
|
|
303
|
+
? this.scope.allNodeClasses
|
|
304
|
+
: lists
|
|
305
|
+
.map((accepts) => this.#satisfyingOneOf(accepts))
|
|
306
|
+
.reduce((narrowest, next) => (next.length < narrowest.length ? next : narrowest))).some(fits);
|
|
307
|
+
return inhabited ? [{ kind: 'node', lists }] : [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Classes are satisfied by origin and name, as `classSatisfies` compares them. */
|
|
311
|
+
function ancestryKey(ref) {
|
|
312
|
+
return `${ref.origin}\u0000${ref.name}`;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Index each Node Class of the closure under every Class its ancestry reaches, walked as
|
|
316
|
+
* `classSatisfies` walks it: the Class itself, then each declared parent that resolves to a Node
|
|
317
|
+
* Class. A Class then satisfies a non-root Node Class exactly when it is indexed under it.
|
|
318
|
+
*/
|
|
319
|
+
function satisfactionIndex(scope) {
|
|
320
|
+
const nodeClasses = [];
|
|
321
|
+
const satisfying = new Map();
|
|
322
|
+
for (const ref of scope.allNodeClasses) {
|
|
323
|
+
const actual = scope.resolveClass(ref);
|
|
324
|
+
if (actual?.kind !== 'node')
|
|
325
|
+
continue;
|
|
326
|
+
const position = nodeClasses.push(ref) - 1;
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
const keys = new Set();
|
|
329
|
+
const pending = [actual];
|
|
330
|
+
for (let value = pending.pop(); value !== undefined; value = pending.pop()) {
|
|
331
|
+
if (seen.has(value))
|
|
332
|
+
continue;
|
|
333
|
+
seen.add(value);
|
|
334
|
+
const key = ancestryKey(value.ref);
|
|
335
|
+
if (!keys.has(key)) {
|
|
336
|
+
keys.add(key);
|
|
337
|
+
const positions = satisfying.get(key);
|
|
338
|
+
if (positions === undefined)
|
|
339
|
+
satisfying.set(key, [position]);
|
|
340
|
+
else
|
|
341
|
+
positions.push(position);
|
|
342
|
+
}
|
|
343
|
+
for (const parent of value.extends) {
|
|
344
|
+
const resolved = scope.resolveClass(parent);
|
|
345
|
+
if (resolved?.kind === 'node')
|
|
346
|
+
pending.push(resolved);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
234
349
|
}
|
|
350
|
+
return { nodeClasses, satisfying };
|
|
235
351
|
}
|
|
236
352
|
function targetContract(term, use) {
|
|
237
353
|
if (use.kind === 'object')
|
|
@@ -329,7 +445,12 @@ function uniqueCandidates(values) {
|
|
|
329
445
|
for (const value of values) {
|
|
330
446
|
const key = value.kind === 'exact'
|
|
331
447
|
? `exact:${definitionRefIndex(value.ref)}`
|
|
332
|
-
:
|
|
448
|
+
: value.kind === 'open'
|
|
449
|
+
? `open:${value.requirements.map(definitionRefIndex).sort().join('|')}`
|
|
450
|
+
: `node:${value.lists
|
|
451
|
+
.map((accepts) => accepts.map(definitionRefIndex).sort().join(','))
|
|
452
|
+
.sort()
|
|
453
|
+
.join('|')}`;
|
|
333
454
|
result.set(key, value);
|
|
334
455
|
}
|
|
335
456
|
return [...result.values()];
|
|
@@ -21,6 +21,12 @@ export interface ValueRule {
|
|
|
21
21
|
readonly statement: string;
|
|
22
22
|
/** The limits of `schema.limits` the rule enforces, by key, where it enforces some. */
|
|
23
23
|
readonly limits?: readonly [LimitKey, ...LimitKey[]];
|
|
24
|
+
/**
|
|
25
|
+
* The conditions of the published JSON Schema of the document that state the rule, where some
|
|
26
|
+
* do: the URI fragment of each `if` or `not` subschema whose failure refuses a value schema of
|
|
27
|
+
* the document with `DV_VALUE_SCHEMA` and this rule's code as its `valueCode`.
|
|
28
|
+
*/
|
|
29
|
+
readonly conditions?: readonly [string, ...string[]];
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* Every value diagnostic code with the rule it reports. A code added to the language does not
|
|
@@ -80,12 +80,7 @@ export const VALUE_RULES = frozen({
|
|
|
80
80
|
},
|
|
81
81
|
VS_REFERENCE_RESOLUTION: {
|
|
82
82
|
stage: 'context',
|
|
83
|
-
positions: [
|
|
84
|
-
'{/path*}/$ref',
|
|
85
|
-
'{/path*}/x-astrale-path/accepts/{index}',
|
|
86
|
-
'',
|
|
87
|
-
'/registry{/path*}',
|
|
88
|
-
],
|
|
83
|
+
positions: ['{/path*}/$ref', '{/path*}/x-astrale-path/accepts/{index}', ''],
|
|
89
84
|
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
85
|
},
|
|
91
86
|
VS_REGEX_PORTABLE: {
|
|
@@ -103,6 +98,7 @@ export const VALUE_RULES = frozen({
|
|
|
103
98
|
stage: 'schema',
|
|
104
99
|
positions: ['{/path*}/x-astrale-value-kind'],
|
|
105
100
|
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.',
|
|
101
|
+
conditions: ['#/$defs/value-location/anyOf/1/if'],
|
|
106
102
|
},
|
|
107
103
|
VS_VALUE_KIND_JSON: {
|
|
108
104
|
stage: 'warning',
|
|
@@ -115,6 +111,8 @@ function frozen(rules) {
|
|
|
115
111
|
Object.freeze(Object.freeze(rule).positions);
|
|
116
112
|
if (rule.limits !== undefined)
|
|
117
113
|
Object.freeze(rule.limits);
|
|
114
|
+
if (rule.conditions !== undefined)
|
|
115
|
+
Object.freeze(rule.conditions);
|
|
118
116
|
}
|
|
119
117
|
return Object.freeze(rules);
|
|
120
118
|
}
|
|
@@ -14,6 +14,11 @@ export declare function classifyOrdering(contract: JsonSchema, registry: ValueSc
|
|
|
14
14
|
/** Recompute the scalar ordering classification from one admitted portable value program. */
|
|
15
15
|
export declare function classifyValueProgramOrdering(program: ValueProgram): OrderingKind | undefined;
|
|
16
16
|
export declare function classifyPreparedOrdering(compiled: CompiledInstanceSchema): OrderingKind | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Every value kind `classifyValueKind` returns, frozen, in the order `ValueKind` names them. A kind
|
|
19
|
+
* added to `ValueKind` does not compile until it is listed here.
|
|
20
|
+
*/
|
|
21
|
+
export declare const VALUE_KINDS: readonly ValueKind[];
|
|
17
22
|
/**
|
|
18
23
|
* The value kind of a property schema (page "Fondations Query v7", § Atoms et value kinds, Y4):
|
|
19
24
|
* the one kind its non-null values admit, `null` being a point of every kind. An annotated
|
|
@@ -34,6 +34,20 @@ export function classifyPreparedOrdering(compiled) {
|
|
|
34
34
|
? kind
|
|
35
35
|
: undefined;
|
|
36
36
|
}
|
|
37
|
+
const valueKinds = {
|
|
38
|
+
text: true,
|
|
39
|
+
number: true,
|
|
40
|
+
instant: true,
|
|
41
|
+
enum: true,
|
|
42
|
+
boolean: true,
|
|
43
|
+
set: true,
|
|
44
|
+
json: true,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Every value kind `classifyValueKind` returns, frozen, in the order `ValueKind` names them. A kind
|
|
48
|
+
* added to `ValueKind` does not compile until it is listed here.
|
|
49
|
+
*/
|
|
50
|
+
export const VALUE_KINDS = Object.freeze(Object.keys(valueKinds));
|
|
37
51
|
/**
|
|
38
52
|
* The value kind of a property schema (page "Fondations Query v7", § Atoms et value kinds, Y4):
|
|
39
53
|
* the one kind its non-null values admit, `null` being a point of every kind. An annotated
|
|
@@ -36,10 +36,9 @@ export function collectResourceClosure(schema, context, index, pointer) {
|
|
|
36
36
|
const target = result.resolved.resource;
|
|
37
37
|
if (resources.has(target.key))
|
|
38
38
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
39
|
+
// The graph NodePath resource is no part of the schema: what it holds is reported at the
|
|
40
|
+
// $ref that names it.
|
|
41
|
+
const registered = { ...target, diagnosticPointer: refPointer };
|
|
43
42
|
resources.set(target.key, registered);
|
|
44
43
|
queue.push(registered);
|
|
45
44
|
});
|
|
@@ -5,6 +5,8 @@ export interface RegistryIndex {
|
|
|
5
5
|
readonly registry: ValueSchemaRegistry;
|
|
6
6
|
readonly domains: ReadonlyMap<string, ValueSchemaDomainRegistry>;
|
|
7
7
|
readonly dependenciesByOwner: ReadonlyMap<string, ReadonlyMap<string, string>>;
|
|
8
|
+
/** The `definitionRefIndex` of each Node Definition its own Domain lists. */
|
|
9
|
+
readonly nodeDefinitions: ReadonlySet<string>;
|
|
8
10
|
}
|
|
9
11
|
export declare function indexRegistry(registry: ValueSchemaRegistry, pointer: string): {
|
|
10
12
|
readonly index: RegistryIndex;
|
|
@@ -4,11 +4,21 @@ import { diagnostic } from '../../validation/diagnostic.js';
|
|
|
4
4
|
import { isValueObject as isJsonObject } from '../model/object.js';
|
|
5
5
|
import { JSON_SCHEMA_DIALECT } from '../model/schema.js';
|
|
6
6
|
import { GRAPH_NODE_PATH_SCHEMA_ID } from '../syntax/node-path/schema.js';
|
|
7
|
+
// A registry is an immutable value, and every value schema of a Domain is validated against the
|
|
8
|
+
// same one: an index that admitted it without a diagnostic is reused, so the closure is indexed
|
|
9
|
+
// once per registry rather than once per value schema.
|
|
10
|
+
const admittedIndexes = new WeakMap();
|
|
7
11
|
export function indexRegistry(registry, pointer) {
|
|
12
|
+
const admitted = admittedIndexes.get(registry);
|
|
13
|
+
if (admitted !== undefined)
|
|
14
|
+
return { index: admitted, diagnostics: [] };
|
|
8
15
|
const diagnostics = [];
|
|
9
16
|
const domains = new Map();
|
|
10
17
|
domains.set(registry.current.origin, registry.current);
|
|
11
|
-
|
|
18
|
+
// The registry is the closure the Kernel builds for the schema at `pointer`, not a document: a
|
|
19
|
+
// fault of one of its entries is a fault of the closure that schema is checked with, reported
|
|
20
|
+
// there, the entry named in the message.
|
|
21
|
+
validateDomainRegistry(registry.current, 'current', false, pointer, diagnostics);
|
|
12
22
|
const dependencies = [
|
|
13
23
|
...registry.directDependencies.map((domain) => ({ domain, lane: 'directDependencies' })),
|
|
14
24
|
...registry.transitiveDependencies.map((domain) => ({
|
|
@@ -17,10 +27,9 @@ export function indexRegistry(registry, pointer) {
|
|
|
17
27
|
})),
|
|
18
28
|
];
|
|
19
29
|
dependencies.forEach(({ domain, lane }, index) => {
|
|
20
|
-
|
|
21
|
-
validateDomainRegistry(domain, domainPointer, true, diagnostics);
|
|
30
|
+
validateDomainRegistry(domain, `${lane}/${index}`, true, pointer, diagnostics);
|
|
22
31
|
if (domain.origin === registry.current.origin || domains.has(domain.origin)) {
|
|
23
|
-
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION',
|
|
32
|
+
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION', pointer, `Registry origin ${domain.origin} is duplicated in the exact closure, at ${lane}/${index}.`));
|
|
24
33
|
return;
|
|
25
34
|
}
|
|
26
35
|
domains.set(domain.origin, domain);
|
|
@@ -37,31 +46,38 @@ export function indexRegistry(registry, pointer) {
|
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
const nodeDefinitions = new Set();
|
|
50
|
+
for (const domain of domains.values()) {
|
|
51
|
+
for (const ref of domain.nodeDefinitions) {
|
|
52
|
+
if (ref.origin === domain.origin)
|
|
53
|
+
nodeDefinitions.add(definitionRefIndex(ref));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
validateGraphNodePathResource(registry.graphNodePath, pointer, diagnostics);
|
|
57
|
+
const index = { registry, domains, dependenciesByOwner, nodeDefinitions };
|
|
58
|
+
if (diagnostics.length === 0)
|
|
59
|
+
admittedIndexes.set(registry, index);
|
|
60
|
+
return { index, diagnostics };
|
|
42
61
|
}
|
|
43
62
|
export function isOriginVisibleFrom(index, ownerOrigin, targetOrigin) {
|
|
44
63
|
return (ownerOrigin === targetOrigin ||
|
|
45
64
|
index.dependenciesByOwner.get(ownerOrigin)?.has(targetOrigin) === true);
|
|
46
65
|
}
|
|
47
66
|
export function hasNodeDefinition(index, ref, ownerOrigin) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return (index.domains
|
|
51
|
-
.get(ref.origin)
|
|
52
|
-
?.nodeDefinitions.some((candidate) => definitionRefIndex(candidate) === definitionRefIndex(ref)) === true);
|
|
67
|
+
return (isOriginVisibleFrom(index, ownerOrigin, ref.origin) &&
|
|
68
|
+
index.nodeDefinitions.has(definitionRefIndex(ref)));
|
|
53
69
|
}
|
|
54
|
-
function validateDomainRegistry(domain,
|
|
70
|
+
function validateDomainRegistry(domain, entry, revisionRequired, pointer, diagnostics) {
|
|
55
71
|
if (!DOMAIN_ORIGIN_PATTERN.test(domain.origin)) {
|
|
56
|
-
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION',
|
|
72
|
+
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION', pointer, `Registry origin is invalid, at ${entry}.`));
|
|
57
73
|
}
|
|
58
74
|
if ((revisionRequired && domain.revision === undefined) ||
|
|
59
75
|
(domain.revision !== undefined && !DOMAIN_REVISION_PATTERN.test(domain.revision))) {
|
|
60
|
-
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION',
|
|
76
|
+
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION', pointer, `A dependency-closure registry entry requires its exact sha256 revision, at ${entry}.`));
|
|
61
77
|
}
|
|
62
78
|
for (const ref of domain.nodeDefinitions) {
|
|
63
79
|
if (ref.origin !== domain.origin) {
|
|
64
|
-
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION',
|
|
80
|
+
diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION', pointer, `A registry domain can list only its own Node Definitions, at ${entry}.`));
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
}
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"code": "DV_VALUE_SCHEMA",
|
|
47
47
|
"valueCode": "VS_PATH_EXTENSION",
|
|
48
48
|
"stage": "rule",
|
|
49
|
-
"statement": "x-astrale-path annotates only a direct Property of a callable input object, beside exactly the $ref of the graph NodePath schema
|
|
49
|
+
"statement": "x-astrale-path annotates only a direct Property of a callable input object, beside exactly the $ref of the graph NodePath schema.",
|
|
50
50
|
"positions": [
|
|
51
51
|
"#/$defs/value-location/anyOf/1/properties/x-astrale-path"
|
|
52
52
|
]
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"stage": "context",
|
|
76
76
|
"statement": "A $ref resolves to a location of its own value schema, and x-astrale-path accepts Node Classes of the Domain or of a direct dependency.",
|
|
77
77
|
"positions": [
|
|
78
|
+
"#/$defs/path-annotation/properties/accepts/items",
|
|
78
79
|
"#/$defs/value-location/anyOf/1/properties/$ref"
|
|
79
80
|
]
|
|
80
81
|
},
|
|
@@ -1384,6 +1385,33 @@
|
|
|
1384
1385
|
"maxLength": 253,
|
|
1385
1386
|
"pattern": "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"
|
|
1386
1387
|
},
|
|
1388
|
+
"path-annotation": {
|
|
1389
|
+
"type": "object",
|
|
1390
|
+
"properties": {
|
|
1391
|
+
"target": {
|
|
1392
|
+
"type": "string",
|
|
1393
|
+
"const": "node"
|
|
1394
|
+
},
|
|
1395
|
+
"cardinality": {
|
|
1396
|
+
"type": "string",
|
|
1397
|
+
"const": "one"
|
|
1398
|
+
},
|
|
1399
|
+
"accepts": {
|
|
1400
|
+
"type": "array",
|
|
1401
|
+
"minItems": 1,
|
|
1402
|
+
"uniqueItems": true,
|
|
1403
|
+
"items": {
|
|
1404
|
+
"$ref": "#/$defs/class-ref"
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
"required": [
|
|
1409
|
+
"target",
|
|
1410
|
+
"cardinality",
|
|
1411
|
+
"accepts"
|
|
1412
|
+
],
|
|
1413
|
+
"additionalProperties": false
|
|
1414
|
+
},
|
|
1387
1415
|
"policy-check-expr": {
|
|
1388
1416
|
"anyOf": [
|
|
1389
1417
|
{
|
|
@@ -2100,7 +2128,7 @@
|
|
|
2100
2128
|
"type": "string"
|
|
2101
2129
|
},
|
|
2102
2130
|
"x-astrale-path": {
|
|
2103
|
-
"
|
|
2131
|
+
"$ref": "#/$defs/path-annotation"
|
|
2104
2132
|
},
|
|
2105
2133
|
"additionalProperties": {
|
|
2106
2134
|
"$ref": "#/$defs/value-subschema"
|
package/dist/v1/schema/rule.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export interface SchemaRule {
|
|
|
24
24
|
readonly enforces?: string;
|
|
25
25
|
/** The limits of `limits` the rule enforces, by key, where it enforces some. */
|
|
26
26
|
readonly limits?: readonly [LimitKey, ...LimitKey[]];
|
|
27
|
+
/**
|
|
28
|
+
* The conditions of the published JSON Schema of the document that state the rule, where some
|
|
29
|
+
* do: the URI fragment of each `if` or `not` subschema, `#/$defs/edge-class/if`, whose failure
|
|
30
|
+
* refuses a document with this rule's code.
|
|
31
|
+
*/
|
|
32
|
+
readonly conditions?: readonly [string, ...string[]];
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
29
35
|
* Every Schema diagnostic code with the rule it reports. A code added to the DSL does not compile
|
package/dist/v1/schema/rule.js
CHANGED
|
@@ -38,7 +38,8 @@ export const RULES = deepFreeze({
|
|
|
38
38
|
},
|
|
39
39
|
DV_CALLABLE_IO: {
|
|
40
40
|
stage: 'rule',
|
|
41
|
-
|
|
41
|
+
// A Method takes a value input only: its input has no mode.
|
|
42
|
+
positions: ['/functions/{function}/input/mode', ...at('/output/mode')],
|
|
42
43
|
statement: 'A durable Function or Method takes a value input and returns a value output.',
|
|
43
44
|
},
|
|
44
45
|
DV_CLASS_ABSTRACT: {
|
|
@@ -185,6 +186,7 @@ export const RULES = deepFreeze({
|
|
|
185
186
|
stage: 'structure',
|
|
186
187
|
positions: ANY,
|
|
187
188
|
statement: `The document is portable JSON: as text, UTF-8 that parses, within ${count(defaultLimits.maxBytes)} bytes, with no byte-order mark and no repeated member name; as a value, null, booleans, finite numbers other than negative zero, strings, arrays and plain objects, with no string or member name that holds U+0000 or a lone surrogate or exceeds ${count(defaultLimits.maxStringBytes)} bytes of UTF-8, at most ${count(defaultLimits.maxDepth)} nested arrays and objects, and at most ${count(defaultLimits.maxMembers)} members and items in all.`,
|
|
189
|
+
conditions: ['#/$defs/portable-string/not'],
|
|
188
190
|
},
|
|
189
191
|
DV_METHOD_ABSTRACT: {
|
|
190
192
|
stage: 'rule',
|
|
@@ -247,6 +249,7 @@ export const RULES = deepFreeze({
|
|
|
247
249
|
...at('/output/item{/path*}'),
|
|
248
250
|
],
|
|
249
251
|
statement: 'Every embedded value schema is in the value language: its diagnostic names the refusal in `valueCode`, and the schema of an automatic or State Property is not a blob.',
|
|
252
|
+
conditions: ['#/$defs/node-property-definition/allOf/0/if'],
|
|
250
253
|
},
|
|
251
254
|
PL_BUDGET: {
|
|
252
255
|
stage: 'rule',
|
|
@@ -307,16 +310,19 @@ export const RULES = deepFreeze({
|
|
|
307
310
|
stage: 'structure',
|
|
308
311
|
positions: ['/functions/{function}/input'],
|
|
309
312
|
statement: 'A Function input in binary mode is a binary input: it declares its metadata and maximumBytes.',
|
|
313
|
+
conditions: ['#/$defs/function-definition/properties/input/anyOf/1/not'],
|
|
310
314
|
},
|
|
311
315
|
STRUCTURE_CALLABLE_POLICY_AUTH: {
|
|
312
316
|
stage: 'structure',
|
|
313
317
|
positions: at('/policy'),
|
|
314
318
|
statement: 'Only an authorized Function or Method declares a Policy.',
|
|
319
|
+
conditions: ['#/$defs/function-definition/if', '#/$defs/method-definition/if'],
|
|
315
320
|
},
|
|
316
321
|
STRUCTURE_DEPENDENCY_ALIAS: {
|
|
317
322
|
stage: 'structure',
|
|
318
323
|
positions: ['/dependencies/{alias}'],
|
|
319
324
|
statement: 'A dependency alias is neither prototype nor constructor.',
|
|
325
|
+
conditions: ['#/$defs/dependency-alias/not'],
|
|
320
326
|
},
|
|
321
327
|
STRUCTURE_DOMAIN_NAME: {
|
|
322
328
|
stage: 'structure',
|
|
@@ -338,6 +344,7 @@ export const RULES = deepFreeze({
|
|
|
338
344
|
stage: 'structure',
|
|
339
345
|
positions: ['/classes/{class}/properties/{property}/schema/x-astrale-value-kind'],
|
|
340
346
|
statement: 'The schema of an Edge Property is not a blob: only a Node holds Blob contents.',
|
|
347
|
+
conditions: ['#/$defs/edge-property-definition/properties/schema/not'],
|
|
341
348
|
},
|
|
342
349
|
STRUCTURE_EDGE_ORIENTATION: {
|
|
343
350
|
stage: 'structure',
|
|
@@ -348,6 +355,7 @@ export const RULES = deepFreeze({
|
|
|
348
355
|
],
|
|
349
356
|
statement: 'A directed Edge Class has source and target endpoints and an undirected one two incident endpoints, and only a directed one declares unique tuples or is acyclic.',
|
|
350
357
|
enforces: 'No Edges of an acyclic Class form a directed cycle.',
|
|
358
|
+
conditions: ['#/$defs/edge-class/if'],
|
|
351
359
|
},
|
|
352
360
|
STRUCTURE_FORMAT: {
|
|
353
361
|
stage: 'structure',
|
|
@@ -384,6 +392,10 @@ export const RULES = deepFreeze({
|
|
|
384
392
|
stage: 'structure',
|
|
385
393
|
positions: ANY,
|
|
386
394
|
statement: 'No object of the document has a member named __proto__, except the transitions of a State.',
|
|
395
|
+
conditions: [
|
|
396
|
+
'#/$defs/json-value/anyOf/5/propertyNames/not',
|
|
397
|
+
'#/$defs/value-schema/anyOf/1/propertyNames/not',
|
|
398
|
+
],
|
|
387
399
|
},
|
|
388
400
|
STRUCTURE_REQUIRED: {
|
|
389
401
|
stage: 'structure',
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { ClassRef } from '../../addressing/index.js';
|
|
2
2
|
import type { ClassDefinition } from '../../language/class/model/class.js';
|
|
3
|
+
import type { ValueSchemaRegistry } from '../../language/value/reference/registry.js';
|
|
3
4
|
import type { DefinitionAnalysisResult } from '../analysis/class.js';
|
|
4
5
|
import type { SchemaDiagnostic } from '../diagnostic.js';
|
|
5
6
|
import type { DomainResolutionContext } from '../resolution/context.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Validate the semantic contracts owned by Class declarations. `registry` is the candidate's value
|
|
9
|
+
* registry; each dependency's is built at most once, so the Classes share them.
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateDefinitions(environment: DomainResolutionContext, analysis: DefinitionAnalysisResult, registry: ValueSchemaRegistry): readonly SchemaDiagnostic[];
|
|
8
12
|
/** Whether `child` safely refines its parent Edge contract `parent`. */
|
|
9
13
|
export declare function edgeContractRefines(environment: DomainResolutionContext, child: Extract<ClassDefinition, {
|
|
10
14
|
readonly kind: 'edge';
|
|
@@ -10,13 +10,24 @@ import { isBlobValueSchema } from '../../language/value/model/blob.js';
|
|
|
10
10
|
import { classifyOrdering } from '../../language/value/ordering.js';
|
|
11
11
|
import { escapePointerToken } from '../../language/value/syntax/pointer.js';
|
|
12
12
|
import { diagnostic } from '../diagnostic.js';
|
|
13
|
-
import { createDomainValueSchemaRegistry
|
|
13
|
+
import { createDomainValueSchemaRegistry } from '../resolution/value.registry.js';
|
|
14
14
|
const formatDefinitionRef = Key.of;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Validate the semantic contracts owned by Class declarations. `registry` is the candidate's value
|
|
17
|
+
* registry; each dependency's is built at most once, so the Classes share them.
|
|
18
|
+
*/
|
|
19
|
+
export function validateDefinitions(environment, analysis, registry) {
|
|
17
20
|
const diagnostics = [...analysis.diagnostics];
|
|
18
21
|
const schema = environment.candidate;
|
|
19
22
|
let scope;
|
|
23
|
+
const registries = new Map([[schema.origin, registry]]);
|
|
24
|
+
const registryOf = (origin) => {
|
|
25
|
+
if (!registries.has(origin)) {
|
|
26
|
+
const dependency = environment.dependencies.get(origin);
|
|
27
|
+
registries.set(origin, dependency === undefined ? undefined : createDomainValueSchemaRegistry(dependency));
|
|
28
|
+
}
|
|
29
|
+
return registries.get(origin);
|
|
30
|
+
};
|
|
20
31
|
for (const [name, declaration] of Object.entries(schema.classes)) {
|
|
21
32
|
const ref = { origin: schema.origin, kind: 'class', name };
|
|
22
33
|
const pointer = `/classes/${escapePointerToken(name)}`;
|
|
@@ -27,7 +38,7 @@ export function validateDefinitions(environment, analysis) {
|
|
|
27
38
|
(declaration.policies.read !== undefined || declaration.policies.traverse !== undefined)) {
|
|
28
39
|
diagnostics.push(diagnostic('DV_CLASS_ABSTRACT', `${pointer}/policies`, 'Abstract Classes cannot declare observation Policies.'));
|
|
29
40
|
}
|
|
30
|
-
validateFacet(
|
|
41
|
+
validateFacet(registry, declaration, pointer, diagnostics);
|
|
31
42
|
validateParents(environment, ref, declaration.kind, declaration.extends, diagnostics);
|
|
32
43
|
if (declaration.kind === 'edge') {
|
|
33
44
|
validateEdgeContract(environment, ref, declaration, pointer, diagnostics);
|
|
@@ -35,12 +46,11 @@ export function validateDefinitions(environment, analysis) {
|
|
|
35
46
|
validateEdgeLifetimes(scope, ref, declaration, pointer, diagnostics);
|
|
36
47
|
validateEdgeClassComposition(environment, ref, declaration, pointer, diagnostics);
|
|
37
48
|
}
|
|
38
|
-
validateUniqueProperties(
|
|
49
|
+
validateUniqueProperties(registryOf, analysis, ref, declaration, pointer, diagnostics);
|
|
39
50
|
}
|
|
40
51
|
return Object.freeze(diagnostics);
|
|
41
52
|
}
|
|
42
|
-
function validateFacet(
|
|
43
|
-
const valueRegistry = createValueSchemaRegistry(environment);
|
|
53
|
+
function validateFacet(valueRegistry, declaration, pointer, diagnostics) {
|
|
44
54
|
for (const [name, property] of Object.entries(declaration.properties)) {
|
|
45
55
|
const propertyPointer = `${pointer}/properties/${escapePointerToken(name)}`;
|
|
46
56
|
// The structure refuses a blob on an Edge Property and a State Property on an Edge Class.
|
|
@@ -147,7 +157,7 @@ function validateEdgeContract(environment, owner, declaration, pointer, diagnost
|
|
|
147
157
|
* inherit without redefinition, so the key reads one meaning on every descendant it covers. A Node
|
|
148
158
|
* tuple names Properties only; an Edge tuple names them as `{ property }` beside its other fields.
|
|
149
159
|
*/
|
|
150
|
-
function validateUniqueProperties(
|
|
160
|
+
function validateUniqueProperties(registryOf, analysis, owner, declaration, pointer, diagnostics) {
|
|
151
161
|
if (declaration.kind === 'edge' && declaration.orientation !== 'directed')
|
|
152
162
|
return;
|
|
153
163
|
if (declaration.unique === undefined)
|
|
@@ -156,20 +166,6 @@ function validateUniqueProperties(environment, analysis, owner, declaration, poi
|
|
|
156
166
|
if (properties === undefined)
|
|
157
167
|
return;
|
|
158
168
|
const code = declaration.kind === 'node' ? 'DV_NODE_UNIQUE' : 'DV_EDGE_UNIQUE';
|
|
159
|
-
const registries = new Map();
|
|
160
|
-
const registryOf = (origin) => {
|
|
161
|
-
if (!registries.has(origin)) {
|
|
162
|
-
const schema = environment.dependencies.get(origin);
|
|
163
|
-
const registry = origin === environment.candidate.origin
|
|
164
|
-
? createValueSchemaRegistry(environment)
|
|
165
|
-
: schema === undefined
|
|
166
|
-
? undefined
|
|
167
|
-
: createDomainValueSchemaRegistry(schema);
|
|
168
|
-
if (registry !== undefined)
|
|
169
|
-
registries.set(origin, registry);
|
|
170
|
-
}
|
|
171
|
-
return registries.get(origin);
|
|
172
|
-
};
|
|
173
169
|
const check = (key, at) => {
|
|
174
170
|
const property = properties.get(key);
|
|
175
171
|
if (property === undefined) {
|