@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.
@@ -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
- return this.scope.concreteNodeClasses.filter((candidate) => this.#classSatisfiesEndpoint(candidate, accepts));
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(this.scope.allNodeClasses.flatMap((ref) => this.#applyExact(candidates, ref)));
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.requirements.every((required) => this.scope.classExtends(exact, required))) {
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.scope.classExtends(candidate.ref, required))
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.scope.classExtends(candidate, accept));
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
- : `open:${value.requirements.map(definitionRefIndex).sort().join('|')}`;
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
- const registered = {
40
- ...target,
41
- diagnosticPointer: `${pointer}/registry/graphNodePath`,
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
- validateDomainRegistry(registry.current, `${pointer}/registry/current`, false, diagnostics);
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
- const domainPointer = `${pointer}/registry/${lane}/${index}`;
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', domainPointer, `Registry origin ${domain.origin} is duplicated in the exact closure.`));
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
- validateGraphNodePathResource(registry.graphNodePath, `${pointer}/registry/graphNodePath`, diagnostics);
41
- return { index: { registry, domains, dependenciesByOwner }, diagnostics };
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
- if (!isOriginVisibleFrom(index, ownerOrigin, ref.origin))
49
- return false;
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, pointer, revisionRequired, diagnostics) {
70
+ function validateDomainRegistry(domain, entry, revisionRequired, pointer, diagnostics) {
55
71
  if (!DOMAIN_ORIGIN_PATTERN.test(domain.origin)) {
56
- diagnostics.push(diagnostic('VS_REFERENCE_RESOLUTION', `${pointer}/origin`, 'Registry origin is invalid.'));
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', `${pointer}/revision`, 'A dependency-closure registry entry requires its exact sha256 revision.'));
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', `${pointer}/nodeDefinitions`, 'A registry domain can list only its own Node Definitions.'));
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, and reads target node, cardinality one and a non-empty set of Node Class refs it accepts.",
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
- "type": "object"
2131
+ "$ref": "#/$defs/path-annotation"
2104
2132
  },
2105
2133
  "additionalProperties": {
2106
2134
  "$ref": "#/$defs/value-subschema"
@@ -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
@@ -38,7 +38,8 @@ export const RULES = deepFreeze({
38
38
  },
39
39
  DV_CALLABLE_IO: {
40
40
  stage: 'rule',
41
- positions: [...at('/input/mode'), ...at('/output/mode')],
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
- /** Validate the semantic contracts owned by Class declarations. */
7
- export declare function validateDefinitions(environment: DomainResolutionContext, analysis: DefinitionAnalysisResult): readonly SchemaDiagnostic[];
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, createValueSchemaRegistry, } from '../resolution/value.registry.js';
13
+ import { createDomainValueSchemaRegistry } from '../resolution/value.registry.js';
14
14
  const formatDefinitionRef = Key.of;
15
- /** Validate the semantic contracts owned by Class declarations. */
16
- export function validateDefinitions(environment, analysis) {
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(environment, declaration, pointer, diagnostics);
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(environment, analysis, ref, declaration, pointer, diagnostics);
49
+ validateUniqueProperties(registryOf, analysis, ref, declaration, pointer, diagnostics);
39
50
  }
40
51
  return Object.freeze(diagnostics);
41
52
  }
42
- function validateFacet(environment, declaration, pointer, diagnostics) {
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(environment, analysis, owner, declaration, pointer, diagnostics) {
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) {