@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.
Files changed (39) hide show
  1. package/dist/v1/compiled/admission/accept.js +10 -27
  2. package/dist/v1/compiled/admission/semantics.js +19 -4
  3. package/dist/v1/compiled/loader/load.d.ts +2 -11
  4. package/dist/v1/compiled/loader/load.js +1 -3
  5. package/dist/v1/language/policy/analysis/checks.d.ts +28 -0
  6. package/dist/v1/language/policy/analysis/checks.js +80 -0
  7. package/dist/v1/language/value/evaluate/evaluator.js +29 -11
  8. package/dist/v1/language/value/profile/rules.js +25 -0
  9. package/dist/v1/language/value/regexp/compile.d.ts +6 -0
  10. package/dist/v1/language/value/regexp/compile.js +18 -0
  11. package/dist/v1/schema/compatibility/api.d.ts +2 -1
  12. package/dist/v1/schema/compatibility/api.js +1 -0
  13. package/dist/v1/schema/compatibility/evidence.d.ts +6 -2
  14. package/dist/v1/schema/compatibility/evidence.js +5 -3
  15. package/dist/v1/schema/compatibility/index.d.ts +1 -3
  16. package/dist/v1/schema/compatibility/index.js +0 -1
  17. package/dist/v1/schema/compatibility/meaning/index.js +1 -1
  18. package/dist/v1/schema/compatibility/model.d.ts +33 -3
  19. package/dist/v1/schema/compatibility/scope.d.ts +8 -0
  20. package/dist/v1/schema/compatibility/scope.js +50 -21
  21. package/dist/v1/schema/compatibility/structure/index.js +6 -1
  22. package/dist/v1/schema/compatibility/structure/requirements.js +5 -3
  23. package/dist/v1/schema/compatibility/subjects.d.ts +5 -0
  24. package/dist/v1/schema/compatibility/subjects.js +39 -0
  25. package/dist/v1/schema/compatibility/versioning/index.d.ts +8 -0
  26. package/dist/v1/schema/compatibility/versioning/index.js +50 -0
  27. package/dist/v1/schema/compatibility/versioning/members.d.ts +12 -0
  28. package/dist/v1/schema/compatibility/versioning/members.js +47 -0
  29. package/dist/v1/schema/compatibility/versioning/surface.d.ts +11 -0
  30. package/dist/v1/schema/compatibility/versioning/surface.js +61 -0
  31. package/dist/v1/schema/diagnostic.d.ts +1 -1
  32. package/dist/v1/schema/index.d.ts +2 -4
  33. package/dist/v1/schema/index.js +1 -2
  34. package/dist/v1/schema/validation/policy/typing.d.ts +2 -0
  35. package/dist/v1/schema/validation/policy/typing.js +5 -1
  36. package/dist/v1/schema/validation/policy/uses.js +20 -8
  37. package/package.json +5 -1
  38. package/dist/v1/schema/compatibility/legacy/index.d.ts +0 -58
  39. 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 !== 1 && root.version !== 2)
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}`, version));
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, version) {
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 = version === 2
102
- ? array(value.patterns, 'entry patterns', patternsPath).map((item, index) => pattern(item, index, `${patternsPath}/${index}`, true))
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, version) {
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, counted) {
841
+ function pattern(input, patternIndex, path) {
859
842
  const value = object(input, `patterns[${patternIndex}]`, path);
860
- if (counted && Object.hasOwn(value, 'runs'))
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 !== undefined &&
551
- contracts.some((contract) => !typing.policyAccepts(value.check.name, contract))) {
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 (branches !== undefined &&
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 { ValueProgram } from '../../language/value/compile/model.js';
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 | LegacyCompiledSchema): import('../../domain/model/domain.js').Domain;
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('patterns' in entry
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
- active = new Set();
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 activeKey = `${location.key}\u0000${instancePointer}`;
25
- if (this.active.has(activeKey)) {
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
- this.active.add(activeKey);
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
- if (location.schema === true)
31
- return evaluation();
32
- if (location.schema === false) {
33
- return evaluation([
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
- return this.evaluateObjectSchema(location, location.schema, input, instancePointer);
50
+ else {
51
+ result = this.evaluateObjectSchema(location, location.schema, input, instancePointer);
52
+ }
53
+ byLocation.set(key, result);
54
+ return result;
38
55
  }
39
- finally {
40
- this.active.delete(activeKey);
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 type { Analysis, Assessment, Comparison, ComparisonEntry, ComparisonEvidence, ComparisonRequest, ComparisonScope, Facet, Fingerprint, MeaningComparison, Observation, SchemaIdentity, StructureComparison, Subject, Usage, } from './model.js';
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 +1,2 @@
1
1
  export { compare, compareMeaning, compareStructure } from './compare.js';
2
+ export { requiredBump } from './versioning/index.js';
@@ -1,5 +1,9 @@
1
1
  import type { Analysis, ComparisonEvidence } from './model.js';
2
- /** Only semantic facts and decisions are durable; provenance and diagnostic prose are not. */
3
- export declare function evidenceOf({ kind, scope, entries, assessments, }: Omit<Analysis, 'evidence'> & {
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
- /** Only semantic facts and decisions are durable; provenance and diagnostic prose are not. */
4
- export function evidenceOf({ kind, scope, entries, assessments, }) {
5
- const version = kind === 'structure' ? 2 : 1;
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 { DependencyMeaningLevel, FieldRole } from './fields.js';
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
- /** Evidence is a semantic projection; presentation/provenance never enters its digest. */
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;