@astrale-os/kernel-dsl 1.0.0-beta.15 → 1.0.0-beta.17

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 (87) hide show
  1. package/dist/v1/builder/class/authoring.js +3 -2
  2. package/dist/v1/builder/class/input.d.ts +45 -14
  3. package/dist/v1/builder/index.d.ts +2 -1
  4. package/dist/v1/builder/lowering/class.d.ts +8 -0
  5. package/dist/v1/builder/lowering/class.js +40 -16
  6. package/dist/v1/builder/lowering/schema/compile.js +3 -3
  7. package/dist/v1/builder/policy/authoring.js +97 -88
  8. package/dist/v1/bundle/model.js +1 -1
  9. package/dist/v1/compiled/admission/accept.js +1 -7
  10. package/dist/v1/compiled/admission/semantics/class.js +2 -3
  11. package/dist/v1/compiled/admission/semantics/policy/parity.js +25 -13
  12. package/dist/v1/compiled/admission/semantics/policy/typing.js +2 -2
  13. package/dist/v1/compiled/admission/wire/class.js +23 -18
  14. package/dist/v1/compiled/admission/wire/policy.d.ts +4 -3
  15. package/dist/v1/compiled/admission/wire/policy.js +8 -7
  16. package/dist/v1/compiled/loader/load.js +1 -1
  17. package/dist/v1/domain/create.js +1 -1
  18. package/dist/v1/domain/model/input.d.ts +2 -1
  19. package/dist/v1/domain/projection/classes.js +1 -1
  20. package/dist/v1/domain/validation/create.d.ts +2 -1
  21. package/dist/v1/domain/validation/create.js +1 -1
  22. package/dist/v1/index.d.ts +1 -1
  23. package/dist/v1/language/.spec/schemas/draft-2020-12.schema.json +462 -0
  24. package/dist/v1/language/callable/resolved.d.ts +11 -2
  25. package/dist/v1/language/class/model/class.d.ts +17 -5
  26. package/dist/v1/language/freeze.d.ts +5 -0
  27. package/dist/v1/{schema/canonical → language}/freeze.js +4 -0
  28. package/dist/v1/language/policy/analysis/typing.d.ts +5 -2
  29. package/dist/v1/language/policy/analysis/typing.js +158 -42
  30. package/dist/v1/language/policy/model/policy.d.ts +44 -4
  31. package/dist/v1/language/policy/model/policy.js +14 -0
  32. package/dist/v1/language/policy/normalization/normalize.d.ts +6 -6
  33. package/dist/v1/language/policy/normalization/normalize.js +5 -5
  34. package/dist/v1/language/validation/rule.d.ts +22 -6
  35. package/dist/v1/language/validation/rule.js +4 -15
  36. package/dist/v1/language/value/evaluate/program.d.ts +2 -1
  37. package/dist/v1/language/value/evaluate/program.js +2 -2
  38. package/dist/v1/language/value/evaluate/validate.d.ts +2 -2
  39. package/dist/v1/language/value/evaluate/validate.js +2 -2
  40. package/dist/v1/language/value/ordering.d.ts +5 -0
  41. package/dist/v1/language/value/ordering.js +14 -0
  42. package/dist/v1/language/value/profile/json.validate.d.ts +3 -1
  43. package/dist/v1/language/value/profile/json.validate.js +3 -2
  44. package/dist/v1/language/value/reference/closure.js +3 -4
  45. package/dist/v1/language/value/reference/registry.index.d.ts +2 -0
  46. package/dist/v1/language/value/reference/registry.index.js +31 -15
  47. package/dist/v1/schema/.spec/schemas/domain-schema.schema.json +210 -27
  48. package/dist/v1/schema/admission/callable.js +1 -1
  49. package/dist/v1/schema/canonical/revision.js +4 -4
  50. package/dist/v1/schema/compatibility/comparators/callables.js +1 -1
  51. package/dist/v1/schema/compatibility/comparators/classes.js +4 -4
  52. package/dist/v1/schema/compatibility/comparators/members.js +1 -1
  53. package/dist/v1/schema/compatibility/comparators/values.js +2 -2
  54. package/dist/v1/schema/compatibility/report.js +1 -1
  55. package/dist/v1/schema/compatibility/versioning/index.js +1 -1
  56. package/dist/v1/schema/model/schema.js +1 -1
  57. package/dist/v1/schema/normalization/domain.js +9 -8
  58. package/dist/v1/schema/resolution/domain.js +1 -1
  59. package/dist/v1/schema/resolution/origins.js +3 -2
  60. package/dist/v1/schema/rule.d.ts +8 -14
  61. package/dist/v1/schema/rule.js +14 -2
  62. package/dist/v1/schema/validation/class.d.ts +6 -2
  63. package/dist/v1/schema/validation/class.js +20 -25
  64. package/dist/v1/schema/validation/domain.js +1 -1
  65. package/dist/v1/schema/validation/domain.json-schema.d.ts +2 -2
  66. package/dist/v1/schema/validation/domain.json-schema.js +20 -6
  67. package/dist/v1/schema/validation/domain.structure.d.ts +4765 -1005
  68. package/dist/v1/schema/validation/domain.structure.js +21 -9
  69. package/dist/v1/schema/validation/json.json-schema.d.ts +18 -2
  70. package/dist/v1/schema/validation/json.json-schema.js +14 -4
  71. package/dist/v1/schema/validation/policy/budgets.d.ts +1 -1
  72. package/dist/v1/schema/validation/policy/budgets.js +4 -4
  73. package/dist/v1/schema/validation/policy/context.d.ts +1 -2
  74. package/dist/v1/schema/validation/policy/context.js +2 -7
  75. package/dist/v1/schema/validation/policy/uses.d.ts +10 -2
  76. package/dist/v1/schema/validation/policy/uses.js +26 -13
  77. package/dist/v1/schema/validation/policy/validate.js +2 -3
  78. package/dist/v1/schema/validation/value-api.d.ts +2 -0
  79. package/dist/v1/schema/validation/value-api.js +2 -0
  80. package/dist/v1/schema/validation/value.json-schema.d.ts +7 -9
  81. package/dist/v1/schema/validation/value.json-schema.js +38 -27
  82. package/package.json +2 -1
  83. package/dist/v1/schema/canonical/domain.d.ts +0 -3
  84. package/dist/v1/schema/canonical/domain.digest.d.ts +0 -3
  85. package/dist/v1/schema/canonical/domain.digest.js +0 -5
  86. package/dist/v1/schema/canonical/domain.js +0 -5
  87. package/dist/v1/schema/canonical/freeze.d.ts +0 -1
@@ -1,3 +1,4 @@
1
+ import type { Options } from '@astrale-os/kernel-dsl/value';
1
2
  import type { DefinitionKey, FunctionRef, Key, MethodKey } from '../../addressing/index.js';
2
3
  import type { ResolvedClass } from '../class/resolved.js';
3
4
  import type { CallablePolicy } from '../policy/model/policy.js';
@@ -36,7 +37,11 @@ export interface ResolvedMethod<out Input = unknown, out Output = unknown, out S
36
37
  readonly static: Static;
37
38
  readonly abstract: Abstract;
38
39
  };
39
- validateInput(input: unknown): readonly Diagnostic[];
40
+ /**
41
+ * The issues of `input` against the callable's input schema, `options` overriding the portable
42
+ * limits it is admitted under: those it crossed the wire under, for a call's input.
43
+ */
44
+ validateInput(input: unknown, options?: Options): readonly Diagnostic[];
40
45
  validateInputField(field: string, input: unknown): readonly Diagnostic[];
41
46
  validateOutput(output: unknown): readonly Diagnostic[];
42
47
  readonly __input?: Input;
@@ -62,7 +67,11 @@ export interface ResolvedFunction<out Input = unknown, out Output = unknown, out
62
67
  /** Present only when the callable input is a safely enumerable closed object. */
63
68
  readonly inputFields?: readonly string[];
64
69
  readonly definition: CallableDefinitionOf<FunctionDefinition, Auth, Policy>;
65
- validateInput(input: unknown): readonly Diagnostic[];
70
+ /**
71
+ * The issues of `input` against the callable's input schema, `options` overriding the portable
72
+ * limits it is admitted under: those it crossed the wire under, for a call's input.
73
+ */
74
+ validateInput(input: unknown, options?: Options): readonly Diagnostic[];
66
75
  validateInputField(field: string, input: unknown): readonly Diagnostic[];
67
76
  validateOutput(output: unknown): readonly Diagnostic[];
68
77
  readonly __input?: Input;
@@ -1,6 +1,6 @@
1
1
  import type { ClassRef, DefinitionBase, PropertyKey } from '../../../addressing/index.js';
2
2
  import type { MethodRecord } from '../../callable/model/callable.js';
3
- import type { ClassPolicy, NodeClassPolicy } from '../../policy/model/policy.js';
3
+ import type { EdgeClassPolicy, EdgeClassWritePolicy, NodeClassCreatePolicy, NodeClassPolicy, NodeClassWritePolicy } from '../../policy/model/policy.js';
4
4
  import type { DataFacet } from './data.js';
5
5
  import type { EdgeContract } from './edge.js';
6
6
  import type { EdgePropertyDefinition, NodePropertyDefinition, PropertyFacet } from './property.js';
@@ -10,15 +10,27 @@ export type ClassKind = 'node' | 'edge';
10
10
  * on that Domain; `'internal'`, those of its Domain only; `'final'`, none.
11
11
  */
12
12
  export type Inheritance = 'open' | 'internal' | 'final';
13
- /** An absent observation rule admits every caller; a delegation is still scoped by its ceiling. */
13
+ /**
14
+ * An absent observation rule admits every caller; a delegation is still scoped by its ceiling. An
15
+ * absent write rule admits only the Domain that declares the Class.
16
+ */
14
17
  export interface NodeClassPolicies {
15
18
  readonly read?: NodeClassPolicy;
16
19
  readonly traverse?: NodeClassPolicy;
20
+ readonly create?: NodeClassCreatePolicy;
21
+ readonly update?: NodeClassWritePolicy;
22
+ readonly delete?: NodeClassWritePolicy;
17
23
  }
18
- /** An absent observation rule admits every caller; a delegation is still scoped by its ceiling. */
24
+ /**
25
+ * An absent observation rule admits every caller; a delegation is still scoped by its ceiling. An
26
+ * absent write rule admits only the Domain that declares the Class.
27
+ */
19
28
  export interface EdgeClassPolicies {
20
- readonly read?: ClassPolicy;
21
- readonly traverse?: ClassPolicy;
29
+ readonly read?: EdgeClassPolicy;
30
+ readonly traverse?: EdgeClassPolicy;
31
+ readonly create?: EdgeClassWritePolicy;
32
+ readonly update?: EdgeClassWritePolicy;
33
+ readonly delete?: EdgeClassWritePolicy;
22
34
  }
23
35
  /**
24
36
  * The Properties one key of a Node Class names, own or inherited, which no two Nodes of the Classes
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Freeze `value` and every object it reaches, in place. An object already frozen is taken as frozen
3
+ * through, so a value built from frozen parts is frozen without walking them again.
4
+ */
5
+ export declare function deepFreeze<T>(value: T): Readonly<T>;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Freeze `value` and every object it reaches, in place. An object already frozen is taken as frozen
3
+ * through, so a value built from frozen parts is frozen without walking them again.
4
+ */
1
5
  export function deepFreeze(value) {
2
6
  if (value === null || typeof value !== 'object' || Object.isFrozen(value)) {
3
7
  return value;
@@ -2,6 +2,7 @@ import type { ClassRef, PolicyRef } from '../../../addressing/index.js';
2
2
  import type { EdgeContract } from '../../class/model/edge.js';
3
3
  import type { PolicyExpression, PolicyObjectRef } from '../model/policy.js';
4
4
  import type { CheckLeafClasses } from './checks.js';
5
+ import { type ClassAncestry } from '../../class/inheritance.js';
5
6
  import { type PatternBranch } from './branches.js';
6
7
  export type ObjectContract = {
7
8
  readonly kind: 'default';
@@ -20,11 +21,13 @@ export type PolicyUseContract = {
20
21
  /** What Policy typing reads of the closure it types against: source or compiled admission. */
21
22
  export interface PolicyTypingScope {
22
23
  readonly normalized: ReadonlyMap<string, PolicyExpression>;
24
+ /** Each resolves, through `resolveClass`, to a Node Class. */
23
25
  readonly allNodeClasses: readonly ClassRef[];
26
+ /** The concrete Classes of `allNodeClasses`, in its order. */
24
27
  readonly concreteNodeClasses: readonly ClassRef[];
25
28
  isLocalPolicy(ref: PolicyRef): boolean;
26
- /** Whether `candidate` is the `required` Class or a same-kind descendant. */
27
- classExtends(candidate: ClassRef, required: ClassRef): boolean;
29
+ /** The Class a ref names in the closure, as satisfaction reads it. */
30
+ resolveClass(ref: ClassRef): ClassAncestry | undefined;
28
31
  edgeDeclaration(ref: ClassRef): EdgeContract | undefined;
29
32
  /** The contract of a Node a Policy pattern references, `undefined` when it has none. */
30
33
  referenceContract(ref: PolicyObjectRef): ObjectContract | undefined;
@@ -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,7 @@ 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
+ #index;
18
20
  constructor(scope) {
19
21
  this.scope = scope;
20
22
  }
@@ -27,9 +29,7 @@ export class PolicyTyping {
27
29
  }
28
30
  /** Whether the Policy accepts every concrete source and target pair the Edge Class accepts. */
29
31
  policyAcceptsEdgeClass(name, declaration) {
30
- const endpoints = declaration.orientation === 'directed'
31
- ? [declaration.endpoints.source, declaration.endpoints.target]
32
- : declaration.endpoints;
32
+ const endpoints = endpointPair(declaration);
33
33
  const sources = this.#concreteEndpointClasses(endpoints[0].accepts);
34
34
  const targets = this.#concreteEndpointClasses(endpoints[1].accepts);
35
35
  return sources.every((source) => targets.every((target) => this.#policyAcceptsEdge(name, { kind: 'exact', ref: source }, { kind: 'exact', ref: target })));
@@ -69,7 +69,12 @@ export class PolicyTyping {
69
69
  return branches !== undefined && branches.every((branch) => this.branchIsTyped(branch, use));
70
70
  }
71
71
  #concreteEndpointClasses(accepts) {
72
- return this.scope.concreteNodeClasses.filter((candidate) => this.#classSatisfiesEndpoint(candidate, accepts));
72
+ const { concrete } = this.#nodeClassIndex();
73
+ return this.#satisfyingOneOf(accepts).filter((ref) => concrete.has(definitionRefIndex(ref)));
74
+ }
75
+ #nodeClassIndex() {
76
+ this.#index ??= nodeClassIndex(this.scope);
77
+ return this.#index;
73
78
  }
74
79
  #expandedBranches(name, visiting, namespace) {
75
80
  if (visiting.has(name))
@@ -158,7 +163,7 @@ export class PolicyTyping {
158
163
  if (contract.kind === 'exact')
159
164
  return this.#applyExact(candidates, contract.ref);
160
165
  if (contract.kind === 'default') {
161
- return uniqueCandidates(this.scope.allNodeClasses.flatMap((ref) => this.#applyExact(candidates, ref)));
166
+ return this.#applyAnyNodeClass(candidates).filter((candidate) => candidate.kind !== 'exact' || this.#isNodeClass(candidate.ref));
162
167
  }
163
168
  return candidates;
164
169
  }
@@ -183,34 +188,38 @@ export class PolicyTyping {
183
188
  if (definitionRefIndex(candidate.ref) === definitionRefIndex(exact))
184
189
  result.push(candidate);
185
190
  }
186
- else if (candidate.requirements.every((required) => this.scope.classExtends(exact, required))) {
191
+ else if (candidate.kind === 'node') {
192
+ if (this.#isNodeClass(exact) && this.#satisfiesLists(exact, candidate.lists)) {
193
+ result.push({ kind: 'exact', ref: exact });
194
+ }
195
+ }
196
+ else if (candidate.requirements.every((required) => this.#classExtends(exact, required))) {
187
197
  result.push({ kind: 'exact', ref: exact });
188
198
  }
189
199
  }
190
- return uniqueCandidates(result);
200
+ return uniqueBy(result, candidateKey);
191
201
  }
192
202
  #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 }))));
203
+ return uniqueBy(candidates.flatMap((candidate) => candidate.kind === 'exact' ? [candidate] : this.#nodeCandidates(candidate)), candidateKey);
198
204
  }
199
205
  #applyRequirement(candidates, required) {
200
206
  const result = [];
201
207
  for (const candidate of candidates) {
202
208
  if (candidate.kind === 'exact') {
203
- if (this.scope.classExtends(candidate.ref, required))
209
+ if (this.#classExtends(candidate.ref, required))
204
210
  result.push(candidate);
205
211
  }
212
+ else if (candidate.kind === 'node') {
213
+ result.push(...this.#inhabited([...candidate.lists, [required]]));
214
+ }
206
215
  else {
207
216
  result.push({
208
217
  kind: 'open',
209
- requirements: uniqueClasses([...candidate.requirements, required]),
218
+ requirements: uniqueBy([...candidate.requirements, required], definitionRefIndex),
210
219
  });
211
220
  }
212
221
  }
213
- return uniqueCandidates(result);
222
+ return uniqueBy(result, candidateKey);
214
223
  }
215
224
  #applyEndpoint(candidates, accepts) {
216
225
  const result = [];
@@ -220,18 +229,125 @@ export class PolicyTyping {
220
229
  result.push(candidate);
221
230
  continue;
222
231
  }
232
+ if (candidate.kind === 'node') {
233
+ if (accepts.length > 0)
234
+ result.push(...this.#inhabited([...candidate.lists, accepts]));
235
+ continue;
236
+ }
223
237
  for (const accept of accepts) {
224
238
  result.push({
225
239
  kind: 'open',
226
- requirements: uniqueClasses([...candidate.requirements, accept]),
240
+ requirements: uniqueBy([...candidate.requirements, accept], definitionRefIndex),
227
241
  });
228
242
  }
229
243
  }
230
- return uniqueCandidates(result);
244
+ return uniqueBy(result, candidateKey);
231
245
  }
232
246
  #classSatisfiesEndpoint(candidate, accepts) {
233
- return accepts.some((accept) => this.scope.classExtends(candidate, accept));
247
+ return accepts.some((accept) => this.#classExtends(candidate, accept));
248
+ }
249
+ /** Whether `candidate` is the `required` Class or a same-kind descendant. */
250
+ #classExtends(candidate, required) {
251
+ const resolve = (ref) => this.scope.resolveClass(ref);
252
+ const actual = resolve(candidate);
253
+ const target = resolve(required);
254
+ return actual !== undefined && target !== undefined && classSatisfies(actual, target, resolve);
234
255
  }
256
+ /**
257
+ * The Node Classes of the closure that satisfy one Class of `accepts`, in closure order: those
258
+ * `#classSatisfiesEndpoint` accepts, read from the index rather than tested one by one.
259
+ */
260
+ #satisfyingOneOf(accepts) {
261
+ const index = this.#nodeClassIndex();
262
+ const positions = new Set();
263
+ for (const accept of accepts) {
264
+ const required = this.scope.resolveClass(accept);
265
+ if (required?.kind !== 'node')
266
+ continue;
267
+ if (isGraphRoot(required))
268
+ return index.nodeClasses;
269
+ for (const position of index.satisfying.get(definitionRefIndex(required.ref)) ?? []) {
270
+ positions.add(position);
271
+ }
272
+ }
273
+ return [...positions].sort((left, right) => left - right).map((at) => index.nodeClasses[at]);
274
+ }
275
+ #satisfiesLists(ref, lists) {
276
+ return lists.every((accepts) => this.#classSatisfiesEndpoint(ref, accepts));
277
+ }
278
+ #isNodeClass(ref) {
279
+ return this.#nodeClassIndex().nodes.has(definitionRefIndex(ref));
280
+ }
281
+ /** The Node Classes an open or `node` candidate can be. */
282
+ #nodeCandidates(candidate) {
283
+ return candidate.kind === 'node'
284
+ ? [candidate]
285
+ : this.#inhabited(candidate.requirements.map((required) => [required]));
286
+ }
287
+ /**
288
+ * The `node` candidate of these lists when some Node Class of the closure satisfies them, so that
289
+ * an empty one is dropped where its expansion would have been empty. A witness is almost always a
290
+ * Class the lists name; only when none is does the search read the Classes that satisfy the
291
+ * narrowest list, every witness being one of them.
292
+ */
293
+ #inhabited(lists) {
294
+ const fits = (ref) => this.#isNodeClass(ref) && this.#satisfiesLists(ref, lists);
295
+ const inhabited = lists.some((accepts) => accepts.some(fits)) ||
296
+ (lists.length === 0
297
+ ? this.scope.allNodeClasses
298
+ : lists
299
+ .map((accepts) => this.#satisfyingOneOf(accepts))
300
+ .reduce((narrowest, next) => (next.length < narrowest.length ? next : narrowest))).some(fits);
301
+ return inhabited ? [{ kind: 'node', lists }] : [];
302
+ }
303
+ }
304
+ /**
305
+ * Index each Node Class of the closure under every Class its ancestry reaches, walked as
306
+ * `classSatisfies` walks it: the Class itself, then each declared parent that resolves to a Node
307
+ * Class. A Class then satisfies a non-root Node Class exactly when it is indexed under it.
308
+ * `classSatisfies` compares origin and name; every ClassRef has kind `class`, so the definition
309
+ * index of a ref keys it as that comparison does.
310
+ */
311
+ function nodeClassIndex(scope) {
312
+ const nodeClasses = [];
313
+ const satisfying = new Map();
314
+ for (const ref of scope.allNodeClasses) {
315
+ const actual = scope.resolveClass(ref);
316
+ if (actual?.kind !== 'node')
317
+ continue;
318
+ const position = nodeClasses.push(ref) - 1;
319
+ for (const key of ancestryKeys(actual, scope)) {
320
+ const positions = satisfying.get(key);
321
+ if (positions === undefined)
322
+ satisfying.set(key, [position]);
323
+ else
324
+ positions.push(position);
325
+ }
326
+ }
327
+ return {
328
+ nodeClasses,
329
+ nodes: new Set(nodeClasses.map(definitionRefIndex)),
330
+ concrete: new Set(scope.concreteNodeClasses.map(definitionRefIndex)),
331
+ satisfying,
332
+ };
333
+ }
334
+ /** The keys of the Node Classes a Node Class's ancestry reaches, itself included. */
335
+ function ancestryKeys(actual, scope) {
336
+ const seen = new Set();
337
+ const keys = new Set();
338
+ const pending = [actual];
339
+ for (let value = pending.pop(); value !== undefined; value = pending.pop()) {
340
+ if (seen.has(value))
341
+ continue;
342
+ seen.add(value);
343
+ keys.add(definitionRefIndex(value.ref));
344
+ for (const parent of value.extends) {
345
+ const resolved = scope.resolveClass(parent);
346
+ if (resolved?.kind === 'node')
347
+ pending.push(resolved);
348
+ }
349
+ }
350
+ return keys;
235
351
  }
236
352
  function targetContract(term, use) {
237
353
  if (use.kind === 'object')
@@ -262,9 +378,7 @@ function variantsForEdge(use, typing) {
262
378
  const declaration = typing.scope.edgeDeclaration(edge.class);
263
379
  if (declaration === undefined)
264
380
  return [];
265
- const endpoints = declaration.orientation === 'directed'
266
- ? [declaration.endpoints.source, declaration.endpoints.target]
267
- : declaration.endpoints;
381
+ const endpoints = endpointPair(declaration);
268
382
  const bounds = edge.repeat ?? { min: 1, max: 1 };
269
383
  if (!Number.isInteger(bounds.min) ||
270
384
  !Number.isInteger(bounds.max) ||
@@ -306,7 +420,13 @@ function variantsForEdge(use, typing) {
306
420
  });
307
421
  }
308
422
  }
309
- return uniqueEdgeVariants(variants);
423
+ return uniqueBy(variants, edgeVariantKey);
424
+ }
425
+ /** The two endpoints of an Edge Class, a directed one's as source then target. */
426
+ function endpointPair(declaration) {
427
+ return declaration.orientation === 'directed'
428
+ ? [declaration.endpoints.source, declaration.endpoints.target]
429
+ : declaration.endpoints;
310
430
  }
311
431
  function startEndpoint(endpoints, reversed) {
312
432
  return reversed ? endpoints[1] : endpoints[0];
@@ -314,26 +434,22 @@ function startEndpoint(endpoints, reversed) {
314
434
  function endEndpoint(endpoints, reversed) {
315
435
  return reversed ? endpoints[0] : endpoints[1];
316
436
  }
317
- function uniqueEdgeVariants(values) {
318
- const result = new Map();
319
- for (const value of values) {
320
- const key = `${value.constraints
321
- .map(([term, accepts]) => `${term}:${accepts.map(definitionRefIndex).sort().join(',')}`)
322
- .join('|')}=${value.equalities.map(([left, right]) => `${left}:${right}`).join('|')}`;
323
- result.set(key, value);
324
- }
325
- return [...result.values()];
437
+ /** The values with distinct keys, each at its key's first position and holding its last value. */
438
+ function uniqueBy(values, key) {
439
+ return [...new Map(values.map((value) => [key(value), value])).values()];
326
440
  }
327
- function uniqueCandidates(values) {
328
- const result = new Map();
329
- for (const value of values) {
330
- const key = value.kind === 'exact'
331
- ? `exact:${definitionRefIndex(value.ref)}`
332
- : `open:${value.requirements.map(definitionRefIndex).sort().join('|')}`;
333
- result.set(key, value);
334
- }
335
- return [...result.values()];
441
+ function edgeVariantKey(variant) {
442
+ const constraints = variant.constraints.map(([term, accepts]) => `${term}:${classSetKey(accepts)}`);
443
+ const equalities = variant.equalities.map(([left, right]) => `${left}:${right}`);
444
+ return `${constraints.join('|')}=${equalities.join('|')}`;
445
+ }
446
+ function candidateKey(candidate) {
447
+ if (candidate.kind === 'exact')
448
+ return `exact:${definitionRefIndex(candidate.ref)}`;
449
+ if (candidate.kind === 'open')
450
+ return `open:${classSetKey(candidate.requirements)}`;
451
+ return `node:${candidate.lists.map(classSetKey).sort().join('|')}`;
336
452
  }
337
- function uniqueClasses(values) {
338
- return [...new Map(values.map((value) => [definitionRefIndex(value), value])).values()];
453
+ function classSetKey(refs) {
454
+ return refs.map(definitionRefIndex).sort().join(',');
339
455
  }
@@ -90,23 +90,63 @@ export type PolicyObjectRef = ClassRef | FunctionRef | PolicyRef | ViewRef | Cor
90
90
  export declare function policyObjectKey(ref: PolicyObjectRef): string;
91
91
  export declare function policyObjectOrigin(ref: PolicyObjectRef): string;
92
92
  export declare function projectedMemberClass(ref: Exclude<PolicyObjectRef, CoreRef>): ClassRef;
93
+ export type ClassObservation = 'read' | 'traverse';
94
+ export type ClassWrite = 'create' | 'update' | 'delete';
95
+ export type ClassOperation = ClassObservation | ClassWrite;
96
+ /**
97
+ * Every Class slot: the observations, which admit every caller when absent, then the writes, which
98
+ * admit only the Domain that declares the Class when absent.
99
+ */
100
+ export declare const CLASS_OPERATIONS: readonly ClassOperation[];
101
+ export declare function isClassObservation(operation: ClassOperation): operation is ClassObservation;
93
102
  /**
94
103
  * One Class observation slot: a named Policy, or `false`, the rule no caller satisfies. Its other
95
104
  * value has no spelling there: an absent observation Policy already means `true`.
96
105
  */
97
106
  export type ClassPolicy = PolicyRef | false;
98
- /** What a Node Class rule may check: the observed Node, or one exact projected member. */
107
+ /**
108
+ * One Class write slot: a named Policy, or `true`, the rule every Identity satisfies. Its other
109
+ * value has no spelling there: an absent write Policy already means the owning Domain only.
110
+ */
111
+ export type ClassWritePolicy = PolicyRef | true;
112
+ /** What a Class rule may check: the observed or written instance, or one exact projected member. */
99
113
  export type ClassRuleObject = Extract<PolicyCheckObject, {
100
114
  readonly kind: 'self' | 'ref';
101
115
  }>;
102
116
  /**
103
- * Policy checks deciding one Node Class observation. `self` is the observed Node; a `ref` object
104
- * is the same Node for every candidate. A lone check of `self` is spelled as its Policy reference.
117
+ * Policy checks deciding one Node Class slot. `self` is the observed or written Node; a `ref`
118
+ * object is the same Node for every candidate. A lone check of `self` is spelled as its Policy
119
+ * reference.
105
120
  */
106
121
  export type ClassRuleChecks = PolicyCheckExpression<PolicyRef, ClassRuleObject>;
122
+ /**
123
+ * Policy checks deciding one Edge Class slot. `self` is the observed or written Edge, checked by a
124
+ * Policy whose source and target are its endpoints; a `ref` object is one exact member, the same
125
+ * Node for every candidate. A lone check of `self` is spelled as its Policy reference.
126
+ */
127
+ export type EdgeRuleChecks = ClassRuleChecks;
128
+ /** What a Node create rule may check: one exact projected member, since the Node is not yet. */
129
+ export type CreateRuleObject = Extract<PolicyCheckObject, {
130
+ readonly kind: 'ref';
131
+ }>;
132
+ /** Policy checks deciding the creation of a Node: every object is a `ref` member. */
133
+ export type CreateRuleChecks = PolicyCheckExpression<PolicyRef, CreateRuleObject>;
107
134
  /** One Node Class observation slot: a Class Policy, or Policy checks over the observed Node. */
108
135
  export type NodeClassPolicy = ClassPolicy | ClassRuleChecks;
109
- export declare function isClassRuleChecks(value: ClassPolicy | ClassRuleChecks | undefined): value is ClassRuleChecks;
136
+ /** One Node Class update or delete slot: a write Policy, or Policy checks over the written Node. */
137
+ export type NodeClassWritePolicy = ClassWritePolicy | ClassRuleChecks;
138
+ /**
139
+ * The Node Class create slot: `true`, or Policy checks over members. A named Policy alone has no
140
+ * object there, so it has no spelling.
141
+ */
142
+ export type NodeClassCreatePolicy = true | CreateRuleChecks;
143
+ /** One Edge Class observation slot: a Class Policy, or Policy checks over the observed Edge. */
144
+ export type EdgeClassPolicy = ClassPolicy | EdgeRuleChecks;
145
+ /** One Edge Class write slot: a write Policy, or Policy checks over the written Edge. */
146
+ export type EdgeClassWritePolicy = ClassWritePolicy | EdgeRuleChecks;
147
+ /** The value of any Class slot. */
148
+ export type ClassSlotPolicy = PolicyRef | boolean | ClassRuleChecks;
149
+ export declare function isClassRuleChecks(value: ClassSlotPolicy | undefined): value is ClassRuleChecks;
110
150
  /**
111
151
  * Whether every way Policy checks can hold passes through a check, so that the caller decides each
112
152
  * admission. `sameNode` compares two objects and names no caller.
@@ -33,6 +33,20 @@ export function projectedMemberClass(ref) {
33
33
  function kernelClassRef(name) {
34
34
  return Object.freeze({ origin: 'kernel.astrale.ai', kind: 'class', name });
35
35
  }
36
+ /**
37
+ * Every Class slot: the observations, which admit every caller when absent, then the writes, which
38
+ * admit only the Domain that declares the Class when absent.
39
+ */
40
+ export const CLASS_OPERATIONS = Object.freeze([
41
+ 'read',
42
+ 'traverse',
43
+ 'create',
44
+ 'update',
45
+ 'delete',
46
+ ]);
47
+ export function isClassObservation(operation) {
48
+ return operation === 'read' || operation === 'traverse';
49
+ }
36
50
  export function isClassRuleChecks(value) {
37
51
  return (typeof value === 'object' &&
38
52
  ('check' in value || 'sameNode' in value || 'allOf' in value || 'anyOf' in value));
@@ -1,4 +1,4 @@
1
- import type { NodeClassPolicy, PolicyCheckExpression, PolicyExpression, PolicyPatternExpression } from '../model/policy.js';
1
+ import type { ClassSlotPolicy, PolicyCheckExpression, PolicyExpression, PolicyPatternExpression } from '../model/policy.js';
2
2
  export declare function normalizePolicyExpression(value: PolicyExpression): PolicyExpression;
3
3
  /**
4
4
  * The fixed point of the single pass: a Pattern the single pass reproduces is returned unchanged,
@@ -6,10 +6,10 @@ export declare function normalizePolicyExpression(value: PolicyExpression): Poli
6
6
  */
7
7
  export declare function normalizePolicyPattern(value: PolicyPatternExpression): PolicyPatternExpression;
8
8
  /**
9
- * The one spelling of a Node Class observation rule: normalized Policy checks without a composition
10
- * of one operand, or the Policy that a lone check of `self` names, which is how a Class has always
11
- * declared one. Callable checks keep their compositions of one operand: their revisions predate
12
- * this rule.
9
+ * The one spelling of a Class rule: normalized Policy checks without a composition of one operand,
10
+ * or the Policy that a lone check of `self` names, which is how a Class has always declared one.
11
+ * Callable checks keep their compositions of one operand: their revisions predate this rule. A
12
+ * create rule checks no `self`, so it keeps its checks.
13
13
  */
14
- export declare function normalizeClassRule(value: NodeClassPolicy): NodeClassPolicy;
14
+ export declare function normalizeClassRule<Slot extends ClassSlotPolicy>(value: Slot): Slot;
15
15
  export declare function normalizePolicyChecks(value: PolicyCheckExpression): PolicyCheckExpression;
@@ -40,16 +40,16 @@ function normalizePatternOnce(value) {
40
40
  return normalizePatternNode(rewritten);
41
41
  }
42
42
  /**
43
- * The one spelling of a Node Class observation rule: normalized Policy checks without a composition
44
- * of one operand, or the Policy that a lone check of `self` names, which is how a Class has always
45
- * declared one. Callable checks keep their compositions of one operand: their revisions predate
46
- * this rule.
43
+ * The one spelling of a Class rule: normalized Policy checks without a composition of one operand,
44
+ * or the Policy that a lone check of `self` names, which is how a Class has always declared one.
45
+ * Callable checks keep their compositions of one operand: their revisions predate this rule. A
46
+ * create rule checks no `self`, so it keeps its checks.
47
47
  */
48
48
  export function normalizeClassRule(value) {
49
49
  if (!isClassRuleChecks(value))
50
50
  return value;
51
51
  const checks = unwrapSingleOperands(normalizePolicyChecks(value));
52
- return 'check' in checks && checks.object.kind === 'self' ? checks.check : checks;
52
+ return ('check' in checks && checks.object.kind === 'self' ? checks.check : checks);
53
53
  }
54
54
  /** Replace each composition of one operand by that operand, merging what it then nests. */
55
55
  function unwrapSingleOperands(value) {
@@ -8,19 +8,35 @@ import type { DiagnosticCode } from './diagnostic.js';
8
8
  * `warning`: a value schema, which the rule reports without refusing it.
9
9
  */
10
10
  export type ValueRuleStage = 'schema' | 'context' | 'value' | 'warning';
11
- /** The rule one value diagnostic code reports. */
12
- export interface ValueRule {
13
- readonly stage: ValueRuleStage;
11
+ /**
12
+ * The rule one diagnostic code reports, whatever it checks: what `schema.RULES` and `VALUE_RULES`
13
+ * say of every code, each with the stages of its own checks.
14
+ */
15
+ export interface DiagnosticRule {
14
16
  /**
15
- * The positions the rule constrains, which its diagnostics point at, relative to the value
16
- * schema, the value or the Domain input it checks: JSON Pointer templates in which a `{name}`
17
- * token stands for one token of the kind it names and `{/path*}` for any number of tokens.
17
+ * The positions the rule constrains, which its diagnostics point at: JSON Pointer templates in
18
+ * which a `{name}` token stands for one token of the kind it names and `{/path*}` for any number
19
+ * of tokens.
18
20
  */
19
21
  readonly positions: readonly [string, ...string[]];
20
22
  /** The rule, in one plain sentence. */
21
23
  readonly statement: string;
22
24
  /** The limits of `schema.limits` the rule enforces, by key, where it enforces some. */
23
25
  readonly limits?: readonly [LimitKey, ...LimitKey[]];
26
+ /**
27
+ * The conditions of the published JSON Schema of the document that state the rule, where some
28
+ * do: the URI fragment of each `if` or `not` subschema whose failure refuses the document with
29
+ * this rule's code.
30
+ */
31
+ readonly conditions?: readonly [string, ...string[]];
32
+ }
33
+ /**
34
+ * The rule one value diagnostic code reports. Its positions are relative to the value schema, the
35
+ * value or the Domain input it checks. A condition refuses a value schema of the document with
36
+ * `DV_VALUE_SCHEMA` and this rule's code as its `valueCode`.
37
+ */
38
+ export interface ValueRule extends DiagnosticRule {
39
+ readonly stage: ValueRuleStage;
24
40
  }
25
41
  /**
26
42
  * Every value diagnostic code with the rule it reports. A code added to the language does not
@@ -1,8 +1,9 @@
1
+ import { deepFreeze } from '../freeze.js';
1
2
  /**
2
3
  * Every value diagnostic code with the rule it reports. A code added to the language does not
3
4
  * compile until it is described here.
4
5
  */
5
- export const VALUE_RULES = frozen({
6
+ export const VALUE_RULES = deepFreeze({
6
7
  CALLABLE_INPUT_FIELD_UNKNOWN: {
7
8
  stage: 'value',
8
9
  positions: [''],
@@ -80,12 +81,7 @@ export const VALUE_RULES = frozen({
80
81
  },
81
82
  VS_REFERENCE_RESOLUTION: {
82
83
  stage: 'context',
83
- positions: [
84
- '{/path*}/$ref',
85
- '{/path*}/x-astrale-path/accepts/{index}',
86
- '',
87
- '/registry{/path*}',
88
- ],
84
+ positions: ['{/path*}/$ref', '{/path*}/x-astrale-path/accepts/{index}', ''],
89
85
  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
86
  },
91
87
  VS_REGEX_PORTABLE: {
@@ -103,6 +99,7 @@ export const VALUE_RULES = frozen({
103
99
  stage: 'schema',
104
100
  positions: ['{/path*}/x-astrale-value-kind'],
105
101
  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.',
102
+ conditions: ['#/$defs/value-location/anyOf/1/if'],
106
103
  },
107
104
  VS_VALUE_KIND_JSON: {
108
105
  stage: 'warning',
@@ -110,11 +107,3 @@ export const VALUE_RULES = frozen({
110
107
  statement: 'A Property of value kind json is filtered by equality only, so its author is warned.',
111
108
  },
112
109
  });
113
- function frozen(rules) {
114
- for (const rule of Object.values(rules)) {
115
- Object.freeze(Object.freeze(rule).positions);
116
- if (rule.limits !== undefined)
117
- Object.freeze(rule.limits);
118
- }
119
- return Object.freeze(rules);
120
- }
@@ -1,3 +1,4 @@
1
+ import type { Options } from '@astrale-os/kernel-dsl/value';
1
2
  import type { Diagnostic } from '../../validation/diagnostic.js';
2
3
  import type { ValueProgram } from '../compile/model.js';
3
- export declare function validateValueProgram(program: ValueProgram, input: unknown, roots?: readonly number[]): readonly Diagnostic[];
4
+ export declare function validateValueProgram(program: ValueProgram, input: unknown, roots?: readonly number[], options?: Options): readonly Diagnostic[];
@@ -1,8 +1,8 @@
1
1
  import { loadValueProgram } from '../compile/load.js';
2
2
  import { validateCanonicalJsonDomain } from '../profile/json.validate.js';
3
3
  import { evaluateInstance } from './evaluator.js';
4
- export function validateValueProgram(program, input, roots = [program.root]) {
5
- const canonical = validateCanonicalJsonDomain(input, '');
4
+ export function validateValueProgram(program, input, roots = [program.root], options) {
5
+ const canonical = validateCanonicalJsonDomain(input, '', options);
6
6
  if (canonical.length > 0)
7
7
  return canonical;
8
8
  return roots.flatMap((root) => {