@astrale-os/kernel-dsl 0.2.0-beta.43 → 0.2.0-beta.45

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 (35) hide show
  1. package/dist/v1/compiled/admission/accept.js +10 -27
  2. package/dist/v1/compiled/loader/load.d.ts +2 -11
  3. package/dist/v1/compiled/loader/load.js +1 -3
  4. package/dist/v1/language/value/evaluate/evaluator.js +29 -11
  5. package/dist/v1/language/value/profile/rules.js +25 -0
  6. package/dist/v1/language/value/regexp/compile.d.ts +6 -0
  7. package/dist/v1/language/value/regexp/compile.js +18 -0
  8. package/dist/v1/schema/compatibility/api.d.ts +2 -1
  9. package/dist/v1/schema/compatibility/api.js +1 -0
  10. package/dist/v1/schema/compatibility/evidence.d.ts +6 -2
  11. package/dist/v1/schema/compatibility/evidence.js +5 -3
  12. package/dist/v1/schema/compatibility/index.d.ts +1 -3
  13. package/dist/v1/schema/compatibility/index.js +0 -1
  14. package/dist/v1/schema/compatibility/meaning/index.js +1 -1
  15. package/dist/v1/schema/compatibility/model.d.ts +33 -3
  16. package/dist/v1/schema/compatibility/scope.d.ts +8 -0
  17. package/dist/v1/schema/compatibility/scope.js +50 -21
  18. package/dist/v1/schema/compatibility/structure/index.js +6 -1
  19. package/dist/v1/schema/compatibility/structure/requirements.js +5 -3
  20. package/dist/v1/schema/compatibility/subjects.d.ts +5 -0
  21. package/dist/v1/schema/compatibility/subjects.js +39 -0
  22. package/dist/v1/schema/compatibility/versioning/index.d.ts +8 -0
  23. package/dist/v1/schema/compatibility/versioning/index.js +50 -0
  24. package/dist/v1/schema/compatibility/versioning/members.d.ts +12 -0
  25. package/dist/v1/schema/compatibility/versioning/members.js +47 -0
  26. package/dist/v1/schema/compatibility/versioning/surface.d.ts +11 -0
  27. package/dist/v1/schema/compatibility/versioning/surface.js +61 -0
  28. package/dist/v1/schema/diagnostic.d.ts +1 -1
  29. package/dist/v1/schema/index.d.ts +2 -4
  30. package/dist/v1/schema/index.js +1 -2
  31. package/dist/v1/schema/validation/policy/budgets.js +16 -3
  32. package/dist/v1/schema/validation/policy/typing.js +15 -0
  33. package/package.json +5 -1
  34. package/dist/v1/schema/compatibility/legacy/index.d.ts +0 -58
  35. 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')
@@ -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) {
@@ -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;
@@ -4,38 +4,66 @@ import { knownDomainClosure } from '../resolution/closure.js';
4
4
  import { resolve } from '../resolution/domain.js';
5
5
  import { contextOf } from './context.js';
6
6
  import { rootReferences, schemaIdentity, usageOf } from './references.js';
7
- import { coordinateForKey, resolveSubject } from './subjects.js';
7
+ import { capabilityCoordinates, coordinateForKey, resolveSubject } from './subjects.js';
8
8
  export function prepare(request) {
9
- const { target } = request;
10
- let source;
11
- let before;
12
- let roots;
13
- const dependent = request.scope.kind === 'dependency' ? request.scope.dependent : undefined;
14
- if (request.scope.kind === 'dependency') {
15
- const retained = retainedContext(request.scope.dependent, target.origin, '/target/origin');
16
- source = retained.source;
17
- before = retained.context;
18
- roots = rootReferences(request.scope.dependent);
19
- }
20
- else {
21
- source = request.scope.source;
9
+ const { scope, target } = request;
10
+ if (scope.kind === 'members') {
11
+ const { source, roots: keys } = scope;
22
12
  if (source.origin !== target.origin)
23
13
  throw new SchemaError([
24
14
  diagnostic('DM_MEMBER_ORIGIN_MISMATCH', '/target/origin', `Member comparison target ${target.origin} does not match source ${source.origin}.`),
25
15
  ]);
26
- const keys = request.scope.roots;
27
16
  if (!Array.isArray(keys) || keys.length === 0 || new Set(keys).size !== keys.length) {
28
17
  throw new SchemaError([
29
18
  diagnostic('DM_MEMBER_ROOT_INVALID', '/roots', 'Member comparison roots must be one non-empty duplicate-free Key set.'),
30
19
  ]);
31
20
  }
32
- resolve(source);
33
- before = contextOf(source);
34
- roots = keys.map((key, index) => ({
21
+ return selectMembers(source, target, keys);
22
+ }
23
+ const { dependent } = scope;
24
+ const { source, context: before } = retainedContext(dependent, target.origin, '/target/origin');
25
+ const keys = scope.capabilities;
26
+ if (keys !== undefined && !Array.isArray(keys)) {
27
+ throw new SchemaError([
28
+ diagnostic('DM_CAPABILITY_INVALID', '/capabilities', 'Declared capabilities must be one Key list.'),
29
+ ]);
30
+ }
31
+ const usage = usageOf(dependent, 'capability');
32
+ const capabilities = capabilityCoordinates(before, source, keys ?? []).map((coordinate) => ({
33
+ coordinate,
34
+ usage,
35
+ }));
36
+ return comparisonContext({
37
+ kind: 'dependency',
38
+ source,
39
+ target,
40
+ dependent,
41
+ before,
42
+ roots: [...rootReferences(dependent), ...capabilities],
43
+ capabilities,
44
+ });
45
+ }
46
+ /**
47
+ * Select distinct Keys of the source against a target the caller checked to be of the same origin.
48
+ * The selection may be empty, which a public request cannot be: `requiredBump` selects every member
49
+ * of a version that may declare none.
50
+ */
51
+ export function selectMembers(source, target, keys) {
52
+ resolve(source);
53
+ const before = contextOf(source);
54
+ return comparisonContext({
55
+ kind: 'members',
56
+ source,
57
+ target,
58
+ before,
59
+ roots: keys.map((key, index) => ({
35
60
  coordinate: coordinateForKey(before, source, key, index),
36
61
  usage: usageOf(source, 'selection', key),
37
- }));
38
- }
62
+ })),
63
+ capabilities: [],
64
+ });
65
+ }
66
+ function comparisonContext({ kind, source, target, dependent, before, roots, capabilities, }) {
39
67
  resolve(target);
40
68
  const after = contextOf(target);
41
69
  const pairs = new Map();
@@ -46,8 +74,9 @@ export function prepare(request) {
46
74
  before,
47
75
  after,
48
76
  roots,
77
+ capabilities,
49
78
  scope: Object.freeze({
50
- kind: request.scope.kind,
79
+ kind,
51
80
  source: schemaIdentity(source),
52
81
  target: schemaIdentity(target),
53
82
  ...(dependent === undefined ? {} : { dependent: schemaIdentity(dependent) }),
@@ -11,6 +11,11 @@ export function analyzeStructure(context) {
11
11
  kind: 'structure',
12
12
  ...result,
13
13
  compatible: result.assessments.every(({ satisfied }) => satisfied),
14
- evidence: evidenceOf({ kind: 'structure', ...result }),
14
+ // Version 3 adds declared capability roots; without one, evidence keeps its version-2 bytes.
15
+ evidence: evidenceOf({
16
+ kind: 'structure',
17
+ version: context.capabilities.length > 0 ? 3 : 2,
18
+ ...result,
19
+ }),
15
20
  });
16
21
  }
@@ -55,9 +55,11 @@ export function compareRequirements({ context, report, }) {
55
55
  }
56
56
  if (Key.origin(coordinate.key) !== context.source.origin)
57
57
  continue;
58
- // Class membership does not establish an invocation dependency. Explicit member selection
59
- // and inherited abstract obligations do establish requirements; call tracking can add others.
60
- const required = facet !== 'method.existence' || usage.relation === 'selection';
58
+ // Class membership does not establish an invocation dependency. Explicit member selection,
59
+ // a Method the dependent declares as a capability, and inherited abstract obligations do.
60
+ const required = facet !== 'method.existence' ||
61
+ usage.relation === 'selection' ||
62
+ usage.relation === 'capability';
61
63
  report.record({
62
64
  pair,
63
65
  facet,
@@ -32,3 +32,8 @@ export declare function resolveSubject({ context, coordinate, }: {
32
32
  export declare function identityOf(coordinate: Coordinate): Subject;
33
33
  /** Resolve one caller-supplied Key of the source into an exact coordinate that exists there. */
34
34
  export declare function coordinateForKey(context: Context, source: DomainSchema, input: SemanticKey, index: number): Coordinate;
35
+ /**
36
+ * Resolve the Function, Method and Class Keys a dependent declares as capabilities into exact
37
+ * coordinates of its retained source. Keys of other origins belong to other comparisons.
38
+ */
39
+ export declare function capabilityCoordinates(context: Context, source: DomainSchema, capabilities: readonly SemanticKey[]): readonly Coordinate[];
@@ -79,6 +79,45 @@ export function coordinateForKey(context, source, input, index) {
79
79
  }
80
80
  return coordinate;
81
81
  }
82
+ /**
83
+ * Resolve the Function, Method and Class Keys a dependent declares as capabilities into exact
84
+ * coordinates of its retained source. Keys of other origins belong to other comparisons.
85
+ */
86
+ export function capabilityCoordinates(context, source, capabilities) {
87
+ return capabilities.flatMap((key, index) => {
88
+ if (!Key.is(key))
89
+ return invalidCapability(key, index, 'is not a canonical semantic Key');
90
+ if (Key.origin(key) !== source.origin)
91
+ return [];
92
+ const coordinate = capabilityCoordinate(source, key);
93
+ if (coordinate === undefined) {
94
+ return invalidCapability(key, index, 'is not a Function, Method or Class');
95
+ }
96
+ if (resolveSubject({ context, coordinate }) === undefined) {
97
+ return invalidCapability(key, index, `does not exist in the retained ${source.origin}`);
98
+ }
99
+ return [coordinate];
100
+ });
101
+ }
102
+ /** A capability names a Function, a Method by its declaring Class, or a Class. */
103
+ function capabilityCoordinate(source, key) {
104
+ const { kind, name, member, ownerName } = keyReference(key);
105
+ const { origin } = source;
106
+ if (member === 'method')
107
+ return methodCoordinate({ origin, kind: 'class', name: ownerName }, name);
108
+ if (member !== undefined)
109
+ return undefined;
110
+ if (kind === 'class')
111
+ return classCoordinate({ origin, kind: 'class', name });
112
+ if (kind === 'function')
113
+ return memberCoordinate({ origin, kind: 'function', name });
114
+ return undefined;
115
+ }
116
+ function invalidCapability(key, index, reason) {
117
+ throw new SchemaError([
118
+ diagnostic('DM_CAPABILITY_INVALID', `/capabilities/${index}`, `Capability ${JSON.stringify(key)} ${reason}.`, typeof key === 'string' ? key : undefined),
119
+ ]);
120
+ }
82
121
  function invalidMemberRoot(key, index, reason) {
83
122
  throw new SchemaError([
84
123
  diagnostic('DM_MEMBER_ROOT_INVALID', `/roots/${index}`, `Key ${JSON.stringify(key)} ${reason}.`, typeof key === 'string' ? key : undefined),
@@ -0,0 +1,8 @@
1
+ import type { DomainSchema } from '../../model/schema.js';
2
+ import type { RequiredBump } from '../model.js';
3
+ /**
4
+ * Minimum schema bump: unsatisfied requirements are major, additions minor, other changes patch.
5
+ * Concrete callable authorization and code behavior are outside this floor; abstract Method auth
6
+ * belongs to the inherited signature. Version arithmetic stays with the caller.
7
+ */
8
+ export declare function requiredBump(previous: DomainSchema, next: DomainSchema): RequiredBump;
@@ -0,0 +1,50 @@
1
+ import { deepFreeze } from '../../canonical/freeze.js';
2
+ import { SchemaError, diagnostic } from '../../diagnostic.js';
3
+ import { compareFootprint } from '../meaning/footprint.js';
4
+ import { schemaIdentity } from '../references.js';
5
+ import { createReport } from '../report.js';
6
+ import { selectMembers } from '../scope.js';
7
+ import { compareRequirements } from '../structure/requirements.js';
8
+ import { compareAdditions, memberCoordinates } from './members.js';
9
+ import { compareSurface } from './surface.js';
10
+ const LEVELS = ['patch', 'minor', 'major'];
11
+ /**
12
+ * Minimum schema bump: unsatisfied requirements are major, additions minor, other changes patch.
13
+ * Concrete callable authorization and code behavior are outside this floor; abstract Method auth
14
+ * belongs to the inherited signature. Version arithmetic stays with the caller.
15
+ */
16
+ export function requiredBump(previous, next) {
17
+ if (previous.origin !== next.origin)
18
+ throw new SchemaError([
19
+ diagnostic('DM_MEMBER_ORIGIN_MISMATCH', '/next/origin', `Next version ${next.origin} does not match previous version ${previous.origin}.`),
20
+ ]);
21
+ const identities = { previous: schemaIdentity(previous), next: schemaIdentity(next) };
22
+ if (identities.previous.revision === identities.next.revision)
23
+ return deepFreeze({ level: 'patch', ...identities, changes: [] });
24
+ const members = memberCoordinates(previous);
25
+ const context = selectMembers(previous, next, members.map(({ key }) => key));
26
+ // Surface observations must win when an existing obligation becomes newly imposed on a Class.
27
+ const report = createReport();
28
+ compareSurface({ context, report });
29
+ compareRequirements({ context, report });
30
+ compareFootprint({ context, report });
31
+ compareAdditions({ context, report, previous: members });
32
+ const { entries, assessments } = report.finish();
33
+ const unsatisfied = new Set(assessments.filter(({ satisfied }) => !satisfied).map(({ observation }) => observation.id));
34
+ const changes = entries.flatMap(({ subject, observations }) => observations
35
+ .filter(({ state }) => state !== 'equal')
36
+ .map((observation) => ({
37
+ subject,
38
+ observation,
39
+ bump: unsatisfied.has(observation.id)
40
+ ? 'major'
41
+ : observation.state === 'added'
42
+ ? 'minor'
43
+ : 'patch',
44
+ })));
45
+ return deepFreeze({
46
+ level: changes.reduce((level, { bump }) => (LEVELS.indexOf(bump) > LEVELS.indexOf(level) ? bump : level), 'patch'),
47
+ ...identities,
48
+ changes,
49
+ });
50
+ }
@@ -0,0 +1,12 @@
1
+ import type { DomainSchema } from '../../model/schema.js';
2
+ import type { Coordinate } from '../coordinate.js';
3
+ import type { Report } from '../report.js';
4
+ import type { ComparisonContext } from '../scope.js';
5
+ /** Inherited members count once, at their declaring Class. */
6
+ export declare function memberCoordinates(schema: DomainSchema): readonly Coordinate[];
7
+ /** New declarations impose no requirements here; the surface pass checks obligations on existing Classes. */
8
+ export declare function compareAdditions({ context, report, previous, }: {
9
+ readonly context: ComparisonContext;
10
+ readonly report: Report;
11
+ readonly previous: readonly Coordinate[];
12
+ }): void;
@@ -0,0 +1,47 @@
1
+ import { compareFacet } from '../comparators/index.js';
2
+ import { classCoordinate, memberCoordinate, methodCoordinate, propertyCoordinate, } from '../coordinate.js';
3
+ /** Inherited members count once, at their declaring Class. */
4
+ export function memberCoordinates(schema) {
5
+ const result = [];
6
+ for (const declaration of Object.values(schema.classes)) {
7
+ result.push(classCoordinate(declaration.ref));
8
+ for (const name of Object.keys(declaration.properties))
9
+ result.push(propertyCoordinate(declaration.ref, name));
10
+ if (declaration.kind === 'node')
11
+ for (const name of Object.keys(declaration.methods))
12
+ result.push(methodCoordinate(declaration.ref, name));
13
+ }
14
+ for (const { ref } of [
15
+ ...Object.values(schema.functions),
16
+ ...Object.values(schema.policies),
17
+ ...Object.values(schema.views),
18
+ ...Object.values(schema.core.nodes),
19
+ ])
20
+ result.push(memberCoordinate(ref));
21
+ return result;
22
+ }
23
+ /** New declarations impose no requirements here; the surface pass checks obligations on existing Classes. */
24
+ export function compareAdditions({ context, report, previous, }) {
25
+ const known = new Set(previous.map(({ key }) => key));
26
+ for (const coordinate of memberCoordinates(context.target)) {
27
+ if (known.has(coordinate.key))
28
+ continue;
29
+ const pair = context.pair(coordinate);
30
+ for (const facet of selectedFacets(coordinate))
31
+ report.record({ pair, facet, ...compareFacet({ context, pair, facet }) });
32
+ }
33
+ }
34
+ function selectedFacets(coordinate) {
35
+ switch (coordinate.kind) {
36
+ case 'class':
37
+ return ['class.reference'];
38
+ case 'property':
39
+ return ['property'];
40
+ case 'method':
41
+ return ['method.existence', 'method.signature'];
42
+ case 'member':
43
+ return coordinate.ref.kind === 'function'
44
+ ? ['function.existence', 'function.signature']
45
+ : [coordinate.ref.kind];
46
+ }
47
+ }
@@ -0,0 +1,11 @@
1
+ import type { Report } from '../report.js';
2
+ import type { ComparisonContext } from '../scope.js';
3
+ /**
4
+ * Compare inherited contracts and newly imposed obligations on existing Classes.
5
+ * Obligations are reported at their declaring Method: changed signatures retain their comparison;
6
+ * an unchanged obligation newly inherited by a Class is added and unsatisfied.
7
+ */
8
+ export declare function compareSurface({ context, report, }: {
9
+ readonly context: ComparisonContext;
10
+ readonly report: Report;
11
+ }): void;
@@ -0,0 +1,61 @@
1
+ import { methodInheritance } from '../comparators/callables.js';
2
+ import { compareAddedProperties } from '../comparators/classes.js';
3
+ import { compareFacet } from '../comparators/index.js';
4
+ import { fingerprintMeaning } from '../comparators/values.js';
5
+ import { classCoordinate, methodCoordinate } from '../coordinate.js';
6
+ /**
7
+ * Compare inherited contracts and newly imposed obligations on existing Classes.
8
+ * Obligations are reported at their declaring Method: changed signatures retain their comparison;
9
+ * an unchanged obligation newly inherited by a Class is added and unsatisfied.
10
+ */
11
+ export function compareSurface({ context, report, }) {
12
+ const established = new Map();
13
+ const imposed = new Map();
14
+ for (const declaration of Object.values(context.source.classes)) {
15
+ if (context.target.classes[declaration.ref.name] === undefined)
16
+ continue;
17
+ const pair = context.pair(classCoordinate(declaration.ref));
18
+ const { source, target } = pair;
19
+ if (source?.kind !== 'class' || target?.kind !== 'class')
20
+ continue;
21
+ report.record({
22
+ pair,
23
+ facet: 'class.inherit',
24
+ ...compareFacet({ context, pair, facet: 'class.inherit' }),
25
+ requirement: { kind: 'class.inherit' },
26
+ });
27
+ compareAddedProperties({ context, pair, report });
28
+ for (const [key, method] of source.analysis.requirements)
29
+ if (!established.has(key))
30
+ established.set(key, method);
31
+ for (const [key, method] of target.analysis.requirements)
32
+ if (!source.analysis.requirements.has(key) && !imposed.has(key))
33
+ imposed.set(key, method);
34
+ }
35
+ for (const key of new Set([...established.keys(), ...imposed.keys()])) {
36
+ const added = imposed.get(key);
37
+ const method = established.get(key) ?? added;
38
+ const pair = context.pair(methodCoordinate(method.owner, method.name));
39
+ const comparison = established.has(key)
40
+ ? compareFacet({ context, pair, facet: 'method.inheritance' })
41
+ : undefined;
42
+ report.record(comparison !== undefined && (added === undefined || changed(comparison))
43
+ ? {
44
+ pair,
45
+ facet: 'method.inheritance',
46
+ ...comparison,
47
+ requirement: { kind: 'method.inheritance' },
48
+ }
49
+ : {
50
+ pair,
51
+ facet: 'method.inheritance',
52
+ after: methodInheritance(added),
53
+ requirement: { kind: 'method.inheritance', satisfied: false },
54
+ });
55
+ }
56
+ }
57
+ function changed({ before, after }) {
58
+ return (before === undefined ||
59
+ after === undefined ||
60
+ fingerprintMeaning(before) !== fingerprintMeaning(after));
61
+ }
@@ -1,4 +1,4 @@
1
- export type SchemaDiagnosticCode = 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_AUTH' | 'DV_CALLABLE_IO' | 'DV_CALLABLE_POLICY_AUTH' | 'DV_CLASS_ABSTRACT' | 'DV_CLASS_ACYCLIC' | 'DV_CLASS_KIND' | 'DV_CORE_EDGE_CLASS' | 'DV_CORE_EDGE_CONSTRAINTS' | 'DV_CORE_EDGE_KEY' | 'DV_CORE_EDGE_PROPERTIES' | 'DV_CORE_NODE_CLASS' | 'DV_CORE_NODE_PROPERTIES' | 'DV_CORE_REF' | 'DV_DATA_COMPOSITION' | 'DV_DEPENDENCY_ACYCLIC' | 'DV_DEPENDENCY_DIRECT' | 'DV_DEPENDENCY_EXACT' | 'DV_DEPENDENCY_LIMITS' | 'DV_DEPENDENCY_REVISION_COHERENCE' | 'DV_DEPENDENCY_UNIQUE' | 'DV_EDGE_ACCEPTS' | 'DV_EDGE_CLASS_COMPOSITION' | 'DV_EDGE_CONSTRAINT' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_PROPERTY_STATE' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | 'DV_VIEW_TARGET' | 'PL_BUDGET' | 'PL_COMPOSITION' | 'PL_CONNECTIVITY' | 'PL_REF' | 'PL_REPEAT' | 'PL_TARGET' | 'PL_TYPING' | 'PL_USE' | 'PL_VARIABLE_SCOPE' | 'STRUCTURE_ABSTRACT_POLICY' | 'STRUCTURE_CALLABLE_POLICY_AUTH' | 'STRUCTURE_DUPLICATE_ITEM' | 'STRUCTURE_EDGE_ORIENTATION' | 'STRUCTURE_FORMAT' | 'STRUCTURE_INVALID' | 'STRUCTURE_KEY' | 'STRUCTURE_MAXIMUM' | 'STRUCTURE_MINIMUM' | 'STRUCTURE_MULTIPLE' | 'STRUCTURE_REQUIRED' | 'STRUCTURE_TYPE' | 'STRUCTURE_UNION' | 'STRUCTURE_UNKNOWN_FIELD' | 'STRUCTURE_VALUE' | 'UNSUPPORTED_DOCUMENT_FORMAT' | 'UNSUPPORTED_DSL_VERSION';
1
+ export type SchemaDiagnosticCode = 'DM_CAPABILITY_INVALID' | 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_AUTH' | 'DV_CALLABLE_IO' | 'DV_CALLABLE_POLICY_AUTH' | 'DV_CLASS_ABSTRACT' | 'DV_CLASS_ACYCLIC' | 'DV_CLASS_KIND' | 'DV_CORE_EDGE_CLASS' | 'DV_CORE_EDGE_CONSTRAINTS' | 'DV_CORE_EDGE_KEY' | 'DV_CORE_EDGE_PROPERTIES' | 'DV_CORE_NODE_CLASS' | 'DV_CORE_NODE_PROPERTIES' | 'DV_CORE_REF' | 'DV_DATA_COMPOSITION' | 'DV_DEPENDENCY_ACYCLIC' | 'DV_DEPENDENCY_DIRECT' | 'DV_DEPENDENCY_EXACT' | 'DV_DEPENDENCY_LIMITS' | 'DV_DEPENDENCY_REVISION_COHERENCE' | 'DV_DEPENDENCY_UNIQUE' | 'DV_EDGE_ACCEPTS' | 'DV_EDGE_CLASS_COMPOSITION' | 'DV_EDGE_CONSTRAINT' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_PROPERTY_STATE' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | 'DV_VIEW_TARGET' | 'PL_BUDGET' | 'PL_COMPOSITION' | 'PL_CONNECTIVITY' | 'PL_REF' | 'PL_REPEAT' | 'PL_TARGET' | 'PL_TYPING' | 'PL_USE' | 'PL_VARIABLE_SCOPE' | 'STRUCTURE_ABSTRACT_POLICY' | 'STRUCTURE_CALLABLE_POLICY_AUTH' | 'STRUCTURE_DUPLICATE_ITEM' | 'STRUCTURE_EDGE_ORIENTATION' | 'STRUCTURE_FORMAT' | 'STRUCTURE_INVALID' | 'STRUCTURE_KEY' | 'STRUCTURE_MAXIMUM' | 'STRUCTURE_MINIMUM' | 'STRUCTURE_MULTIPLE' | 'STRUCTURE_REQUIRED' | 'STRUCTURE_TYPE' | 'STRUCTURE_UNION' | 'STRUCTURE_UNKNOWN_FIELD' | 'STRUCTURE_VALUE' | 'UNSUPPORTED_DOCUMENT_FORMAT' | 'UNSUPPORTED_DSL_VERSION';
2
2
  export interface SchemaDiagnostic<out Code extends SchemaDiagnosticCode = SchemaDiagnosticCode> {
3
3
  readonly code: Code;
4
4
  readonly pointer: string;
@@ -11,8 +11,8 @@ export type { SchemaDiagnosticCode as DiagnosticCode } from './diagnostic.js';
11
11
  export { acceptDomainSchema as accept } from './admission/accept.js';
12
12
  export { decodeDomainSchema as decode, encodeDomainSchema as encode, serializeDomainSchema as serialize, } from './codec/schema.js';
13
13
  export { revisionOfDomainSchema as revision } from './canonical/revision.js';
14
- export { ANNOTATION_KEYWORDS, FIELD_ROLES, callableFingerprint, compareDependencyMeaning, compareMemberMeaning, describeMeaningChange, footprintOf, meaningOf, } from './compatibility/index.js';
15
- export type { DependencyMeaningChange, DependencyMeaningChangeKind, DependencyMeaningComparison, DependencyMeaningLevel, FieldRole, FootprintEntry, MeaningDifference, MeaningFingerprint, } from './compatibility/index.js';
14
+ export { ANNOTATION_KEYWORDS, FIELD_ROLES, callableFingerprint } from './compatibility/index.js';
15
+ export type { FieldRole, MeaningDifference, MeaningFingerprint } from './compatibility/index.js';
16
16
  export * as compatibility from './compatibility/api.js';
17
17
  export { header } from './codec/header.js';
18
18
  export { DOMAIN_FORMAT as FORMAT, DOMAIN_VERSION as VERSION } from './model/format.js';
@@ -25,5 +25,3 @@ export type { DomainSchemaHeader as Header } from './codec/header.js';
25
25
  export type { DomainDependency } from '../addressing/index.js';
26
26
  /** The versioned value-schema grammar embedded in declarations. */
27
27
  export * as values from './validation/value-api.js';
28
- export { compareDependencyStructure } from './compatibility/index.js';
29
- export type { DependencyStructureComparison } from './compatibility/index.js';
@@ -4,7 +4,7 @@ export const resolve = resolveDomainSchema;
4
4
  export { acceptDomainSchema as accept } from './admission/accept.js';
5
5
  export { decodeDomainSchema as decode, encodeDomainSchema as encode, serializeDomainSchema as serialize, } from './codec/schema.js';
6
6
  export { revisionOfDomainSchema as revision } from './canonical/revision.js';
7
- export { ANNOTATION_KEYWORDS, FIELD_ROLES, callableFingerprint, compareDependencyMeaning, compareMemberMeaning, describeMeaningChange, footprintOf, meaningOf, } from './compatibility/index.js';
7
+ export { ANNOTATION_KEYWORDS, FIELD_ROLES, callableFingerprint } from './compatibility/index.js';
8
8
  export * as compatibility from './compatibility/api.js';
9
9
  export { header } from './codec/header.js';
10
10
  export { DOMAIN_FORMAT as FORMAT, DOMAIN_VERSION as VERSION } from './model/format.js';
@@ -12,4 +12,3 @@ export { compile, load } from '../compiled/index.js';
12
12
  export { SchemaError as Error } from './diagnostic.js';
13
13
  /** The versioned value-schema grammar embedded in declarations. */
14
14
  export * as values from './validation/value-api.js';
15
- export { compareDependencyStructure } from './compatibility/index.js';
@@ -3,8 +3,14 @@ import { BRANCH_OVERFLOW, MAX_BRANCHES, MAX_CHECK_DEPTH, MAX_CHECK_LEAVES, MAX_D
3
3
  import { escapePointer } from './context.js';
4
4
  export { BRANCH_OVERFLOW, MAX_BRANCHES, MAX_CHECK_DEPTH, MAX_CHECK_LEAVES, MAX_DOMAINS, MAX_EDGES, MAX_PATTERN_DEPTH, MAX_REPEAT, MAX_VARIABLES, measureChecks, };
5
5
  export function validatePolicyBudgets(context) {
6
+ // Measure every distinct composition node once. A node's expanded measure is independent of the
7
+ // path that reaches it (the only path-sensitive outcome, the back-edge `undefined`, already holds
8
+ // for every node on or reaching a composition cycle, regardless of entry), so caching the
9
+ // completed result by node identity turns path enumeration over the composition DAG into work
10
+ // linear in the number of nodes while preserving the exact measure and PL_BUDGET decision.
11
+ const measures = new Map();
6
12
  for (const name of Object.keys(context.environment.candidate.policies)) {
7
- const measure = measurePolicy(context, name, new Set());
13
+ const measure = measurePolicy(context, name, new Set(), measures);
8
14
  if (measure === undefined)
9
15
  continue;
10
16
  const pointer = `/policies/${escapePointer(name)}`;
@@ -23,14 +29,21 @@ export function validatePolicyBudgets(context) {
23
29
  }
24
30
  }
25
31
  }
26
- function measurePolicy(context, name, visiting) {
32
+ function measurePolicy(context, name, visiting, measures) {
27
33
  if (visiting.has(name))
28
34
  return undefined;
35
+ if (measures.has(name))
36
+ return measures.get(name);
29
37
  const declaration = context.normalized.get(name);
30
38
  if (declaration === undefined)
31
39
  return undefined;
32
40
  const nested = new Set(visiting);
33
41
  nested.add(name);
42
+ const measure = measureDeclaration(context, declaration, nested, measures);
43
+ measures.set(name, measure);
44
+ return measure;
45
+ }
46
+ function measureDeclaration(context, declaration, nested, measures) {
34
47
  if ('match' in declaration) {
35
48
  return measurePattern(declaration.match, context.environment.candidate.origin);
36
49
  }
@@ -38,7 +51,7 @@ function measurePolicy(context, name, visiting) {
38
51
  const children = refs.flatMap((ref) => {
39
52
  if (!context.isLocalPolicy(ref))
40
53
  return [];
41
- const value = measurePolicy(context, ref.name, nested);
54
+ const value = measurePolicy(context, ref.name, nested, measures);
42
55
  return value === undefined ? [] : [value];
43
56
  });
44
57
  if (children.length !== refs.length || children.length === 0)
@@ -5,6 +5,13 @@ import { escapePointer } from './context.js';
5
5
  import { referenceContracts } from './objects.js';
6
6
  export class PolicyTyping {
7
7
  context;
8
+ // Expanded branches of each named Policy, cached by node identity. A Policy's expansion is reused
9
+ // across every composition path that reaches it (and across every use site and object contract,
10
+ // none of which the expansion depends on), so a shared-child composition DAG is expanded in work
11
+ // linear in the number of Policies instead of enumerating every path. The expansion result is
12
+ // independent of the reaching path up to the opaque term-namespace labels, which only ever
13
+ // disambiguate variables within one branch and leave targetMode and branch typing unchanged.
14
+ #expansions = new Map();
8
15
  constructor(context) {
9
16
  this.context = context;
10
17
  }
@@ -59,6 +66,14 @@ export class PolicyTyping {
59
66
  #expandedBranches(name, visiting, namespace) {
60
67
  if (visiting.has(name))
61
68
  return undefined;
69
+ const cached = this.#expansions.get(name);
70
+ if (cached !== undefined || this.#expansions.has(name))
71
+ return cached;
72
+ const expanded = this.#expandDeclaration(name, visiting, namespace);
73
+ this.#expansions.set(name, expanded);
74
+ return expanded;
75
+ }
76
+ #expandDeclaration(name, visiting, namespace) {
62
77
  const declaration = this.context.normalized.get(name);
63
78
  if (declaration === undefined)
64
79
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-dsl",
3
- "version": "0.2.0-beta.43",
3
+ "version": "0.2.0-beta.45",
4
4
  "description": "Astrale Kernel DSL - Schema authoring, inference, and IR serialization",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -67,6 +67,10 @@
67
67
  "types": "./dist/v1/schema/index.d.ts",
68
68
  "import": "./dist/v1/schema/index.js"
69
69
  },
70
+ "./v1/schema/compatibility": {
71
+ "types": "./dist/v1/schema/compatibility/api.d.ts",
72
+ "import": "./dist/v1/schema/compatibility/api.js"
73
+ },
70
74
  "./v1/schema/fingerprint": {
71
75
  "types": "./dist/v1/schema/fingerprint.d.ts",
72
76
  "import": "./dist/v1/schema/fingerprint.js"
@@ -1,58 +0,0 @@
1
- /** Historical positional APIs and version-3 wire evidence. New analyses never depend on this adapter. */
2
- import type { Value as JsonValue } from '@astrale-os/kernel-dsl/value';
3
- import type { Definition, Key as SemanticKey, Revision } from '../../../addressing/index.js';
4
- import type { DomainSchema } from '../../model/schema.js';
5
- import type { MeaningDifference } from '../diff.js';
6
- import type { DependencyMeaningLevel } from '../fields.js';
7
- import type { Fingerprint as MeaningFingerprint } from '../model.js';
8
- export type DependencyMeaningChangeKind = 'missing' | 'changed' | 'added';
9
- export type DependencyMeaningChange = {
10
- readonly key: SemanticKey;
11
- readonly kind: 'missing';
12
- readonly sourceFingerprint: MeaningFingerprint;
13
- } | {
14
- readonly key: SemanticKey;
15
- readonly kind: 'changed';
16
- readonly sourceFingerprint: MeaningFingerprint;
17
- readonly targetFingerprint: MeaningFingerprint;
18
- /** Every exact path of the meaning that differs, in JSON Pointer order. */
19
- readonly differences: readonly MeaningDifference[];
20
- } | {
21
- /** A required, non-automatic Property the target adds to an inherited Class. */
22
- readonly key: SemanticKey;
23
- readonly kind: 'added';
24
- readonly targetFingerprint: MeaningFingerprint;
25
- readonly meaning: JsonValue;
26
- };
27
- export interface DependencyMeaningComparison {
28
- readonly version: 3;
29
- readonly dependency: SemanticKey.Origin;
30
- readonly source: Revision;
31
- readonly target: Revision;
32
- readonly footprint: readonly SemanticKey[];
33
- readonly changes: readonly DependencyMeaningChange[];
34
- readonly fingerprint: MeaningFingerprint;
35
- }
36
- /** One footprint entry: the Key, what it is, and for a Class how strongly it is used. */
37
- export interface FootprintEntry {
38
- readonly key: SemanticKey;
39
- readonly kind: Definition.Kind | 'property' | 'method';
40
- readonly level?: DependencyMeaningLevel;
41
- }
42
- export interface DependencyStructureComparison {
43
- readonly kind: 'structure';
44
- readonly version: 1;
45
- readonly dependency: SemanticKey.Origin;
46
- readonly source: Revision;
47
- readonly target: Revision;
48
- readonly footprint: readonly SemanticKey[];
49
- readonly changes: readonly DependencyMeaningChange[];
50
- readonly fingerprint: MeaningFingerprint;
51
- }
52
- export declare function compareDependencyMeaning(dependent: DomainSchema, target: DomainSchema): DependencyMeaningComparison;
53
- export declare function compareMemberMeaning(source: DomainSchema, target: DomainSchema, roots: readonly [SemanticKey, ...SemanticKey[]]): DependencyMeaningComparison;
54
- export declare function compareDependencyStructure(dependent: DomainSchema, target: DomainSchema): DependencyStructureComparison;
55
- export declare function footprintOf(dependent: DomainSchema, origin: SemanticKey.Origin): readonly FootprintEntry[];
56
- export declare function meaningOf(schema: DomainSchema, key: SemanticKey, level?: DependencyMeaningLevel): JsonValue;
57
- /** One line an operator can act on, naming the exact paths of a changed meaning. */
58
- export declare function describeMeaningChange(change: DependencyMeaningChange): string;
@@ -1,145 +0,0 @@
1
- import { Key } from '../../../addressing/index.js';
2
- import { compareUnicode } from '../../../addressing/ordering.js';
3
- import { compareFacet } from '../comparators/index.js';
4
- import { fingerprintMeaning } from '../comparators/values.js';
5
- import { compareMeaning, compareStructure } from '../compare.js';
6
- import { prepare, retainedContext } from '../scope.js';
7
- export function compareDependencyMeaning(dependent, target) {
8
- return legacyMeaning(compareMeaning({ scope: { kind: 'dependency', dependent }, target }));
9
- }
10
- export function compareMemberMeaning(source, target, roots) {
11
- return legacyMeaning(compareMeaning({ scope: { kind: 'members', source, roots }, target }));
12
- }
13
- export function compareDependencyStructure(dependent, target) {
14
- const report = compareStructure({ scope: { kind: 'dependency', dependent }, target });
15
- const failed = new Set(report.assessments.filter((item) => !item.satisfied).map((item) => item.observation.id));
16
- const changes = report.entries.flatMap((entry) => entry.observations.filter((item) => failed.has(item.id)).map((item) => changeOf(entry, item)));
17
- return Object.freeze({
18
- kind: 'structure',
19
- version: 1,
20
- dependency: report.scope.source.origin,
21
- source: report.scope.source.revision,
22
- target: report.scope.target.revision,
23
- footprint: Object.freeze(report.entries
24
- .filter((entry) => Key.origin(entry.subject.key) === report.scope.source.origin &&
25
- entry.observations.some((item) => item.before !== undefined))
26
- .map((entry) => entry.subject.key)),
27
- changes: Object.freeze(changes),
28
- fingerprint: report.evidence.fingerprint,
29
- });
30
- }
31
- export function footprintOf(dependent, origin) {
32
- const { source } = retainedContext(dependent, origin, '/origin');
33
- const report = compareMeaning({ scope: { kind: 'dependency', dependent }, target: source });
34
- return Object.freeze(report.entries.map((entry) => Object.freeze({
35
- ...entry.subject,
36
- ...(entry.subject.kind === 'class'
37
- ? {
38
- level: strongest(entry).facet === 'class.inherit'
39
- ? 'inherit'
40
- : 'reference',
41
- }
42
- : {}),
43
- })));
44
- }
45
- export function meaningOf(schema, key, level = 'reference') {
46
- const context = prepare({
47
- scope: { kind: 'members', source: schema, roots: [key] },
48
- target: schema,
49
- });
50
- const coordinate = context.roots[0].coordinate;
51
- const facet = coordinate.kind === 'class'
52
- ? level === 'inherit'
53
- ? 'class.inherit'
54
- : 'class.reference'
55
- : coordinate.kind === 'method'
56
- ? 'method.signature'
57
- : coordinate.kind === 'property'
58
- ? 'property'
59
- : coordinate.ref.kind === 'function'
60
- ? 'function.signature'
61
- : coordinate.ref.kind;
62
- return compareFacet({ context, pair: context.pair(coordinate), facet }).before;
63
- }
64
- function strongest(entry) {
65
- return (entry.observations.find((item) => item.facet === 'class.inherit') ??
66
- entry.observations.find((item) => item.facet !== 'property.required') ??
67
- entry.observations[0]);
68
- }
69
- function legacyMeaning(report) {
70
- const coordinates = [];
71
- const footprint = [];
72
- const changes = [];
73
- const additions = [];
74
- for (const entry of report.entries) {
75
- const added = entry.observations.find((item) => item.facet === 'property.required');
76
- if (added !== undefined)
77
- additions.push(changeOf(entry, added));
78
- const observation = strongest(entry);
79
- const change = changeOf(entry, observation);
80
- if (observation.before === undefined) {
81
- continue;
82
- }
83
- footprint.push(entry.subject.key);
84
- coordinates.push([
85
- entry.subject.key,
86
- fingerprintMeaning(observation.before),
87
- observation.after === undefined ? null : fingerprintMeaning(observation.after),
88
- ]);
89
- if (change !== undefined)
90
- changes.push(change);
91
- }
92
- additions.sort((a, b) => compareUnicode(a.key, b.key));
93
- const identity = {
94
- version: 3,
95
- dependency: report.scope.source.origin,
96
- source: report.scope.source.revision,
97
- target: report.scope.target.revision,
98
- };
99
- return Object.freeze({
100
- ...identity,
101
- footprint: Object.freeze(footprint),
102
- changes: Object.freeze([...changes, ...additions]),
103
- fingerprint: fingerprintMeaning({
104
- ...identity,
105
- coordinates,
106
- additions: additions.map(({ key, targetFingerprint }) => [key, targetFingerprint]),
107
- }),
108
- });
109
- }
110
- function changeOf(entry, observation) {
111
- const key = entry.subject.key;
112
- if (observation.state === 'equal')
113
- return undefined;
114
- if (observation.before === undefined)
115
- return Object.freeze({
116
- key,
117
- kind: 'added',
118
- targetFingerprint: fingerprintMeaning(observation.after),
119
- meaning: observation.after,
120
- });
121
- const sourceFingerprint = fingerprintMeaning(observation.before);
122
- if (observation.after === undefined)
123
- return Object.freeze({ key, kind: 'missing', sourceFingerprint });
124
- return Object.freeze({
125
- key,
126
- kind: 'changed',
127
- sourceFingerprint,
128
- targetFingerprint: fingerprintMeaning(observation.after),
129
- differences: observation.differences,
130
- });
131
- }
132
- /** One line an operator can act on, naming the exact paths of a changed meaning. */
133
- export function describeMeaningChange(change) {
134
- switch (change.kind) {
135
- case 'missing':
136
- return `${change.key} missing`;
137
- case 'added':
138
- return `${change.key} added as an obligation`;
139
- case 'changed': {
140
- const paths = change.differences.map(({ path }) => path);
141
- const shown = paths.slice(0, 3).join(', ');
142
- return `${change.key} changed at ${paths.length > 3 ? `${shown} and ${paths.length - 3} more` : shown}`;
143
- }
144
- }
145
- }