@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.
- package/dist/v1/addressing/domain-dependency.d.ts +0 -6
- package/dist/v1/addressing/domain-dependency.js +1 -6
- package/dist/v1/builder/class/authoring.d.ts +43 -0
- package/dist/v1/builder/index.d.ts +1 -1
- package/dist/v1/builder/lowering/policy.js +1 -0
- package/dist/v1/builder/lowering/schema/context.d.ts +5 -0
- package/dist/v1/builder/lowering/schema/context.js +22 -0
- package/dist/v1/builder/policy/authoring.d.ts +84 -4
- package/dist/v1/builder/policy/authoring.js +41 -12
- package/dist/v1/builder/schema/projection/class.d.ts +116 -14
- package/dist/v1/bundle/codec.js +7 -7
- package/dist/v1/compiled/admission/accept.js +3 -3
- package/dist/v1/compiled/admission/closure.js +2 -2
- package/dist/v1/compiled/admission/semantics/core.js +3 -7
- package/dist/v1/compiled/admission/semantics/policy/parity.js +3 -10
- package/dist/v1/compiled/admission/semantics/policy/typing.js +2 -2
- package/dist/v1/language/.spec/schemas/draft-2020-12.schema.json +462 -0
- package/dist/v1/language/core/facts.d.ts +17 -4
- package/dist/v1/language/core/facts.js +26 -5
- package/dist/v1/language/limits.d.ts +64 -0
- package/dist/v1/language/limits.js +60 -0
- package/dist/v1/language/policy/analysis/budgets.d.ts +39 -18
- package/dist/v1/language/policy/analysis/budgets.js +30 -20
- package/dist/v1/language/policy/analysis/checks.js +3 -2
- package/dist/v1/language/policy/analysis/typing.d.ts +4 -2
- package/dist/v1/language/policy/analysis/typing.js +136 -14
- package/dist/v1/language/validation/rule.d.ts +9 -0
- package/dist/v1/language/validation/rule.js +9 -7
- package/dist/v1/language/value/ordering.d.ts +5 -0
- package/dist/v1/language/value/ordering.js +14 -0
- package/dist/v1/language/value/reference/closure.js +3 -4
- package/dist/v1/language/value/reference/registry.index.d.ts +2 -0
- package/dist/v1/language/value/reference/registry.index.js +31 -15
- package/dist/v1/language/value/regexp/ast.js +6 -7
- package/dist/v1/language/value/regexp/compile.d.ts +5 -11
- package/dist/v1/language/value/regexp/compile.js +14 -12
- package/dist/v1/language/value/regexp/syntax.js +3 -2
- package/dist/v1/schema/.spec/schemas/domain-schema.schema.json +30 -2
- package/dist/v1/schema/admission/accept.js +3 -14
- package/dist/v1/schema/admission/callable.d.ts +21 -0
- package/dist/v1/schema/admission/callable.js +96 -0
- package/dist/v1/schema/index.d.ts +7 -0
- package/dist/v1/schema/index.js +3 -0
- package/dist/v1/schema/normalization/domain.d.ts +2 -0
- package/dist/v1/schema/normalization/domain.js +1 -1
- package/dist/v1/schema/resolution/context.js +7 -7
- package/dist/v1/schema/resolution/domain.d.ts +5 -0
- package/dist/v1/schema/resolution/domain.js +1 -1
- package/dist/v1/schema/rule.d.ts +9 -0
- package/dist/v1/schema/rule.js +29 -4
- package/dist/v1/schema/validation/callable.d.ts +33 -3
- package/dist/v1/schema/validation/callable.js +49 -15
- package/dist/v1/schema/validation/class.d.ts +6 -2
- package/dist/v1/schema/validation/class.js +18 -22
- package/dist/v1/schema/validation/core.js +16 -10
- package/dist/v1/schema/validation/domain.js +3 -3
- package/dist/v1/schema/validation/domain.route.d.ts +6 -1
- package/dist/v1/schema/validation/domain.route.js +18 -0
- package/dist/v1/schema/validation/domain.structure.d.ts +28 -26
- package/dist/v1/schema/validation/domain.structure.js +22 -6
- package/dist/v1/schema/validation/policy/budgets.d.ts +33 -2
- package/dist/v1/schema/validation/policy/budgets.js +89 -16
- package/dist/v1/schema/validation/policy/context.d.ts +1 -2
- package/dist/v1/schema/validation/policy/context.js +2 -7
- package/dist/v1/schema/validation/policy/references.js +2 -1
- package/dist/v1/schema/validation/policy/uses.d.ts +29 -0
- package/dist/v1/schema/validation/policy/uses.js +28 -48
- package/dist/v1/schema/validation/value-api.d.ts +2 -0
- package/dist/v1/schema/validation/value-api.js +2 -0
- package/dist/v1/schema/validation/value.d.ts +3 -0
- package/dist/v1/schema/validation/value.js +3 -9
- package/dist/v1/schema/validation/value.json-schema.d.ts +3 -3
- package/dist/v1/schema/validation/value.json-schema.js +22 -4
- package/package.json +2 -1
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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) =>
|
|
23
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
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) =>
|
|
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 {
|
|
129
|
+
return { checkDepth: 1, checkBranches: 1, checkLeaves: 1 };
|
|
117
130
|
const children = ('allOf' in expression ? expression.allOf : expression.anyOf).map(measureChecks);
|
|
118
|
-
const
|
|
119
|
-
? children.reduce((value, child) =>
|
|
120
|
-
: children.reduce((value, child) => Math.min(
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 {
|
|
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).
|
|
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
|
-
/**
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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 >
|
|
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
|
-
:
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
39
|
+
// The graph NodePath resource is no part of the schema: what it holds is reported at the
|
|
40
|
+
// $ref that names it.
|
|
41
|
+
const registered = { ...target, diagnosticPointer: refPointer };
|
|
43
42
|
resources.set(target.key, registered);
|
|
44
43
|
queue.push(registered);
|
|
45
44
|
});
|
|
@@ -5,6 +5,8 @@ export interface RegistryIndex {
|
|
|
5
5
|
readonly registry: ValueSchemaRegistry;
|
|
6
6
|
readonly domains: ReadonlyMap<string, ValueSchemaDomainRegistry>;
|
|
7
7
|
readonly dependenciesByOwner: ReadonlyMap<string, ReadonlyMap<string, string>>;
|
|
8
|
+
/** The `definitionRefIndex` of each Node Definition its own Domain lists. */
|
|
9
|
+
readonly nodeDefinitions: ReadonlySet<string>;
|
|
8
10
|
}
|
|
9
11
|
export declare function indexRegistry(registry: ValueSchemaRegistry, pointer: string): {
|
|
10
12
|
readonly index: RegistryIndex;
|