@astrale-os/kernel-dsl 1.0.0-beta.14 → 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.
Files changed (74) hide show
  1. package/dist/v1/addressing/domain-dependency.d.ts +0 -6
  2. package/dist/v1/addressing/domain-dependency.js +1 -6
  3. package/dist/v1/builder/class/authoring.d.ts +43 -0
  4. package/dist/v1/builder/index.d.ts +1 -1
  5. package/dist/v1/builder/lowering/policy.js +1 -0
  6. package/dist/v1/builder/lowering/schema/context.d.ts +5 -0
  7. package/dist/v1/builder/lowering/schema/context.js +22 -0
  8. package/dist/v1/builder/policy/authoring.d.ts +84 -4
  9. package/dist/v1/builder/policy/authoring.js +41 -12
  10. package/dist/v1/builder/schema/projection/class.d.ts +116 -14
  11. package/dist/v1/bundle/codec.js +7 -7
  12. package/dist/v1/compiled/admission/accept.js +3 -3
  13. package/dist/v1/compiled/admission/closure.js +2 -2
  14. package/dist/v1/compiled/admission/semantics/core.js +3 -7
  15. package/dist/v1/compiled/admission/semantics/policy/parity.js +3 -10
  16. package/dist/v1/compiled/admission/semantics/policy/typing.js +2 -2
  17. package/dist/v1/language/.spec/schemas/draft-2020-12.schema.json +462 -0
  18. package/dist/v1/language/core/facts.d.ts +17 -4
  19. package/dist/v1/language/core/facts.js +26 -5
  20. package/dist/v1/language/limits.d.ts +64 -0
  21. package/dist/v1/language/limits.js +60 -0
  22. package/dist/v1/language/policy/analysis/budgets.d.ts +39 -18
  23. package/dist/v1/language/policy/analysis/budgets.js +30 -20
  24. package/dist/v1/language/policy/analysis/checks.js +3 -2
  25. package/dist/v1/language/policy/analysis/typing.d.ts +4 -2
  26. package/dist/v1/language/policy/analysis/typing.js +136 -14
  27. package/dist/v1/language/validation/rule.d.ts +9 -0
  28. package/dist/v1/language/validation/rule.js +9 -7
  29. package/dist/v1/language/value/ordering.d.ts +5 -0
  30. package/dist/v1/language/value/ordering.js +14 -0
  31. package/dist/v1/language/value/reference/closure.js +3 -4
  32. package/dist/v1/language/value/reference/registry.index.d.ts +2 -0
  33. package/dist/v1/language/value/reference/registry.index.js +31 -15
  34. package/dist/v1/language/value/regexp/ast.js +6 -7
  35. package/dist/v1/language/value/regexp/compile.d.ts +5 -11
  36. package/dist/v1/language/value/regexp/compile.js +14 -12
  37. package/dist/v1/language/value/regexp/syntax.js +3 -2
  38. package/dist/v1/schema/.spec/schemas/domain-schema.schema.json +30 -2
  39. package/dist/v1/schema/admission/accept.js +3 -14
  40. package/dist/v1/schema/admission/callable.d.ts +21 -0
  41. package/dist/v1/schema/admission/callable.js +96 -0
  42. package/dist/v1/schema/index.d.ts +7 -0
  43. package/dist/v1/schema/index.js +3 -0
  44. package/dist/v1/schema/normalization/domain.d.ts +2 -0
  45. package/dist/v1/schema/normalization/domain.js +1 -1
  46. package/dist/v1/schema/resolution/context.js +7 -7
  47. package/dist/v1/schema/resolution/domain.d.ts +5 -0
  48. package/dist/v1/schema/resolution/domain.js +1 -1
  49. package/dist/v1/schema/rule.d.ts +9 -0
  50. package/dist/v1/schema/rule.js +29 -4
  51. package/dist/v1/schema/validation/callable.d.ts +33 -3
  52. package/dist/v1/schema/validation/callable.js +49 -15
  53. package/dist/v1/schema/validation/class.d.ts +6 -2
  54. package/dist/v1/schema/validation/class.js +18 -22
  55. package/dist/v1/schema/validation/core.js +16 -10
  56. package/dist/v1/schema/validation/domain.js +3 -3
  57. package/dist/v1/schema/validation/domain.route.d.ts +6 -1
  58. package/dist/v1/schema/validation/domain.route.js +18 -0
  59. package/dist/v1/schema/validation/domain.structure.d.ts +28 -26
  60. package/dist/v1/schema/validation/domain.structure.js +22 -6
  61. package/dist/v1/schema/validation/policy/budgets.d.ts +33 -2
  62. package/dist/v1/schema/validation/policy/budgets.js +89 -16
  63. package/dist/v1/schema/validation/policy/context.d.ts +1 -2
  64. package/dist/v1/schema/validation/policy/context.js +2 -7
  65. package/dist/v1/schema/validation/policy/references.js +2 -1
  66. package/dist/v1/schema/validation/policy/uses.d.ts +29 -0
  67. package/dist/v1/schema/validation/policy/uses.js +28 -48
  68. package/dist/v1/schema/validation/value-api.d.ts +2 -0
  69. package/dist/v1/schema/validation/value-api.js +2 -0
  70. package/dist/v1/schema/validation/value.d.ts +3 -0
  71. package/dist/v1/schema/validation/value.js +3 -9
  72. package/dist/v1/schema/validation/value.json-schema.d.ts +3 -3
  73. package/dist/v1/schema/validation/value.json-schema.js +22 -4
  74. package/package.json +2 -1
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The limits of the V1 language, each defined once: every check of a source Schema, a Bundle or a
3
+ * compiled closure reads its bound here. Every limit is inclusive: a count equal to it is
4
+ * accepted, one more is refused. `schema.limits` publishes this object, and each rule of
5
+ * `schema.RULES` and `schema.values.RULES` that enforces some of them names them in `limits`.
6
+ */
7
+ export declare const limits: Readonly<{
8
+ /** Bounds of the dependency graph of one Domain (`DV_DEPENDENCY_LIMITS`). */
9
+ dependency: Readonly<{
10
+ /** Direct dependencies one Domain declares. */
11
+ direct: 32;
12
+ /** Domains of a dependency closure, the Domain itself included. */
13
+ closure: 256;
14
+ /** Dependencies on the longest chain from a Domain: a direct dependency is at depth 1. */
15
+ depth: 32;
16
+ }>;
17
+ /** Bounds of one I-Regexp in a value schema (`VS_REGEX_PORTABLE`), as RFC 9485 §8 allows. */
18
+ regexp: Readonly<{
19
+ /** Code points of one source. */
20
+ maxCodePoints: 4096;
21
+ /** Groups nested in one another in one source. */
22
+ maxNesting: 32;
23
+ /** The greatest bound of a counted repetition `{n}` or `{n,m}`. */
24
+ maxRepeat: 1000;
25
+ /** States of one compiled machine; the counted-run form never expands and is not bounded. */
26
+ maxStates: 10000;
27
+ }>;
28
+ /**
29
+ * Bounds of the Policy language. A named Policy is measured once normalized and once each
30
+ * composition is expanded into the Policies it names; a check tree, the checks of a callable or
31
+ * of a Node Class rule, once normalized (`PL_BUDGET`). The structure bounds the others
32
+ * (`STRUCTURE_MAXIMUM`).
33
+ */
34
+ policy: Readonly<{
35
+ /**
36
+ * Levels of a Policy pattern: an Edge or sameNode is one, and each allOf, anyOf, exists and
37
+ * composition adds one.
38
+ */
39
+ patternDepth: 4;
40
+ /**
41
+ * Branches of a Policy's normal form: an anyOf adds those of its operands, an allOf multiplies
42
+ * them.
43
+ */
44
+ normalizedBranches: 8;
45
+ /** Nodes one exists declares. */
46
+ variablesPerExists: 4;
47
+ /** Graph predicates, Edges and sameNode equalities, of one normalized branch. */
48
+ edgePredicates: 6;
49
+ /** The greatest bound of a repeated Edge. */
50
+ repetition: 3;
51
+ /** Domains a Policy names: its own, and the origin of every Class and object it references. */
52
+ referencedDomains: 4;
53
+ /** Levels of a check tree: a check or sameNode is one, and each allOf and anyOf adds one. */
54
+ checkDepth: 4;
55
+ /** Branches of a check tree: an anyOf adds those of its operands, an allOf multiplies them. */
56
+ checkBranches: 8;
57
+ /** Checks and sameNode equalities of a check tree. */
58
+ checkLeaves: 8;
59
+ }>;
60
+ }>;
61
+ /** A limit by its key in `limits`, `<group>.<name>`: `policy.patternDepth`. */
62
+ export type LimitKey = {
63
+ readonly [Group in keyof typeof limits & string]: `${Group}.${keyof (typeof limits)[Group] & string}`;
64
+ }[keyof typeof limits & string];
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The limits of the V1 language, each defined once: every check of a source Schema, a Bundle or a
3
+ * compiled closure reads its bound here. Every limit is inclusive: a count equal to it is
4
+ * accepted, one more is refused. `schema.limits` publishes this object, and each rule of
5
+ * `schema.RULES` and `schema.values.RULES` that enforces some of them names them in `limits`.
6
+ */
7
+ export const limits = Object.freeze({
8
+ /** Bounds of the dependency graph of one Domain (`DV_DEPENDENCY_LIMITS`). */
9
+ dependency: Object.freeze({
10
+ /** Direct dependencies one Domain declares. */
11
+ direct: 32,
12
+ /** Domains of a dependency closure, the Domain itself included. */
13
+ closure: 256,
14
+ /** Dependencies on the longest chain from a Domain: a direct dependency is at depth 1. */
15
+ depth: 32,
16
+ }),
17
+ /** Bounds of one I-Regexp in a value schema (`VS_REGEX_PORTABLE`), as RFC 9485 §8 allows. */
18
+ regexp: Object.freeze({
19
+ /** Code points of one source. */
20
+ maxCodePoints: 4096,
21
+ /** Groups nested in one another in one source. */
22
+ maxNesting: 32,
23
+ /** The greatest bound of a counted repetition `{n}` or `{n,m}`. */
24
+ maxRepeat: 1000,
25
+ /** States of one compiled machine; the counted-run form never expands and is not bounded. */
26
+ maxStates: 10_000,
27
+ }),
28
+ /**
29
+ * Bounds of the Policy language. A named Policy is measured once normalized and once each
30
+ * composition is expanded into the Policies it names; a check tree, the checks of a callable or
31
+ * of a Node Class rule, once normalized (`PL_BUDGET`). The structure bounds the others
32
+ * (`STRUCTURE_MAXIMUM`).
33
+ */
34
+ policy: Object.freeze({
35
+ /**
36
+ * Levels of a Policy pattern: an Edge or sameNode is one, and each allOf, anyOf, exists and
37
+ * composition adds one.
38
+ */
39
+ patternDepth: 4,
40
+ /**
41
+ * Branches of a Policy's normal form: an anyOf adds those of its operands, an allOf multiplies
42
+ * them.
43
+ */
44
+ normalizedBranches: 8,
45
+ /** Nodes one exists declares. */
46
+ variablesPerExists: 4,
47
+ /** Graph predicates, Edges and sameNode equalities, of one normalized branch. */
48
+ edgePredicates: 6,
49
+ /** The greatest bound of a repeated Edge. */
50
+ repetition: 3,
51
+ /** Domains a Policy names: its own, and the origin of every Class and object it references. */
52
+ referencedDomains: 4,
53
+ /** Levels of a check tree: a check or sameNode is one, and each allOf and anyOf adds one. */
54
+ checkDepth: 4,
55
+ /** Branches of a check tree: an anyOf adds those of its operands, an allOf multiplies them. */
56
+ checkBranches: 8,
57
+ /** Checks and sameNode equalities of a check tree. */
58
+ checkLeaves: 8,
59
+ }),
60
+ });
@@ -1,28 +1,49 @@
1
1
  import type { PolicyRef } from '../../../addressing/index.js';
2
2
  import type { PolicyCheckExpression, PolicyExpression } from '../model/policy.js';
3
- export declare const MAX_PATTERN_DEPTH = 4;
4
- export declare const MAX_BRANCHES = 8;
5
- export declare const MAX_VARIABLES = 4;
6
- export declare const MAX_EDGES = 6;
7
- export declare const MAX_REPEAT = 3;
8
- export declare const MAX_DOMAINS = 4;
9
- export declare const MAX_CHECK_DEPTH = 4;
10
- export declare const MAX_CHECK_LEAVES = 8;
3
+ import { limits } from '../../limits.js';
4
+ /** A key of `limits.policy`. */
5
+ export type PolicyLimit = keyof typeof limits.policy;
6
+ /**
7
+ * Where a count of Policy branches stops: one past its limit. Each branch count is exact up to its
8
+ * limit plus one and reads as that once it is over, so expansion stays bounded.
9
+ */
11
10
  export declare const BRANCH_OVERFLOW: number;
12
- export interface PatternMeasure {
13
- readonly depth: number;
14
- readonly edgeCounts: readonly number[];
15
- readonly domains: ReadonlySet<string>;
11
+ /**
12
+ * What `PL_BUDGET` compares with `limits.policy` for one named Policy once normalized and each of
13
+ * its compositions expanded, every count under the key of its limit. The branch count stops one
14
+ * past its limit: it is exact up to that, and once it is over, `edgePredicates` reads the branches
15
+ * it counted only. The other counts are exact.
16
+ */
17
+ export interface PolicyMeasure {
18
+ /** Levels of the expanded pattern. */
19
+ readonly patternDepth: number;
20
+ /** Branches of its normal form, at most `limits.policy.normalizedBranches + 1`. */
21
+ readonly normalizedBranches: number;
22
+ /** The most graph predicates, Edges and sameNode equalities, of one branch. */
23
+ readonly edgePredicates: number;
24
+ /** Domains the expanded pattern names, its own included. */
25
+ readonly referencedDomains: number;
26
+ }
27
+ /**
28
+ * What `PL_BUDGET` compares with `limits.policy` for one check tree once normalized, every count
29
+ * under the key of its limit. The branch count stops one past its limit; the other counts are
30
+ * exact.
31
+ */
32
+ export interface CheckMeasure {
33
+ /** Levels of the tree. */
34
+ readonly checkDepth: number;
35
+ /** Branches of its normal form, at most `limits.policy.checkBranches + 1`. */
36
+ readonly checkBranches: number;
37
+ /** Checks and sameNode equalities of the tree. */
38
+ readonly checkLeaves: number;
16
39
  }
40
+ /** The limits of `limits.policy` a measure exceeds, in the order the measure lists them. */
41
+ export declare function exceededLimits<Measure extends PolicyMeasure | CheckMeasure>(measure: Measure): readonly (keyof Measure & PolicyLimit)[];
17
42
  /**
18
43
  * Measure named Policies by their expanded branches, depth and referenced Domains. A Policy has no
19
44
  * measure when it is not normalized, when its expansion reaches itself, or when a composition names
20
45
  * a Policy `isLocal` refuses or one without a measure.
21
46
  */
22
- export declare function policyMeasurer(origin: string, normalized: ReadonlyMap<string, PolicyExpression>, isLocal: (ref: PolicyRef) => boolean): (name: string) => PatternMeasure | undefined;
23
- export interface CheckMeasure {
24
- readonly depth: number;
25
- readonly branches: number;
26
- readonly leaves: number;
27
- }
47
+ export declare function policyMeasurer(origin: string, normalized: ReadonlyMap<string, PolicyExpression>, isLocal: (ref: PolicyRef) => boolean): (name: string) => PolicyMeasure | undefined;
48
+ /** Measure one normalized check tree. */
28
49
  export declare function measureChecks(expression: PolicyCheckExpression): CheckMeasure;
@@ -1,13 +1,15 @@
1
+ import { limits } from '../../limits.js';
1
2
  import { policyObjectOrigin, policyVariableClass } from '../model/policy.js';
2
- export const MAX_PATTERN_DEPTH = 4;
3
- export const MAX_BRANCHES = 8;
4
- export const MAX_VARIABLES = 4;
5
- export const MAX_EDGES = 6;
6
- export const MAX_REPEAT = 3;
7
- export const MAX_DOMAINS = 4;
8
- export const MAX_CHECK_DEPTH = 4;
9
- export const MAX_CHECK_LEAVES = 8;
10
- export const BRANCH_OVERFLOW = MAX_BRANCHES + 1;
3
+ /**
4
+ * Where a count of Policy branches stops: one past its limit. Each branch count is exact up to its
5
+ * limit plus one and reads as that once it is over, so expansion stays bounded.
6
+ */
7
+ export const BRANCH_OVERFLOW = limits.policy.normalizedBranches + 1;
8
+ const CHECK_BRANCH_OVERFLOW = limits.policy.checkBranches + 1;
9
+ /** The limits of `limits.policy` a measure exceeds, in the order the measure lists them. */
10
+ export function exceededLimits(measure) {
11
+ return Object.keys(measure).filter((limit) => measure[limit] > limits.policy[limit]);
12
+ }
11
13
  /**
12
14
  * Measure named Policies by their expanded branches, depth and referenced Domains. A Policy has no
13
15
  * measure when it is not normalized, when its expansion reaches itself, or when a composition names
@@ -48,7 +50,17 @@ export function policyMeasurer(origin, normalized, isLocal) {
48
50
  return undefined;
49
51
  return combineMeasures('allOf' in declaration ? 'allOf' : 'anyOf', children);
50
52
  };
51
- return (name) => measurePolicy(name, new Set());
53
+ return (name) => {
54
+ const measure = measurePolicy(name, new Set());
55
+ return measure === undefined
56
+ ? undefined
57
+ : {
58
+ patternDepth: measure.depth,
59
+ normalizedBranches: measure.edgeCounts.length,
60
+ edgePredicates: Math.max(0, ...measure.edgeCounts),
61
+ referencedDomains: measure.domains.size,
62
+ };
63
+ };
52
64
  }
53
65
  function measurePattern(pattern, origin) {
54
66
  if ('sameNode' in pattern)
@@ -111,19 +123,17 @@ function combineMeasures(kind, children) {
111
123
  }
112
124
  return { depth, edgeCounts, domains };
113
125
  }
126
+ /** Measure one normalized check tree. */
114
127
  export function measureChecks(expression) {
115
128
  if ('check' in expression || 'sameNode' in expression)
116
- return { depth: 1, branches: 1, leaves: 1 };
129
+ return { checkDepth: 1, checkBranches: 1, checkLeaves: 1 };
117
130
  const children = ('allOf' in expression ? expression.allOf : expression.anyOf).map(measureChecks);
118
- const branches = 'allOf' in expression
119
- ? children.reduce((value, child) => saturatingProduct(value, child.branches), 1)
120
- : children.reduce((value, child) => Math.min(BRANCH_OVERFLOW, value + child.branches), 0);
131
+ const checkBranches = 'allOf' in expression
132
+ ? children.reduce((value, child) => Math.min(CHECK_BRANCH_OVERFLOW, value * child.checkBranches), 1)
133
+ : children.reduce((value, child) => Math.min(CHECK_BRANCH_OVERFLOW, value + child.checkBranches), 0);
121
134
  return {
122
- depth: 1 + Math.max(0, ...children.map((child) => child.depth)),
123
- branches,
124
- leaves: children.reduce((value, child) => value + child.leaves, 0),
135
+ checkDepth: 1 + Math.max(0, ...children.map((child) => child.checkDepth)),
136
+ checkBranches,
137
+ checkLeaves: children.reduce((value, child) => value + child.checkLeaves, 0),
125
138
  };
126
139
  }
127
- function saturatingProduct(left, right) {
128
- return Math.min(BRANCH_OVERFLOW, left * right);
129
- }
@@ -1,5 +1,6 @@
1
+ import { limits } from '../../limits.js';
1
2
  import { policyObjectKey } from '../model/policy.js';
2
- import { MAX_BRANCHES, measureChecks } from './budgets.js';
3
+ import { measureChecks } from './budgets.js';
3
4
  /** Names a callable check object. Every leaf over one object sees the same Node at runtime. */
4
5
  function checkObjectName(object) {
5
6
  if (object.kind === 'self')
@@ -36,7 +37,7 @@ export function admittedCheckClasses(typing) {
36
37
  * check budget is already refused, so it is not expanded.
37
38
  */
38
39
  export function neverAdmittedClasses(expression, leaves) {
39
- if (measureChecks(expression).branches > MAX_BRANCHES)
40
+ if (measureChecks(expression).checkBranches > limits.policy.checkBranches)
40
41
  return [];
41
42
  const accepted = new Map();
42
43
  for (const [leaf, classes] of leaves)
@@ -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';
@@ -21,10 +22,11 @@ export type PolicyUseContract = {
21
22
  export interface PolicyTypingScope {
22
23
  readonly normalized: ReadonlyMap<string, PolicyExpression>;
23
24
  readonly allNodeClasses: readonly ClassRef[];
25
+ /** The concrete Classes of `allNodeClasses`, in its order. */
24
26
  readonly concreteNodeClasses: readonly ClassRef[];
25
27
  isLocalPolicy(ref: PolicyRef): boolean;
26
- /** Whether `candidate` is the `required` Class or a same-kind descendant. */
27
- classExtends(candidate: ClassRef, required: ClassRef): boolean;
28
+ /** The Class a ref names in the closure, as satisfaction reads it. */
29
+ resolveClass(ref: ClassRef): ClassAncestry | undefined;
28
30
  edgeDeclaration(ref: ClassRef): EdgeContract | undefined;
29
31
  /** The contract of a Node a Policy pattern references, `undefined` when it has none. */
30
32
  referenceContract(ref: PolicyObjectRef): ObjectContract | undefined;
@@ -1,7 +1,8 @@
1
1
  import { Key } from '../../../addressing/index.js';
2
2
  import { definitionRefIndex } from '../../../addressing/ordering.js';
3
+ import { classSatisfies, isGraphRoot } from '../../class/inheritance.js';
4
+ import { limits } from '../../limits.js';
3
5
  import { branchesOf, branchProduct, capBranches, targetMode, termKey, UnionFind, } from './branches.js';
4
- import { MAX_REPEAT } from './budgets.js';
5
6
  import { admittedCheckClasses } from './checks.js';
6
7
  export class PolicyTyping {
7
8
  scope;
@@ -12,8 +13,12 @@ export class PolicyTyping {
12
13
  // independent of the reaching path up to the opaque term-namespace labels, which only ever
13
14
  // disambiguate variables within one branch and leave targetMode and branch typing unchanged.
14
15
  // Two occurrences of one Policy in a branch share labels only when a Policy reaches it by two
15
- // paths; for that to merge distinct variables, the Policy expands deeper than MAX_PATTERN_DEPTH.
16
+ // paths; for that to merge distinct variables, the Policy expands deeper than
17
+ // `limits.policy.patternDepth`.
16
18
  #expansions = new Map();
19
+ #nodeClasses;
20
+ #concreteClasses;
21
+ #satisfaction;
17
22
  constructor(scope) {
18
23
  this.scope = scope;
19
24
  }
@@ -68,7 +73,9 @@ export class PolicyTyping {
68
73
  return branches !== undefined && branches.every((branch) => this.branchIsTyped(branch, use));
69
74
  }
70
75
  #concreteEndpointClasses(accepts) {
71
- 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)));
72
79
  }
73
80
  #expandedBranches(name, visiting, namespace) {
74
81
  if (visiting.has(name))
@@ -157,7 +164,11 @@ export class PolicyTyping {
157
164
  if (contract.kind === 'exact')
158
165
  return this.#applyExact(candidates, contract.ref);
159
166
  if (contract.kind === 'default') {
160
- 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)));
161
172
  }
162
173
  return candidates;
163
174
  }
@@ -182,26 +193,30 @@ export class PolicyTyping {
182
193
  if (definitionRefIndex(candidate.ref) === definitionRefIndex(exact))
183
194
  result.push(candidate);
184
195
  }
185
- 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))) {
186
202
  result.push({ kind: 'exact', ref: exact });
187
203
  }
188
204
  }
189
205
  return uniqueCandidates(result);
190
206
  }
191
207
  #applyAnyNodeClass(candidates) {
192
- return uniqueCandidates(candidates.flatMap((candidate) => candidate.kind === 'exact'
193
- ? [candidate]
194
- : this.scope.allNodeClasses
195
- .filter((ref) => candidate.requirements.every((required) => this.scope.classExtends(ref, required)))
196
- .map((ref) => ({ kind: 'exact', ref }))));
208
+ return uniqueCandidates(candidates.flatMap((candidate) => candidate.kind === 'exact' ? [candidate] : this.#nodeCandidates(candidate)));
197
209
  }
198
210
  #applyRequirement(candidates, required) {
199
211
  const result = [];
200
212
  for (const candidate of candidates) {
201
213
  if (candidate.kind === 'exact') {
202
- if (this.scope.classExtends(candidate.ref, required))
214
+ if (this.#classExtends(candidate.ref, required))
203
215
  result.push(candidate);
204
216
  }
217
+ else if (candidate.kind === 'node') {
218
+ result.push(...this.#inhabited([...candidate.lists, [required]]));
219
+ }
205
220
  else {
206
221
  result.push({
207
222
  kind: 'open',
@@ -219,6 +234,11 @@ export class PolicyTyping {
219
234
  result.push(candidate);
220
235
  continue;
221
236
  }
237
+ if (candidate.kind === 'node') {
238
+ if (accepts.length > 0)
239
+ result.push(...this.#inhabited([...candidate.lists, accepts]));
240
+ continue;
241
+ }
222
242
  for (const accept of accepts) {
223
243
  result.push({
224
244
  kind: 'open',
@@ -229,8 +249,105 @@ export class PolicyTyping {
229
249
  return uniqueCandidates(result);
230
250
  }
231
251
  #classSatisfiesEndpoint(candidate, accepts) {
232
- 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
+ }
233
349
  }
350
+ return { nodeClasses, satisfying };
234
351
  }
235
352
  function targetContract(term, use) {
236
353
  if (use.kind === 'object')
@@ -269,7 +386,7 @@ function variantsForEdge(use, typing) {
269
386
  !Number.isInteger(bounds.max) ||
270
387
  bounds.min < 0 ||
271
388
  bounds.max < bounds.min ||
272
- bounds.max > MAX_REPEAT) {
389
+ bounds.max > limits.policy.repetition) {
273
390
  return [];
274
391
  }
275
392
  const sourceKey = termKey(edge.source, use.namespace);
@@ -328,7 +445,12 @@ function uniqueCandidates(values) {
328
445
  for (const value of values) {
329
446
  const key = value.kind === 'exact'
330
447
  ? `exact:${definitionRefIndex(value.ref)}`
331
- : `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('|')}`;
332
454
  result.set(key, value);
333
455
  }
334
456
  return [...result.values()];
@@ -1,3 +1,4 @@
1
+ import type { LimitKey } from '../limits.js';
1
2
  import type { DiagnosticCode } from './diagnostic.js';
2
3
  /**
3
4
  * What a rule reads to decide. `schema`: a value schema alone, with its place in the declaration
@@ -18,6 +19,14 @@ export interface ValueRule {
18
19
  readonly positions: readonly [string, ...string[]];
19
20
  /** The rule, in one plain sentence. */
20
21
  readonly statement: string;
22
+ /** The limits of `schema.limits` the rule enforces, by key, where it enforces some. */
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[]];
21
30
  }
22
31
  /**
23
32
  * Every value diagnostic code with the rule it reports. A code added to the language does not
@@ -80,18 +80,14 @@ 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: {
92
87
  stage: 'schema',
93
88
  positions: ['{/path*}/pattern', '{/path*}/fullMatch', '{/path*}/patternProperties/{pattern}'],
94
89
  statement: 'Every pattern, fullMatch and patternProperties name is an RFC 9485 I-Regexp within the regular-expression limits.',
90
+ limits: ['regexp.maxCodePoints', 'regexp.maxNesting', 'regexp.maxRepeat', 'regexp.maxStates'],
95
91
  },
96
92
  VS_RESOURCE_ID: {
97
93
  stage: 'schema',
@@ -102,6 +98,7 @@ export const VALUE_RULES = frozen({
102
98
  stage: 'schema',
103
99
  positions: ['{/path*}/x-astrale-value-kind'],
104
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'],
105
102
  },
106
103
  VS_VALUE_KIND_JSON: {
107
104
  stage: 'warning',
@@ -110,7 +107,12 @@ export const VALUE_RULES = frozen({
110
107
  },
111
108
  });
112
109
  function frozen(rules) {
113
- for (const rule of Object.values(rules))
110
+ for (const rule of Object.values(rules)) {
114
111
  Object.freeze(Object.freeze(rule).positions);
112
+ if (rule.limits !== undefined)
113
+ Object.freeze(rule.limits);
114
+ if (rule.conditions !== undefined)
115
+ Object.freeze(rule.conditions);
116
+ }
115
117
  return Object.freeze(rules);
116
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;