@astrale-os/kernel-dsl 0.2.0-beta.42 → 0.2.0-beta.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/v1/compiled/admission/accept.js +10 -27
- package/dist/v1/compiled/admission/semantics.js +19 -4
- package/dist/v1/compiled/loader/load.d.ts +2 -11
- package/dist/v1/compiled/loader/load.js +1 -3
- package/dist/v1/language/policy/analysis/checks.d.ts +28 -0
- package/dist/v1/language/policy/analysis/checks.js +80 -0
- package/dist/v1/language/value/evaluate/evaluator.js +29 -11
- package/dist/v1/language/value/profile/rules.js +25 -0
- package/dist/v1/language/value/regexp/compile.d.ts +6 -0
- package/dist/v1/language/value/regexp/compile.js +18 -0
- package/dist/v1/schema/compatibility/api.d.ts +2 -1
- package/dist/v1/schema/compatibility/api.js +1 -0
- package/dist/v1/schema/compatibility/evidence.d.ts +6 -2
- package/dist/v1/schema/compatibility/evidence.js +5 -3
- package/dist/v1/schema/compatibility/index.d.ts +1 -3
- package/dist/v1/schema/compatibility/index.js +0 -1
- package/dist/v1/schema/compatibility/meaning/index.js +1 -1
- package/dist/v1/schema/compatibility/model.d.ts +33 -3
- package/dist/v1/schema/compatibility/scope.d.ts +8 -0
- package/dist/v1/schema/compatibility/scope.js +50 -21
- package/dist/v1/schema/compatibility/structure/index.js +6 -1
- package/dist/v1/schema/compatibility/structure/requirements.js +5 -3
- package/dist/v1/schema/compatibility/subjects.d.ts +5 -0
- package/dist/v1/schema/compatibility/subjects.js +39 -0
- package/dist/v1/schema/compatibility/versioning/index.d.ts +8 -0
- package/dist/v1/schema/compatibility/versioning/index.js +50 -0
- package/dist/v1/schema/compatibility/versioning/members.d.ts +12 -0
- package/dist/v1/schema/compatibility/versioning/members.js +47 -0
- package/dist/v1/schema/compatibility/versioning/surface.d.ts +11 -0
- package/dist/v1/schema/compatibility/versioning/surface.js +61 -0
- package/dist/v1/schema/diagnostic.d.ts +1 -1
- package/dist/v1/schema/index.d.ts +2 -4
- package/dist/v1/schema/index.js +1 -2
- package/dist/v1/schema/validation/policy/typing.d.ts +2 -0
- package/dist/v1/schema/validation/policy/typing.js +5 -1
- package/dist/v1/schema/validation/policy/uses.js +20 -8
- package/package.json +5 -1
- package/dist/v1/schema/compatibility/legacy/index.d.ts +0 -58
- package/dist/v1/schema/compatibility/legacy/index.js +0 -145
|
@@ -21,11 +21,10 @@ export function acceptCompiledSchema(input) {
|
|
|
21
21
|
exact(root, ['format', 'version', 'root', 'closure'], 'CompiledSchema', '');
|
|
22
22
|
if (root.format !== 'astrale.dsl.compiled-schema')
|
|
23
23
|
invalid('format', undefined, '/format');
|
|
24
|
-
if (root.version !==
|
|
24
|
+
if (root.version !== 2)
|
|
25
25
|
invalid('version', undefined, '/version');
|
|
26
|
-
const version = root.version;
|
|
27
26
|
const rootPin = dependency(root.root, 'root', '/root');
|
|
28
|
-
const closure = array(root.closure, 'closure', '/closure').map((value, index) => entry(value, index, `/closure/${index}
|
|
27
|
+
const closure = array(root.closure, 'closure', '/closure').map((value, index) => entry(value, index, `/closure/${index}`));
|
|
29
28
|
if (closure.length > dependencyLimits.closure) {
|
|
30
29
|
invalid('dependency closure limit', 'COMPILED_DEPENDENCY_LIMITS', '/closure');
|
|
31
30
|
}
|
|
@@ -75,17 +74,9 @@ export function acceptCompiledSchema(input) {
|
|
|
75
74
|
validateCompiledSemantics(candidate);
|
|
76
75
|
return deepFreeze(candidate);
|
|
77
76
|
}
|
|
78
|
-
function entry(input, entryIndex, path
|
|
77
|
+
function entry(input, entryIndex, path) {
|
|
79
78
|
const value = object(input, `closure[${entryIndex}]`, path);
|
|
80
|
-
exact(value, [
|
|
81
|
-
'origin',
|
|
82
|
-
'revision',
|
|
83
|
-
'dependencies',
|
|
84
|
-
'definitions',
|
|
85
|
-
'facts',
|
|
86
|
-
'values',
|
|
87
|
-
...(version === 2 ? ['patterns'] : []),
|
|
88
|
-
], 'entry', path);
|
|
79
|
+
exact(value, ['origin', 'revision', 'dependencies', 'definitions', 'facts', 'values', 'patterns'], 'entry', path);
|
|
89
80
|
const origin = originValue(value.origin, 'entry origin', `${path}/origin`);
|
|
90
81
|
const revision = revisionValue(value.revision, 'entry revision', `${path}/revision`);
|
|
91
82
|
const dependenciesPath = `${path}/dependencies`;
|
|
@@ -98,10 +89,8 @@ function entry(input, entryIndex, path, version) {
|
|
|
98
89
|
}
|
|
99
90
|
const valuesPath = `${path}/values`;
|
|
100
91
|
const patternsPath = `${path}/patterns`;
|
|
101
|
-
const patterns =
|
|
102
|
-
|
|
103
|
-
: [];
|
|
104
|
-
const values = array(value.values, 'values', valuesPath).map((item, index) => program(item, index, `${valuesPath}/${index}`, patterns, version));
|
|
92
|
+
const patterns = array(value.patterns, 'entry patterns', patternsPath).map((item, index) => pattern(item, index, `${patternsPath}/${index}`));
|
|
93
|
+
const values = array(value.values, 'values', valuesPath).map((item, index) => program(item, index, `${valuesPath}/${index}`, patterns));
|
|
105
94
|
const bound = values.map((value) => bindValueProgram(value, patterns));
|
|
106
95
|
const definitionsPath = `${path}/definitions`;
|
|
107
96
|
const definitions = object(value.definitions, 'definitions', definitionsPath);
|
|
@@ -803,20 +792,14 @@ function walk(input, path, visit) {
|
|
|
803
792
|
for (const [key, value] of Object.entries(input))
|
|
804
793
|
walk(value, pointer(path, key), visit);
|
|
805
794
|
}
|
|
806
|
-
function program(input, programIndex, path, table
|
|
795
|
+
function program(input, programIndex, path, table) {
|
|
807
796
|
const value = object(input, `values[${programIndex}]`, path);
|
|
808
797
|
exact(value, ['root', 'nodes', 'patterns'], 'ValueProgram', path);
|
|
809
798
|
const nodesPath = `${path}/nodes`;
|
|
810
799
|
const nodes = array(value.nodes, 'ValueProgram nodes', nodesPath).map((node, index) => valueNode(node, index, `${nodesPath}/${index}`));
|
|
811
800
|
const root = index(value.root, nodes.length, 'ValueProgram root', `${path}/root`);
|
|
812
801
|
const patternsPath = `${path}/patterns`;
|
|
813
|
-
const patterns = array(value.patterns, 'ValueProgram patterns', patternsPath).map((item, position) => {
|
|
814
|
-
const patternPath = `${patternsPath}/${position}`;
|
|
815
|
-
if (version === 2)
|
|
816
|
-
return index(item, table.length, 'ValueProgram pattern', patternPath);
|
|
817
|
-
// Legacy programs can carry different machines with the same source label. Preserve each.
|
|
818
|
-
return table.push(pattern(item, position, patternPath, false)) - 1;
|
|
819
|
-
});
|
|
802
|
+
const patterns = array(value.patterns, 'ValueProgram patterns', patternsPath).map((item, position) => index(item, table.length, 'ValueProgram pattern', `${patternsPath}/${position}`));
|
|
820
803
|
const sources = patterns.map((index) => table[index].source);
|
|
821
804
|
unique(sources, 'patterns', patternsPath);
|
|
822
805
|
const available = new Set(sources);
|
|
@@ -855,9 +838,9 @@ function valueNode(input, nodeIndex, path) {
|
|
|
855
838
|
: { reference: integer(value.reference, 'reference', `${path}/reference`) }),
|
|
856
839
|
};
|
|
857
840
|
}
|
|
858
|
-
function pattern(input, patternIndex, path
|
|
841
|
+
function pattern(input, patternIndex, path) {
|
|
859
842
|
const value = object(input, `patterns[${patternIndex}]`, path);
|
|
860
|
-
if (
|
|
843
|
+
if (Object.hasOwn(value, 'runs'))
|
|
861
844
|
return runPattern(value, path);
|
|
862
845
|
exact(value, ['source', 'start', 'states'], 'Pattern', path);
|
|
863
846
|
if (typeof value.source !== 'string')
|
|
@@ -6,6 +6,7 @@ import { isMethodExecutable } from '../../language/callable/executable.js';
|
|
|
6
6
|
import { policyObjectKey, policyObjectOrigin, policyVariableClass, projectedMemberClass, } from '../../language/index.js';
|
|
7
7
|
import { branchesOf, branchProduct, capBranches, isConnected, targetMode, termKey, } from '../../language/policy/analysis/branches.js';
|
|
8
8
|
import { BRANCH_OVERFLOW, MAX_BRANCHES, MAX_CHECK_DEPTH, MAX_CHECK_LEAVES, MAX_DOMAINS, MAX_EDGES, MAX_PATTERN_DEPTH, MAX_REPEAT, MAX_VARIABLES, measureChecks, } from '../../language/policy/analysis/budgets.js';
|
|
9
|
+
import { admittedCheckClasses, neverAdmittedClasses, } from '../../language/policy/analysis/checks.js';
|
|
9
10
|
import { normalizePolicyExpression } from '../../language/policy/normalization/normalize.js';
|
|
10
11
|
import { bindValueProgram } from '../../language/value/compile/load.js';
|
|
11
12
|
import { validateValueProgram } from '../../language/value/evaluate/program.js';
|
|
@@ -538,6 +539,7 @@ function validateCallablePolicyTyping(context, typing, entry, expression, input,
|
|
|
538
539
|
measure.leaves > MAX_CHECK_LEAVES) {
|
|
539
540
|
fail(path, 'Callable Policy checks exceed the V1 depth, branch, or leaf budget.');
|
|
540
541
|
}
|
|
542
|
+
const leaves = new Map();
|
|
541
543
|
const visit = (value, pointer) => {
|
|
542
544
|
if ('sameNode' in value)
|
|
543
545
|
return;
|
|
@@ -547,12 +549,23 @@ function validateCallablePolicyTyping(context, typing, entry, expression, input,
|
|
|
547
549
|
return;
|
|
548
550
|
}
|
|
549
551
|
const contracts = callableObjectContracts(context, entry, input, receiver, value.object);
|
|
550
|
-
if (contracts
|
|
551
|
-
|
|
552
|
+
if (contracts === undefined)
|
|
553
|
+
return;
|
|
554
|
+
const typings = new Map(contracts.map((contract) => [
|
|
555
|
+
contract.kind === 'exact' ? Key.of(contract.ref) : 'default',
|
|
556
|
+
typing.policyBranchTyping(value.check.name, contract),
|
|
557
|
+
]));
|
|
558
|
+
const admitted = admittedCheckClasses(typings);
|
|
559
|
+
if (admitted === undefined) {
|
|
552
560
|
fail(pointer, 'Named Policy is incompatible with the callable object contract.');
|
|
553
561
|
}
|
|
562
|
+
leaves.set(value, { accepted: [...typings.keys()], admitted });
|
|
554
563
|
};
|
|
555
564
|
visit(expression, path);
|
|
565
|
+
const [never] = neverAdmittedClasses(expression, leaves);
|
|
566
|
+
if (never !== undefined) {
|
|
567
|
+
fail(path, `Callable Policy checks can never admit ${never[0]} of Class ${never[1]}.`);
|
|
568
|
+
}
|
|
556
569
|
}
|
|
557
570
|
function callableObjectContracts(context, entry, input, receiver, object) {
|
|
558
571
|
if (object.kind === 'self') {
|
|
@@ -629,9 +642,11 @@ class CompiledPolicyTyping {
|
|
|
629
642
|
return 'allOf' in declaration ? branchProduct(children) : capBranches(children.flat());
|
|
630
643
|
}
|
|
631
644
|
policyAccepts(name, object) {
|
|
645
|
+
return this.policyBranchTyping(name, object)?.every(Boolean) ?? false;
|
|
646
|
+
}
|
|
647
|
+
policyBranchTyping(name, object) {
|
|
632
648
|
const branches = this.expandedBranches(name, new Set(), `$use:${name}`);
|
|
633
|
-
return (
|
|
634
|
-
branches.every((branch) => this.branchIsTyped(branch, { kind: 'object', object })));
|
|
649
|
+
return branches?.map((branch) => this.branchIsTyped(branch, { kind: 'object', object }));
|
|
635
650
|
}
|
|
636
651
|
policyAcceptsEdge(name, source, target) {
|
|
637
652
|
const branches = this.expandedBranches(name, new Set(), `$edge-use:${name}`);
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { CompiledSchema, CompiledSchemaEntry, DomainOfCompiled } from '../model/schema.js';
|
|
3
|
-
type LegacyEntry = Omit<CompiledSchemaEntry, 'values' | 'patterns'> & {
|
|
4
|
-
readonly values: readonly ValueProgram[];
|
|
5
|
-
};
|
|
6
|
-
type LegacyCompiledSchema = Omit<CompiledSchema, 'version' | 'closure'> & {
|
|
7
|
-
readonly version: 1;
|
|
8
|
-
readonly closure: readonly LegacyEntry[];
|
|
9
|
-
};
|
|
1
|
+
import type { CompiledSchema, DomainOfCompiled } from '../model/schema.js';
|
|
10
2
|
/** Load admitted compiled data through the one resolved Domain constructor. */
|
|
11
3
|
export declare function load<const Input extends CompiledSchema>(input: Input): DomainOfCompiled<Input>;
|
|
12
4
|
export declare function load(input: unknown): import('../../domain/model/domain.js').Domain;
|
|
@@ -14,5 +6,4 @@ export declare function load(input: unknown): import('../../domain/model/domain.
|
|
|
14
6
|
* Realize compiled material whose exact embedded bytes were already proven by generated build code.
|
|
15
7
|
* External bytes and caller-provided values must continue through {@link load}.
|
|
16
8
|
*/
|
|
17
|
-
export declare function realize(compiled: CompiledSchema
|
|
18
|
-
export {};
|
|
9
|
+
export declare function realize(compiled: CompiledSchema): import('../../domain/model/domain.js').Domain;
|
|
@@ -69,9 +69,7 @@ function loadEntry(entry) {
|
|
|
69
69
|
...entry.definitions.views,
|
|
70
70
|
...entry.definitions.core,
|
|
71
71
|
]),
|
|
72
|
-
validation: programValidation(
|
|
73
|
-
? entry.values.map((value) => bindValueProgram(value, entry.patterns))
|
|
74
|
-
: entry.values, propertyPrograms, propertyOrdering, callablePrograms),
|
|
72
|
+
validation: programValidation(entry.values.map((value) => bindValueProgram(value, entry.patterns)), propertyPrograms, propertyOrdering, callablePrograms),
|
|
75
73
|
};
|
|
76
74
|
}
|
|
77
75
|
function loadMethod(entry, value) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PolicyCheckExpression } from '../model/policy.js';
|
|
2
|
+
export type PolicyCheckLeaf = Extract<PolicyCheckExpression, {
|
|
3
|
+
readonly check: unknown;
|
|
4
|
+
}>;
|
|
5
|
+
/** How one check leaf types against the Classes its object accepts. */
|
|
6
|
+
export interface CheckLeafClasses {
|
|
7
|
+
/** Every Class the leaf's object accepts. */
|
|
8
|
+
readonly accepted: readonly string[];
|
|
9
|
+
/** The accepted Classes at least one normalized branch of the leaf's Policy is typed for. */
|
|
10
|
+
readonly admitted: ReadonlySet<string>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The Classes a leaf admits, from the typing of each normalized branch of its Policy for each
|
|
14
|
+
* accepted Class of its object. `undefined` when the Policy does not expand, or when one branch is
|
|
15
|
+
* typed for no accepted Class: that branch can never match this object, so it is refused rather
|
|
16
|
+
* than silently erased.
|
|
17
|
+
*/
|
|
18
|
+
export declare function admittedCheckClasses(typing: ReadonlyMap<string, readonly boolean[] | undefined>): ReadonlySet<string> | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The accepted Classes of each object that the whole check tree can never admit.
|
|
21
|
+
*
|
|
22
|
+
* The tree is read in disjunctive normal form. A conjunction admits Class C of object O when every
|
|
23
|
+
* leaf over O in it admits C and every other object it checks keeps a Class that all of its leaves
|
|
24
|
+
* over that object admit; an object the conjunction does not check keeps every accepted Class.
|
|
25
|
+
* `sameNode` compares identities, not Classes, so it never removes a Class here. A tree over the
|
|
26
|
+
* check budget is already refused, so it is not expanded.
|
|
27
|
+
*/
|
|
28
|
+
export declare function neverAdmittedClasses(expression: PolicyCheckExpression, leaves: ReadonlyMap<PolicyCheckLeaf, CheckLeafClasses>): readonly (readonly [object: string, className: string])[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { policyObjectKey } from '../model/policy.js';
|
|
2
|
+
import { MAX_BRANCHES, measureChecks } from './budgets.js';
|
|
3
|
+
/** Names a callable check object. Every leaf over one object sees the same Node at runtime. */
|
|
4
|
+
function checkObjectName(object) {
|
|
5
|
+
if (object.kind === 'self')
|
|
6
|
+
return 'self';
|
|
7
|
+
if (object.kind === 'input')
|
|
8
|
+
return `input.${object.field}`;
|
|
9
|
+
return policyObjectKey(object.ref);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The Classes a leaf admits, from the typing of each normalized branch of its Policy for each
|
|
13
|
+
* accepted Class of its object. `undefined` when the Policy does not expand, or when one branch is
|
|
14
|
+
* typed for no accepted Class: that branch can never match this object, so it is refused rather
|
|
15
|
+
* than silently erased.
|
|
16
|
+
*/
|
|
17
|
+
export function admittedCheckClasses(typing) {
|
|
18
|
+
const rows = [...typing];
|
|
19
|
+
if (!rows.every((row) => row[1] !== undefined)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const branches = Math.max(0, ...rows.map(([, row]) => row.length));
|
|
23
|
+
for (let index = 0; index < branches; index++) {
|
|
24
|
+
if (!rows.some(([, row]) => row[index] === true))
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return new Set(rows.filter(([, row]) => row.some(Boolean)).map(([name]) => name));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The accepted Classes of each object that the whole check tree can never admit.
|
|
31
|
+
*
|
|
32
|
+
* The tree is read in disjunctive normal form. A conjunction admits Class C of object O when every
|
|
33
|
+
* leaf over O in it admits C and every other object it checks keeps a Class that all of its leaves
|
|
34
|
+
* over that object admit; an object the conjunction does not check keeps every accepted Class.
|
|
35
|
+
* `sameNode` compares identities, not Classes, so it never removes a Class here. A tree over the
|
|
36
|
+
* check budget is already refused, so it is not expanded.
|
|
37
|
+
*/
|
|
38
|
+
export function neverAdmittedClasses(expression, leaves) {
|
|
39
|
+
if (measureChecks(expression).branches > MAX_BRANCHES)
|
|
40
|
+
return [];
|
|
41
|
+
const accepted = new Map();
|
|
42
|
+
for (const [leaf, classes] of leaves)
|
|
43
|
+
accepted.set(checkObjectName(leaf.object), classes.accepted);
|
|
44
|
+
const admitted = new Map();
|
|
45
|
+
for (const conjunction of disjunctiveForm(expression)) {
|
|
46
|
+
const constrained = new Map();
|
|
47
|
+
for (const leaf of conjunction) {
|
|
48
|
+
if (!('check' in leaf))
|
|
49
|
+
continue;
|
|
50
|
+
const classes = leaves.get(leaf);
|
|
51
|
+
if (classes === undefined)
|
|
52
|
+
continue;
|
|
53
|
+
const object = checkObjectName(leaf.object);
|
|
54
|
+
const previous = constrained.get(object);
|
|
55
|
+
constrained.set(object, previous === undefined
|
|
56
|
+
? classes.admitted
|
|
57
|
+
: new Set([...previous].filter((name) => classes.admitted.has(name))));
|
|
58
|
+
}
|
|
59
|
+
if ([...constrained.values()].some((classes) => classes.size === 0))
|
|
60
|
+
continue;
|
|
61
|
+
for (const [object, all] of accepted) {
|
|
62
|
+
const reached = admitted.get(object) ?? new Set();
|
|
63
|
+
for (const name of constrained.get(object) ?? all)
|
|
64
|
+
reached.add(name);
|
|
65
|
+
admitted.set(object, reached);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...accepted].flatMap(([object, all]) => all
|
|
69
|
+
.filter((name) => admitted.get(object)?.has(name) !== true)
|
|
70
|
+
.map((name) => [object, name]));
|
|
71
|
+
}
|
|
72
|
+
function disjunctiveForm(expression) {
|
|
73
|
+
if ('check' in expression || 'sameNode' in expression)
|
|
74
|
+
return [[expression]];
|
|
75
|
+
if ('anyOf' in expression)
|
|
76
|
+
return expression.anyOf.flatMap(disjunctiveForm);
|
|
77
|
+
return expression.allOf
|
|
78
|
+
.map(disjunctiveForm)
|
|
79
|
+
.reduce((left, right) => left.flatMap((prefix) => right.map((suffix) => [...prefix, ...suffix])), [[]]);
|
|
80
|
+
}
|
|
@@ -13,7 +13,7 @@ const MAX_CACHED_PATTERN_RESULTS = 256;
|
|
|
13
13
|
const MAX_CACHED_PATTERN_INPUT_LENGTH = 128;
|
|
14
14
|
class InstanceEvaluator {
|
|
15
15
|
compiled;
|
|
16
|
-
|
|
16
|
+
resultCache = new Map();
|
|
17
17
|
searchResults = new Map();
|
|
18
18
|
fullMatchResults = new Map();
|
|
19
19
|
patternResultCount = 0;
|
|
@@ -21,23 +21,41 @@ class InstanceEvaluator {
|
|
|
21
21
|
this.compiled = compiled;
|
|
22
22
|
}
|
|
23
23
|
evaluate(location, input, instancePointer) {
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const key = `${location.key}\u0000${instancePointer}`;
|
|
25
|
+
// propertyNames evaluates the name at the property's value pointer. Cache by the actual
|
|
26
|
+
// input too, retaining one result per repeated sub-problem within this call. A null entry
|
|
27
|
+
// marks an active evaluation; completed results and their annotations are only read.
|
|
28
|
+
let byLocation = this.resultCache.get(input);
|
|
29
|
+
const memoized = byLocation?.get(key);
|
|
30
|
+
if (memoized === null) {
|
|
26
31
|
throw new TypeError('Admitted value schema entered a non-productive evaluation cycle.');
|
|
27
32
|
}
|
|
28
|
-
|
|
33
|
+
if (memoized !== undefined)
|
|
34
|
+
return memoized;
|
|
35
|
+
if (byLocation === undefined) {
|
|
36
|
+
byLocation = new Map();
|
|
37
|
+
this.resultCache.set(input, byLocation);
|
|
38
|
+
}
|
|
39
|
+
byLocation.set(key, null);
|
|
29
40
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
let result;
|
|
42
|
+
if (location.schema === true) {
|
|
43
|
+
result = evaluation();
|
|
44
|
+
}
|
|
45
|
+
else if (location.schema === false) {
|
|
46
|
+
result = evaluation([
|
|
34
47
|
invalid(instancePointer, 'false', 'The false schema rejects every value.'),
|
|
35
48
|
]);
|
|
36
49
|
}
|
|
37
|
-
|
|
50
|
+
else {
|
|
51
|
+
result = this.evaluateObjectSchema(location, location.schema, input, instancePointer);
|
|
52
|
+
}
|
|
53
|
+
byLocation.set(key, result);
|
|
54
|
+
return result;
|
|
38
55
|
}
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
catch (error) {
|
|
57
|
+
byLocation.delete(key);
|
|
58
|
+
throw error;
|
|
41
59
|
}
|
|
42
60
|
}
|
|
43
61
|
evaluateObjectSchema(location, schema, input, instancePointer) {
|
|
@@ -3,6 +3,7 @@ import { diagnostic } from '../../validation/diagnostic.js';
|
|
|
3
3
|
import { blob, BLOB_VALUE_KIND } from '../model/blob.js';
|
|
4
4
|
import { isValueObject as isJsonObject } from '../model/object.js';
|
|
5
5
|
import { VALUE_SCHEMA_DIALECT } from '../model/schema.js';
|
|
6
|
+
import { compilePattern, PatternComplexityError } from '../regexp/compile.js';
|
|
6
7
|
import { isIRegexp } from '../regexp/syntax.js';
|
|
7
8
|
import { walkSchema } from '../syntax/walk.js';
|
|
8
9
|
import { FORBIDDEN_DYNAMIC_KEYWORDS, STANDARD_SCHEMA_KEYWORDS } from './vocabulary.js';
|
|
@@ -98,14 +99,38 @@ function validateRegexes(schema, pointer, diagnostics) {
|
|
|
98
99
|
if (typeof schema.fullMatch === 'string' && !isIRegexp(schema.fullMatch)) {
|
|
99
100
|
diagnostics.push(diagnostic('VS_REGEX_PORTABLE', `${pointer}/fullMatch`, 'fullMatch must satisfy the complete RFC 9485 I-Regexp syntax.'));
|
|
100
101
|
}
|
|
102
|
+
else if (typeof schema.fullMatch === 'string') {
|
|
103
|
+
checkPatternComplexity(schema.fullMatch, `${pointer}/fullMatch`, diagnostics);
|
|
104
|
+
}
|
|
101
105
|
if (typeof schema.pattern === 'string' && !isIRegexp(schema.pattern)) {
|
|
102
106
|
diagnostics.push(diagnostic('VS_REGEX_PORTABLE', `${pointer}/pattern`, 'pattern must satisfy the complete RFC 9485 I-Regexp syntax.'));
|
|
103
107
|
}
|
|
108
|
+
else if (typeof schema.pattern === 'string') {
|
|
109
|
+
checkPatternComplexity(schema.pattern, `${pointer}/pattern`, diagnostics);
|
|
110
|
+
}
|
|
104
111
|
if (isJsonObject(schema.patternProperties)) {
|
|
105
112
|
for (const pattern of Object.keys(schema.patternProperties)) {
|
|
106
113
|
if (!isIRegexp(pattern)) {
|
|
107
114
|
diagnostics.push(diagnostic('VS_REGEX_PORTABLE', `${pointer}/patternProperties/${escapeToken(pattern)}`, 'patternProperties keys must satisfy RFC 9485 I-Regexp syntax.'));
|
|
108
115
|
}
|
|
116
|
+
else {
|
|
117
|
+
checkPatternComplexity(pattern, `${pointer}/patternProperties/${escapeToken(pattern)}`, diagnostics);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Reject a syntactically valid I-Regexp that compiles to an unbounded NFA (nested counted
|
|
124
|
+
* repetitions, P1-I-01) at admission, rather than letting it unroll to ~120k states and cost
|
|
125
|
+
* seconds per validated value. Other compile failures are already reported by the syntax checks.
|
|
126
|
+
*/
|
|
127
|
+
function checkPatternComplexity(source, pointer, diagnostics) {
|
|
128
|
+
try {
|
|
129
|
+
compilePattern(source);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof PatternComplexityError) {
|
|
133
|
+
diagnostics.push(diagnostic('VS_REGEX_PORTABLE', pointer, `${error.message} Simplify nested counted repetitions to stay within the I-Regexp complexity bound.`));
|
|
109
134
|
}
|
|
110
135
|
}
|
|
111
136
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type { Pattern, RunPattern } from '../compile/model.js';
|
|
2
|
+
export declare const MAX_PATTERN_STATES = 10000;
|
|
3
|
+
/** Thrown when an I-Regexp source compiles past `MAX_PATTERN_STATES` NFA states. */
|
|
4
|
+
export declare class PatternComplexityError extends RangeError {
|
|
5
|
+
readonly limit: number;
|
|
6
|
+
constructor(limit?: number);
|
|
7
|
+
}
|
|
2
8
|
/** Compile scalar sequences as counted runs; preserve the NFA for branching expressions. */
|
|
3
9
|
export declare function compilePattern(source: string): Pattern | RunPattern;
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { parseIRegexp } from './ast.js';
|
|
2
|
+
// A branching I-Regexp compiles to an NFA whose size is the product of its nested counted
|
|
3
|
+
// repetitions, so a short source such as `(a{0,1000}){0,60}b` unrolls to ~120k states and costs
|
|
4
|
+
// seconds per validated value (P1-I-01). Bound the NFA so a pathological source is rejected at
|
|
5
|
+
// schema admission instead. The limit is well above realistic patterns: the RFC 9485 domain-name
|
|
6
|
+
// regex `([a-z0-9-]{1,63}\.){1,32}[a-z]{2,63}` compiles to ~4.3k states. Pure scalar runs
|
|
7
|
+
// (`a{0,1000}`) never reach this path: they compile to counted runs, not NFA states.
|
|
8
|
+
export const MAX_PATTERN_STATES = 10_000;
|
|
9
|
+
/** Thrown when an I-Regexp source compiles past `MAX_PATTERN_STATES` NFA states. */
|
|
10
|
+
export class PatternComplexityError extends RangeError {
|
|
11
|
+
limit;
|
|
12
|
+
constructor(limit = MAX_PATTERN_STATES) {
|
|
13
|
+
super(`I-Regexp compiles to more than ${limit} states.`);
|
|
14
|
+
this.limit = limit;
|
|
15
|
+
this.name = 'PatternComplexityError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
/** Compile scalar sequences as counted runs; preserve the NFA for branching expressions. */
|
|
3
19
|
export function compilePattern(source) {
|
|
4
20
|
const expression = parseIRegexp(source);
|
|
@@ -123,6 +139,8 @@ class Builder {
|
|
|
123
139
|
return { start: left.start, end: right.end };
|
|
124
140
|
}
|
|
125
141
|
state() {
|
|
142
|
+
if (this.states.length >= MAX_PATTERN_STATES)
|
|
143
|
+
throw new PatternComplexityError();
|
|
126
144
|
return this.states.push({ epsilon: [], transitions: [] }) - 1;
|
|
127
145
|
}
|
|
128
146
|
epsilon(from, to) {
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { compare, compareMeaning, compareStructure } from './compare.js';
|
|
2
|
-
export
|
|
2
|
+
export { requiredBump } from './versioning/index.js';
|
|
3
|
+
export type { Analysis, Assessment, Bump, Comparison, ComparisonEntry, ComparisonEvidence, ComparisonRequest, ComparisonScope, Facet, Fingerprint, MeaningComparison, Observation, RequiredBump, SchemaIdentity, StructureComparison, Subject, Usage, VersionChange, } from './model.js';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Analysis, ComparisonEvidence } from './model.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Only semantic facts and decisions are durable; provenance and diagnostic prose are not. Each
|
|
4
|
+
* analysis names the version of the requirement policy its facts were assessed under.
|
|
5
|
+
*/
|
|
6
|
+
export declare function evidenceOf({ kind, version, scope, entries, assessments, }: Omit<Analysis, 'evidence'> & {
|
|
4
7
|
readonly kind: ComparisonEvidence['kind'];
|
|
8
|
+
readonly version: ComparisonEvidence['version'];
|
|
5
9
|
}): ComparisonEvidence;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { compareUnicode } from '../../addressing/ordering.js';
|
|
2
2
|
import { fingerprintMeaning } from './comparators/values.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Only semantic facts and decisions are durable; provenance and diagnostic prose are not. Each
|
|
5
|
+
* analysis names the version of the requirement policy its facts were assessed under.
|
|
6
|
+
*/
|
|
7
|
+
export function evidenceOf({ kind, version, scope, entries, assessments, }) {
|
|
6
8
|
return Object.freeze({
|
|
7
9
|
kind,
|
|
8
10
|
version,
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export * from './api.js';
|
|
2
|
-
export { compareDependencyMeaning, compareDependencyStructure, compareMemberMeaning, describeMeaningChange, footprintOf, meaningOf, } from './legacy/index.js';
|
|
3
|
-
export type { DependencyMeaningChange, DependencyMeaningChangeKind, DependencyMeaningComparison, DependencyStructureComparison, FootprintEntry, } from './legacy/index.js';
|
|
4
2
|
export type { MeaningDifference } from './diff.js';
|
|
5
3
|
export { ANNOTATION_KEYWORDS, FIELD_ROLES } from './fields.js';
|
|
6
|
-
export type {
|
|
4
|
+
export type { FieldRole } from './fields.js';
|
|
7
5
|
export { callableFingerprint } from './comparators/callables.js';
|
|
8
6
|
export type { Fingerprint as MeaningFingerprint } from './model.js';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export * from './api.js';
|
|
2
|
-
export { compareDependencyMeaning, compareDependencyStructure, compareMemberMeaning, describeMeaningChange, footprintOf, meaningOf, } from './legacy/index.js';
|
|
3
2
|
export { ANNOTATION_KEYWORDS, FIELD_ROLES } from './fields.js';
|
|
4
3
|
export { callableFingerprint } from './comparators/callables.js';
|
|
@@ -9,6 +9,6 @@ export function analyzeMeaning(context) {
|
|
|
9
9
|
kind: 'meaning',
|
|
10
10
|
...result,
|
|
11
11
|
equivalent: result.assessments.every(({ satisfied }) => satisfied),
|
|
12
|
-
evidence: evidenceOf({ kind: 'meaning', ...result }),
|
|
12
|
+
evidence: evidenceOf({ kind: 'meaning', version: 1, ...result }),
|
|
13
13
|
});
|
|
14
14
|
}
|
|
@@ -9,6 +9,14 @@ export interface ComparisonRequest {
|
|
|
9
9
|
readonly scope: {
|
|
10
10
|
readonly kind: 'dependency';
|
|
11
11
|
readonly dependent: DomainSchema;
|
|
12
|
+
/**
|
|
13
|
+
* Function, Method and Class Keys the dependent declares as capability requirements outside
|
|
14
|
+
* its schema. Keys of the target origin become roots with relation `capability`: a Function
|
|
15
|
+
* or Method requires its existence and dispatch, a Class its reference meaning and effective
|
|
16
|
+
* Property surface. Keys of other origins are ignored; a value that is not a Key list, or an
|
|
17
|
+
* unresolvable or unsupported Key of the target origin, is rejected with DM_CAPABILITY_INVALID.
|
|
18
|
+
*/
|
|
19
|
+
readonly capabilities?: readonly Key[];
|
|
12
20
|
} | {
|
|
13
21
|
readonly kind: 'members';
|
|
14
22
|
readonly source: DomainSchema;
|
|
@@ -36,7 +44,7 @@ export interface Subject {
|
|
|
36
44
|
export interface Usage {
|
|
37
45
|
readonly source: SchemaIdentity;
|
|
38
46
|
readonly key?: Key;
|
|
39
|
-
readonly relation: ReferenceSite | 'member' | 'abstract' | 'selection';
|
|
47
|
+
readonly relation: ReferenceSite | 'member' | 'abstract' | 'selection' | 'capability';
|
|
40
48
|
}
|
|
41
49
|
export type Facet = 'class.reference' | 'class.inherit' | 'class.composition' | 'property' | 'property.required' | 'method.existence' | 'method.inheritance' | 'method.signature' | 'function.existence' | 'function.signature' | 'policy' | 'view' | 'core';
|
|
42
50
|
/** A fact about a compared facet, including unchanged facets. No admission decision is implied. */
|
|
@@ -64,10 +72,16 @@ export interface Assessment {
|
|
|
64
72
|
};
|
|
65
73
|
readonly satisfied: boolean;
|
|
66
74
|
}
|
|
67
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Evidence is a semantic projection; presentation/provenance never enters its digest. Versions are
|
|
77
|
+
* per kind. Meaning is version 1. Structure is version 2, or version 3 when declared capabilities
|
|
78
|
+
* are among its roots, so a comparison without them keeps the version-2 bytes. Capability Keys join
|
|
79
|
+
* the root Key set of both digests: one that is not already a root changes the meaning fingerprint,
|
|
80
|
+
* never its version, and meaning evidence never records whether capabilities were declared.
|
|
81
|
+
*/
|
|
68
82
|
export interface ComparisonEvidence {
|
|
69
83
|
readonly kind: 'structure' | 'meaning';
|
|
70
|
-
readonly version: 1 | 2;
|
|
84
|
+
readonly version: 1 | 2 | 3;
|
|
71
85
|
readonly fingerprint: Fingerprint;
|
|
72
86
|
}
|
|
73
87
|
export interface Analysis {
|
|
@@ -94,3 +108,19 @@ export interface Comparison {
|
|
|
94
108
|
readonly meaning: ComparisonEvidence;
|
|
95
109
|
};
|
|
96
110
|
}
|
|
111
|
+
/** The semantic level a schema change requires at minimum; version numbers stay with the caller. */
|
|
112
|
+
export type Bump = 'patch' | 'minor' | 'major';
|
|
113
|
+
/** One non-equal observation between two versions of one Domain, with the level it alone requires. */
|
|
114
|
+
export interface VersionChange {
|
|
115
|
+
readonly subject: Subject;
|
|
116
|
+
readonly observation: Observation;
|
|
117
|
+
readonly bump: Bump;
|
|
118
|
+
}
|
|
119
|
+
/** The floor `next` sets over `previous`: the highest level among its changes, patch without any. */
|
|
120
|
+
export interface RequiredBump {
|
|
121
|
+
readonly level: Bump;
|
|
122
|
+
readonly previous: SchemaIdentity;
|
|
123
|
+
readonly next: SchemaIdentity;
|
|
124
|
+
/** Every non-equal observation, ordered by subject Key then observation id; empty for equal revisions. */
|
|
125
|
+
readonly changes: readonly VersionChange[];
|
|
126
|
+
}
|
|
@@ -18,9 +18,17 @@ export interface ComparisonContext {
|
|
|
18
18
|
readonly before: Context;
|
|
19
19
|
readonly after: Context;
|
|
20
20
|
readonly roots: readonly Reference[];
|
|
21
|
+
/** Roots the dependent declares as capabilities on the compared origin; also in `roots`. */
|
|
22
|
+
readonly capabilities: readonly Reference[];
|
|
21
23
|
pair(coordinate: Coordinate): SubjectPair;
|
|
22
24
|
}
|
|
23
25
|
export declare function prepare(request: ComparisonRequest): ComparisonContext;
|
|
26
|
+
/**
|
|
27
|
+
* Select distinct Keys of the source against a target the caller checked to be of the same origin.
|
|
28
|
+
* The selection may be empty, which a public request cannot be: `requiredBump` selects every member
|
|
29
|
+
* of a version that may declare none.
|
|
30
|
+
*/
|
|
31
|
+
export declare function selectMembers(source: DomainSchema, target: DomainSchema, keys: readonly Key[]): ComparisonContext;
|
|
24
32
|
export declare function retainedContext(dependent: DomainSchema, origin: Key.Origin, path: '/target/origin' | '/origin'): {
|
|
25
33
|
source: DomainSchema;
|
|
26
34
|
context: Context;
|