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

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 (51) hide show
  1. package/dist/v1/addressing/class-key.js +5 -4
  2. package/dist/v1/addressing/method-key.js +3 -2
  3. package/dist/v1/builder/lowering/value/zod.js +11 -2
  4. package/dist/v1/builder/value/full-match.js +6 -3
  5. package/dist/v1/compiled/admission/accept.js +27 -6
  6. package/dist/v1/compiled/admission/semantics.js +41 -16
  7. package/dist/v1/compiled/loader/load.js +1 -0
  8. package/dist/v1/domain/model/input.d.ts +3 -0
  9. package/dist/v1/domain/projection/classes.js +1 -0
  10. package/dist/v1/domain/validation/create.d.ts +2 -0
  11. package/dist/v1/domain/validation/create.js +12 -0
  12. package/dist/v1/language/class/resolved.d.ts +3 -0
  13. package/dist/v1/language/index.d.ts +2 -0
  14. package/dist/v1/language/index.js +1 -0
  15. package/dist/v1/language/policy/normalization/normalize.d.ts +4 -0
  16. package/dist/v1/language/policy/normalization/normalize.js +51 -13
  17. package/dist/v1/language/value/inclusion/includes.d.ts +9 -0
  18. package/dist/v1/language/value/inclusion/includes.js +777 -0
  19. package/dist/v1/language/value/profile/rules.js +11 -32
  20. package/dist/v1/language/value/regexp/ast.js +3 -64
  21. package/dist/v1/language/value/regexp/compile.d.ts +25 -2
  22. package/dist/v1/language/value/regexp/compile.js +48 -12
  23. package/dist/v1/language/value/regexp/syntax.d.ts +11 -1
  24. package/dist/v1/language/value/regexp/syntax.js +14 -2
  25. package/dist/v1/language/value/regexp/unicode.d.ts +32 -0
  26. package/dist/v1/language/value/regexp/unicode.js +139 -0
  27. package/dist/v1/language/value/syntax/full-match.js +6 -3
  28. package/dist/v1/schema/compatibility/comparators/callables.d.ts +7 -0
  29. package/dist/v1/schema/compatibility/comparators/callables.js +13 -0
  30. package/dist/v1/schema/compatibility/comparators/classes.d.ts +18 -0
  31. package/dist/v1/schema/compatibility/comparators/classes.js +54 -0
  32. package/dist/v1/schema/compatibility/comparators/index.d.ts +1 -1
  33. package/dist/v1/schema/compatibility/comparators/index.js +14 -2
  34. package/dist/v1/schema/compatibility/evidence.d.ts +8 -1
  35. package/dist/v1/schema/compatibility/evidence.js +10 -1
  36. package/dist/v1/schema/compatibility/meaning/footprint.js +3 -1
  37. package/dist/v1/schema/compatibility/meaning/index.js +4 -2
  38. package/dist/v1/schema/compatibility/model.d.ts +11 -6
  39. package/dist/v1/schema/compatibility/references.d.ts +8 -1
  40. package/dist/v1/schema/compatibility/references.js +39 -6
  41. package/dist/v1/schema/compatibility/scope.d.ts +5 -0
  42. package/dist/v1/schema/compatibility/scope.js +5 -2
  43. package/dist/v1/schema/compatibility/structure/index.js +21 -2
  44. package/dist/v1/schema/compatibility/structure/requirements.js +26 -1
  45. package/dist/v1/schema/compatibility/subjects.js +20 -2
  46. package/dist/v1/schema/resolution/domain.js +1 -0
  47. package/dist/v1/schema/resolution/origins.d.ts +11 -4
  48. package/dist/v1/schema/resolution/origins.js +7 -8
  49. package/dist/v1/schema/validation/class.d.ts +10 -0
  50. package/dist/v1/schema/validation/class.js +4 -2
  51. package/package.json +1 -1
@@ -20,10 +20,11 @@ export function ClassKey(input) {
20
20
  function is(input) {
21
21
  if (typeof input !== 'string' || !Key.is(input))
22
22
  return false;
23
- const separator = input.indexOf(':');
24
- return (input.slice(separator + 1).startsWith('class.') &&
25
- !input.includes('.property.') &&
26
- !input.includes('.method.'));
23
+ // Only the coordinate is tested: an origin label may itself be `property` or `method`.
24
+ const coordinate = input.slice(input.indexOf(':') + 1);
25
+ return (coordinate.startsWith('class.') &&
26
+ !coordinate.includes('.property.') &&
27
+ !coordinate.includes('.method.'));
27
28
  }
28
29
  ClassKey.is = is;
29
30
  function ref(input) {
@@ -21,8 +21,9 @@ export function MethodKey(input) {
21
21
  function is(input) {
22
22
  if (typeof input !== 'string' || !Key.is(input))
23
23
  return false;
24
- const coordinate = input.slice(input.indexOf(':') + 1);
25
- return coordinate.startsWith('class.') && coordinate.includes('.method.');
24
+ // The member kind is the third segment: the owner Class may itself be named `method`.
25
+ const [kind, , member] = input.slice(input.indexOf(':') + 1).split('.');
26
+ return kind === 'class' && member === 'method';
26
27
  }
27
28
  MethodKey.is = is;
28
29
  function address(input) {
@@ -161,7 +161,10 @@ function inspectCheck(def, pointer, diagnostics) {
161
161
  return;
162
162
  }
163
163
  if (def.check === 'string_format' && def.format === 'regex' && def.pattern instanceof RegExp) {
164
- if (!isLosslessRegexp(def.pattern)) {
164
+ if (def.pattern.flags === '') {
165
+ diagnostics.push(diagnostic('ZOD_SCHEMA_NOT_LOSSLESS', pointer, 'Zod RegExp without the u flag reads \\p{...} as literal text, and classes and quantifiers by UTF-16 code unit, unlike the V1 I-Regexp it would lower to; add the u flag.'));
166
+ }
167
+ else if (!isLosslessRegexp(def.pattern)) {
165
168
  diagnostics.push(diagnostic('ZOD_SCHEMA_NOT_LOSSLESS', pointer, 'Zod RegExp flags, anchors, dot semantics, or syntax cannot be preserved as a V1 I-Regexp substring pattern.'));
166
169
  }
167
170
  }
@@ -292,8 +295,14 @@ function escapePointerToken(value) {
292
295
  function isPlainRecord(value) {
293
296
  return value !== null && typeof value === 'object' && !Array.isArray(value);
294
297
  }
298
+ /**
299
+ * Whether a Zod RegExp search means its source read as an I-Regexp. Only the u flag qualifies:
300
+ * without it, `\p{L}` matches the text `p{L}`, and classes and quantifiers apply to UTF-16 code
301
+ * units rather than scalars; `v` reads class syntax differently, and every other flag changes the
302
+ * match itself.
303
+ */
295
304
  function isLosslessRegexp(pattern) {
296
- if (pattern.flags !== '' && pattern.flags !== 'u')
305
+ if (pattern.flags !== 'u')
297
306
  return false;
298
307
  if (!isIRegexp(pattern.source))
299
308
  return false;
@@ -1,11 +1,14 @@
1
- import { isIRegexp } from '../../language/value/regexp/syntax.js';
1
+ import { refuseIRegexp, stateBudgetMessage } from '../../language/value/regexp/syntax.js';
2
2
  import { fullMatch as languageFullMatch, } from '../../language/value/syntax/full-match.js';
3
3
  import { fail } from '../schema/diagnostic.js';
4
4
  export { fullMatchSource } from '../../language/value/syntax/full-match.js';
5
5
  /** Authoring facade preserving the Builder-owned diagnostic for invalid source syntax. */
6
6
  export function fullMatch(source) {
7
- if (!isIRegexp(source)) {
8
- fail('BUILDER_FULL_MATCH_INVALID', '/fullMatch', 'fullMatch(...) requires a complete RFC 9485 I-Regexp source.');
7
+ const refusal = refuseIRegexp(source);
8
+ if (refusal !== undefined) {
9
+ fail('BUILDER_FULL_MATCH_INVALID', '/fullMatch', refusal === 'syntax'
10
+ ? 'fullMatch(...) requires a complete RFC 9485 I-Regexp source.'
11
+ : stateBudgetMessage('fullMatch(...)'));
9
12
  }
10
13
  return languageFullMatch(source);
11
14
  }
@@ -8,6 +8,8 @@ import { admitStateTopology } from '../../language/state/topology.js';
8
8
  import { bindValueProgram } from '../../language/value/compile/load.js';
9
9
  import { isValueObject } from '../../language/value/model/object.js';
10
10
  import { classifyValueProgramOrdering } from '../../language/value/ordering.js';
11
+ import { compilePattern } from '../../language/value/regexp/compile.js';
12
+ import { isIRegexp } from '../../language/value/regexp/syntax.js';
11
13
  import { invalidCompiled } from '../diagnostic.js';
12
14
  import { revisionOfCompiledEntry } from './revision.js';
13
15
  import { validateCompiledSemantics } from './semantics.js';
@@ -24,7 +26,8 @@ export function acceptCompiledSchema(input) {
24
26
  if (root.version !== 2)
25
27
  invalid('version', undefined, '/version');
26
28
  const rootPin = dependency(root.root, 'root', '/root');
27
- const closure = array(root.closure, 'closure', '/closure').map((value, index) => entry(value, index, `/closure/${index}`));
29
+ const machines = new Map();
30
+ const closure = array(root.closure, 'closure', '/closure').map((value, index) => entry(value, index, `/closure/${index}`, machines));
28
31
  if (closure.length > dependencyLimits.closure) {
29
32
  invalid('dependency closure limit', 'COMPILED_DEPENDENCY_LIMITS', '/closure');
30
33
  }
@@ -74,7 +77,7 @@ export function acceptCompiledSchema(input) {
74
77
  validateCompiledSemantics(candidate);
75
78
  return deepFreeze(candidate);
76
79
  }
77
- function entry(input, entryIndex, path) {
80
+ function entry(input, entryIndex, path, machines) {
78
81
  const value = object(input, `closure[${entryIndex}]`, path);
79
82
  exact(value, ['origin', 'revision', 'dependencies', 'definitions', 'facts', 'values', 'patterns'], 'entry', path);
80
83
  const origin = originValue(value.origin, 'entry origin', `${path}/origin`);
@@ -89,7 +92,7 @@ function entry(input, entryIndex, path) {
89
92
  }
90
93
  const valuesPath = `${path}/values`;
91
94
  const patternsPath = `${path}/patterns`;
92
- const patterns = array(value.patterns, 'entry patterns', patternsPath).map((item, index) => pattern(item, index, `${patternsPath}/${index}`));
95
+ const patterns = array(value.patterns, 'entry patterns', patternsPath).map((item, index) => pattern(item, index, `${patternsPath}/${index}`, machines));
93
96
  const values = array(value.values, 'values', valuesPath).map((item, index) => program(item, index, `${valuesPath}/${index}`, patterns));
94
97
  const bound = values.map((value) => bindValueProgram(value, patterns));
95
98
  const definitionsPath = `${path}/definitions`;
@@ -838,10 +841,28 @@ function valueNode(input, nodeIndex, path) {
838
841
  : { reference: integer(value.reference, 'reference', `${path}/reference`) }),
839
842
  };
840
843
  }
841
- function pattern(input, patternIndex, path) {
844
+ /**
845
+ * Admit one received pattern as data and return the machine compiled from its source. A received
846
+ * machine never decides membership: it may come from another compiler, from a host with other
847
+ * Unicode data, or from a forger, and the artifact carries no compiler version that could tell a
848
+ * current machine from those, so it is ignored rather than compared. Its source decides, through
849
+ * the same bounded `isIRegexp` and `compilePattern` that source resolution uses.
850
+ */
851
+ function pattern(input, patternIndex, path, machines) {
842
852
  const value = object(input, `patterns[${patternIndex}]`, path);
843
- if (Object.hasOwn(value, 'runs'))
844
- return runPattern(value, path);
853
+ const { source } = Object.hasOwn(value, 'runs')
854
+ ? runPattern(value, path)
855
+ : nfaPattern(value, path);
856
+ let machine = machines.get(source);
857
+ if (machine === undefined) {
858
+ if (!isIRegexp(source))
859
+ invalid('Pattern source', 'COMPILED_SOURCE_INVALID', `${path}/source`);
860
+ machine = compilePattern(source);
861
+ machines.set(source, machine);
862
+ }
863
+ return machine;
864
+ }
865
+ function nfaPattern(value, path) {
845
866
  exact(value, ['source', 'start', 'states'], 'Pattern', path);
846
867
  if (typeof value.source !== 'string')
847
868
  invalid('Pattern source', undefined, `${path}/source`);
@@ -21,10 +21,11 @@ export function validateCompiledSemantics(candidate) {
21
21
  const partial = index(candidate);
22
22
  const analyses = analyzeClasses(partial.classes);
23
23
  const context = { ...partial, analyses };
24
+ const admitted = admittedOrigins(candidate);
24
25
  validateClassFacets(context);
25
- validateEdges(context);
26
+ validateEdges(context, admitted);
26
27
  validateViews(candidate, context);
27
- validatePolicies(candidate, context);
28
+ validatePolicies(candidate, context, admitted);
28
29
  validateCore(candidate, context);
29
30
  }
30
31
  function index(candidate) {
@@ -171,7 +172,30 @@ function validateClassFacets(context) {
171
172
  }
172
173
  }
173
174
  }
174
- function validateEdges(context) {
175
+ /**
176
+ * The origins each closure entry was admitted against: itself and its transitive dependencies.
177
+ * Source admission decides undirected overlap and types Policies within that closure alone, so a
178
+ * dependent's Class extending a dependency's Classes never reaches the dependency's own decisions.
179
+ */
180
+ function admittedOrigins(candidate) {
181
+ const entries = new Map(candidate.closure.map((entry) => [entry.origin, entry]));
182
+ const scopes = new Map();
183
+ const scope = (origin) => {
184
+ const known = scopes.get(origin);
185
+ if (known !== undefined)
186
+ return known;
187
+ const origins = new Set([origin]);
188
+ for (const dependency of Object.values(entries.get(origin)?.dependencies ?? {}))
189
+ for (const reached of scope(dependency.origin))
190
+ origins.add(reached);
191
+ scopes.set(origin, origins);
192
+ return origins;
193
+ };
194
+ for (const entry of candidate.closure)
195
+ scope(entry.origin);
196
+ return scopes;
197
+ }
198
+ function validateEdges(context, admitted) {
175
199
  for (const located of context.classes.values()) {
176
200
  if (!isCompiledEdge(located.value))
177
201
  continue;
@@ -194,7 +218,7 @@ function validateEdges(context) {
194
218
  }
195
219
  if (edge.orientation === 'undirected' &&
196
220
  !sameIncidentEndpoint(edge.endpoints[0], edge.endpoints[1]) &&
197
- undirectedOverlap(context, edge.endpoints)) {
221
+ undirectedOverlap(context, edge.endpoints, admitted.get(located.entry.origin))) {
198
222
  fail(`${located.pointer}/endpoints`, 'Unequal undirected endpoint contracts overlap.');
199
223
  }
200
224
  for (const [parentIndex, parentRef] of edge.extends.entries()) {
@@ -242,8 +266,9 @@ function sameIncidentEndpoint(left, right) {
242
266
  a.length === b.length &&
243
267
  a.every((value, index) => value === b[index]));
244
268
  }
245
- function undirectedOverlap(context, endpoints) {
246
- return [...context.classes.values()].some(({ value }) => value.kind === 'node' &&
269
+ function undirectedOverlap(context, endpoints, origins) {
270
+ return [...context.classes.values()].some(({ value }) => origins.has(value.ref.origin) &&
271
+ value.kind === 'node' &&
247
272
  !value.abstract &&
248
273
  classSatisfiesAny(context, value.ref, endpoints[0].accepts) &&
249
274
  classSatisfiesAny(context, value.ref, endpoints[1].accepts));
@@ -274,14 +299,14 @@ function validateViews(candidate, context) {
274
299
  }
275
300
  }
276
301
  }
277
- function validatePolicies(candidate, context) {
302
+ function validatePolicies(candidate, context, admitted) {
278
303
  for (const [entryIndex, entry] of candidate.closure.entries()) {
279
304
  const localPolicies = new Map(entry.definitions.policies.map((value) => [value.ref.name, value]));
280
305
  for (const [policyIndex, policy] of entry.definitions.policies.entries()) {
281
306
  validatePolicyExpression(context, entry, policy.expression, `/closure/${entryIndex}/definitions/policies/${policyIndex}/expression`, localPolicies);
282
307
  }
283
308
  validatePolicyCycles(entry, localPolicies, entryIndex);
284
- validatePolicyParity(context, entry, entryIndex, localPolicies);
309
+ validatePolicyParity(context, entry, entryIndex, localPolicies, admitted.get(entry.origin));
285
310
  for (const [functionIndex, callable] of entry.definitions.functions.entries()) {
286
311
  if (callable.policy !== undefined)
287
312
  validateCallablePolicy(context, entry, callable.policy, typeof callable.input === 'number' ? callable.input : callable.input.metadata, callable.fields, false, `/closure/${entryIndex}/definitions/functions/${functionIndex}/policy`);
@@ -455,9 +480,9 @@ function isNodeClassRef(context, value) {
455
480
  }
456
481
  return context.classes.get(Key.of(ref))?.value.kind === 'node';
457
482
  }
458
- function validatePolicyParity(context, entry, entryIndex, policies) {
483
+ function validatePolicyParity(context, entry, entryIndex, policies, origins) {
459
484
  const normalized = new Map([...policies].map(([name, policy]) => [name, normalizePolicyExpression(policy.expression)]));
460
- const typing = new CompiledPolicyTyping(context, entry, normalized);
485
+ const typing = new CompiledPolicyTyping(context, entry, normalized, origins);
461
486
  for (const [name, declaration] of policies) {
462
487
  const path = `/closure/${entryIndex}/definitions/policies/${declarationIndex(entry, 'policies', name)}`;
463
488
  if ('match' in declaration.expression) {
@@ -602,21 +627,21 @@ function nodePathAccepts(entry, input, field) {
602
627
  ? annotation.accepts
603
628
  : [];
604
629
  }
630
+ /** Types one entry's Policies against the Node Classes of its own closure, as source admission. */
605
631
  class CompiledPolicyTyping {
606
632
  context;
607
633
  entry;
608
634
  normalized;
609
635
  allNodeClasses;
610
636
  concreteNodeClasses;
611
- constructor(context, entry, normalized) {
637
+ constructor(context, entry, normalized, origins) {
612
638
  this.context = context;
613
639
  this.entry = entry;
614
640
  this.normalized = normalized;
615
- this.allNodeClasses = [...context.classes.values()]
616
- .filter(({ value }) => value.kind === 'node')
617
- .map(({ value }) => value.ref);
618
- this.concreteNodeClasses = [...context.classes.values()]
619
- .filter(({ value }) => value.kind === 'node' && !value.abstract)
641
+ const nodeClasses = [...context.classes.values()].filter(({ value }) => value.kind === 'node' && origins.has(value.ref.origin));
642
+ this.allNodeClasses = nodeClasses.map(({ value }) => value.ref);
643
+ this.concreteNodeClasses = nodeClasses
644
+ .filter(({ value }) => !value.abstract)
620
645
  .map(({ value }) => value.ref);
621
646
  }
622
647
  expandedBranches(name, visiting, namespace) {
@@ -125,6 +125,7 @@ function programValidation(programs, properties, propertyOrdering, callables) {
125
125
  propertyValidation.set(key, Object.freeze({
126
126
  ordering: propertyOrdering.get(key),
127
127
  validate: (value) => validateValueProgram(program, value),
128
+ program: () => program,
128
129
  }));
129
130
  }
130
131
  const callableValidation = new Map();
@@ -3,6 +3,7 @@ import type { DomainDependency, DomainDependencyMap } from '../../addressing/ind
3
3
  import type { CoreEdge } from '../../language/core/model/core.js';
4
4
  import type { Definition } from '../../language/definition.js';
5
5
  import type { Diagnostic } from '../../language/validation/diagnostic.js';
6
+ import type { ValueProgram } from '../../language/value/compile/model.js';
6
7
  import type { OrderingKind } from '../../language/value/ordering.js';
7
8
  /** One admitted Schema entry in a dependency-closed Domain universe. */
8
9
  export interface DomainEntryInput {
@@ -22,6 +23,8 @@ export interface DomainInput {
22
23
  export interface DomainValidation {
23
24
  propertyExists(key: PropertyKey): boolean;
24
25
  property(key: PropertyKey, input: unknown): readonly Diagnostic[];
26
+ /** The admitted program `property` evaluates. */
27
+ propertyProgram(key: PropertyKey): ValueProgram;
25
28
  propertyOrdering(key: PropertyKey): OrderingKind | undefined;
26
29
  callableExists(key: AddressKey.Callable): boolean;
27
30
  callableInput(key: AddressKey.Callable, input: unknown): readonly Diagnostic[];
@@ -45,6 +45,7 @@ export function createClasses(definitions, validation) {
45
45
  definition: propertyDefinition,
46
46
  ...(ordering === undefined ? {} : { ordering }),
47
47
  validate: (input) => validation.property(propertyKey, input),
48
+ program: () => validation.propertyProgram(propertyKey),
48
49
  });
49
50
  ownProperties.push(property);
50
51
  effectiveProperties.set(property.key, property);
@@ -1,10 +1,12 @@
1
1
  import type { Key, PropertyKey } from '../../addressing/index.js';
2
2
  import type { Diagnostic } from '../../language/validation/diagnostic.js';
3
+ import type { ValueProgram } from '../../language/value/compile/model.js';
3
4
  import type { OrderingKind } from '../../language/value/ordering.js';
4
5
  import type { DomainValidation } from '../model/input.js';
5
6
  export interface PropertyValidation {
6
7
  readonly ordering?: OrderingKind;
7
8
  validate(input: unknown): readonly Diagnostic[];
9
+ program(): ValueProgram;
8
10
  }
9
11
  export interface CallableValidation {
10
12
  readonly fields?: readonly string[];
@@ -6,6 +6,12 @@ export function createDomainValidation(properties, callables) {
6
6
  const target = properties.get(key);
7
7
  return target === undefined ? unknownTarget('Property', key) : target.validate(input);
8
8
  },
9
+ propertyProgram(key) {
10
+ const target = properties.get(key);
11
+ if (target === undefined)
12
+ throw new TypeError(`Property ${key} is absent.`);
13
+ return target.program();
14
+ },
9
15
  propertyOrdering: (key) => properties.get(key)?.ordering,
10
16
  callableExists: (key) => callables.has(key),
11
17
  callableInput(key, input) {
@@ -33,6 +39,12 @@ export function combineDomainValidation(values) {
33
39
  const owner = values.find((value) => value.propertyExists(key));
34
40
  return owner === undefined ? unknownTarget('Property', key) : owner.property(key, input);
35
41
  },
42
+ propertyProgram(key) {
43
+ const owner = values.find((value) => value.propertyExists(key));
44
+ if (owner === undefined)
45
+ throw new TypeError(`Property ${key} is absent.`);
46
+ return owner.propertyProgram(key);
47
+ },
36
48
  propertyOrdering: (key) => values.find((value) => value.propertyExists(key))?.propertyOrdering(key),
37
49
  callableExists: (key) => values.some((value) => value.callableExists(key)),
38
50
  callableInput(key, input) {
@@ -3,6 +3,7 @@ import type { ClassRef, DefinitionKey, Key, PropertyKey } from '../../addressing
3
3
  import type { ResolvedMethodCollection } from '../callable/resolved.js';
4
4
  import type { StateTopology } from '../state/index.js';
5
5
  import type { Diagnostic } from '../validation/diagnostic.js';
6
+ import type { ValueProgram } from '../value/compile/model.js';
6
7
  import type { OrderingKind } from '../value/ordering.js';
7
8
  import type { ClassDefinition, ClassKind } from './model/class.js';
8
9
  import type { DataDefinition } from './model/data.js';
@@ -19,6 +20,8 @@ export interface ResolvedProperty<out Value = PortableValue, out Required extend
19
20
  /** The one portable scalar ordering domain, when the Property is orderable. */
20
21
  readonly ordering?: OrderingKind;
21
22
  validate(input: unknown): readonly Diagnostic[];
23
+ /** The admitted program `validate` evaluates. */
24
+ program(): ValueProgram;
22
25
  readonly __value?: Value;
23
26
  readonly __input?: Input;
24
27
  }
@@ -4,6 +4,8 @@ export type { ClassRef, CoreRef, DefinitionBase, DefinitionKey, DefinitionRef, F
4
4
  export type { Value as PropertyValue } from '@astrale-os/kernel-dsl/value';
5
5
  export type { JsonSchema as ValueSchema, JsonSchemaObject as ValueSchemaObject, } from './value/model/schema.js';
6
6
  export type { OrderingKind } from './value/ordering.js';
7
+ export type { ValueProgram } from './value/compile/model.js';
8
+ export { valueProgramIncludes } from './value/inclusion/includes.js';
7
9
  export { stateMachine } from './state/index.js';
8
10
  export type { EventOf, StateMachine, StateOf, StateTopology } from './state/index.js';
9
11
  export type * from './callable/model/callable.js';
@@ -1,5 +1,6 @@
1
1
  export { isMethodExecutable } from './callable/executable.js';
2
2
  export { isBinaryInput } from './callable/model/callable.js';
3
+ export { valueProgramIncludes } from './value/inclusion/includes.js';
3
4
  export { stateMachine } from './state/index.js';
4
5
  export { PropertyValues } from './class/property-values.js';
5
6
  export { policyVariableClass, policyVariableSelector, policyObjectKey, policyObjectOrigin, projectedMemberClass, } from './policy/model/policy.js';
@@ -1,4 +1,8 @@
1
1
  import type { PolicyCheckExpression, PolicyExpression, PolicyPatternExpression } from '../model/policy.js';
2
2
  export declare function normalizePolicyExpression(value: PolicyExpression): PolicyExpression;
3
+ /**
4
+ * The fixed point of the single pass: a Pattern the single pass reproduces is returned unchanged,
5
+ * so the canonical form of every Pattern that was already stable keeps its bytes.
6
+ */
3
7
  export declare function normalizePolicyPattern(value: PolicyPatternExpression): PolicyPatternExpression;
4
8
  export declare function normalizePolicyChecks(value: PolicyCheckExpression): PolicyCheckExpression;
@@ -10,7 +10,32 @@ export function normalizePolicyExpression(value) {
10
10
  }
11
11
  return { anyOf: uniqueRefs(value.anyOf) };
12
12
  }
13
+ /**
14
+ * Passes after the first, while one still changes the Pattern. A pass numbers variables by the
15
+ * operands as written, then merges duplicate operands and flattens nested ones, so its result can
16
+ * number differently when normalized again; the next pass numbers that result by itself. One
17
+ * further pass reached the fixed point on every Pattern tested, the other is margin, and a
18
+ * Pattern still changing keeps its last pass rather than failing.
19
+ */
20
+ const SETTLING_PASSES = 2;
21
+ /**
22
+ * The fixed point of the single pass: a Pattern the single pass reproduces is returned unchanged,
23
+ * so the canonical form of every Pattern that was already stable keeps its bytes.
24
+ */
13
25
  export function normalizePolicyPattern(value) {
26
+ let current = normalizePatternOnce(value);
27
+ let encoding = json.serialize(current);
28
+ for (let pass = 0; pass < SETTLING_PASSES; pass++) {
29
+ const next = normalizePatternOnce(current);
30
+ const nextEncoding = json.serialize(next);
31
+ if (nextEncoding === encoding)
32
+ return current;
33
+ current = next;
34
+ encoding = nextEncoding;
35
+ }
36
+ return current;
37
+ }
38
+ function normalizePatternOnce(value) {
14
39
  const rewritten = rewriteVariables(value, canonicalVariableIds(value));
15
40
  return normalizePatternNode(rewritten);
16
41
  }
@@ -71,7 +96,8 @@ function normalizePatternNode(value) {
71
96
  }
72
97
  function canonicalVariableIds(value) {
73
98
  const declarations = new Map();
74
- collectDeclarations(value, declarations);
99
+ const depths = new Map();
100
+ collectDeclarations(value, declarations, depths);
75
101
  const uses = new Map([...declarations.keys()].map((id) => [id, []]));
76
102
  collectUses(value, uses);
77
103
  let colors = declarations;
@@ -89,8 +115,15 @@ function canonicalVariableIds(value) {
89
115
  break;
90
116
  colors = next;
91
117
  }
118
+ // Colors ignore where a variable is declared, so two variables can tie on color although
119
+ // different `exists` declare them, and which of them is numbered first decides which `exists`
120
+ // declares the smaller id. Their declaration depth tells them apart where it differs: operands of
121
+ // equal signature are ordered by their signature over depth-scoped colors before authored order,
122
+ // and the final order compares depth before the authored id (decision
123
+ // `policy-pattern-canonical-ties`). Both keys only order what color and first use leave tied.
124
+ const scoped = new Map([...colors].map(([id, color]) => [id, `${color}@${depths.get(id) ?? 0}`]));
92
125
  const firstUse = new Map();
93
- collectFirstUseKeys(value, colors, firstUse);
126
+ collectFirstUseKeys(value, colors, scoped, firstUse);
94
127
  const ordered = [...declarations.keys()].sort((left, right) => {
95
128
  const byColor = compareUtf8Bytes(colors.get(left) ?? '', colors.get(right) ?? '');
96
129
  if (byColor !== 0)
@@ -98,26 +131,31 @@ function canonicalVariableIds(value) {
98
131
  const byUse = compareUtf8Bytes(firstUse.get(left) ?? '', firstUse.get(right) ?? '');
99
132
  if (byUse !== 0)
100
133
  return byUse;
101
- // A remaining tie is an automorphic binder class: assigning either first
102
- // yields the same recursively sorted canonical Pattern.
134
+ const byDepth = (depths.get(left) ?? 0) - (depths.get(right) ?? 0);
135
+ if (byDepth !== 0)
136
+ return byDepth;
137
+ // A remaining tie keeps the authored order of the ids: such variables were interchangeable in
138
+ // every admissible Pattern checked (`__tests__/canonical.test.ts`).
103
139
  return left - right;
104
140
  });
105
141
  return new Map(ordered.map((id, index) => [id, index]));
106
142
  }
107
- function collectDeclarations(value, result) {
143
+ /** Each declared variable's selector signature and the number of `exists` around its declaration. */
144
+ function collectDeclarations(value, result, depths, depth = 0) {
108
145
  if ('exists' in value) {
109
146
  for (const node of value.exists.nodes) {
110
147
  result.set(node.variable.id, selectorSignature(node));
148
+ depths.set(node.variable.id, depth + 1);
111
149
  }
112
- collectDeclarations(value.exists.where, result);
150
+ collectDeclarations(value.exists.where, result, depths, depth + 1);
113
151
  return;
114
152
  }
115
153
  if ('allOf' in value)
116
154
  for (const child of value.allOf)
117
- collectDeclarations(child, result);
155
+ collectDeclarations(child, result, depths, depth);
118
156
  if ('anyOf' in value)
119
157
  for (const child of value.anyOf)
120
- collectDeclarations(child, result);
158
+ collectDeclarations(child, result, depths, depth);
121
159
  }
122
160
  function collectUses(value, result) {
123
161
  if ('sameNode' in value) {
@@ -155,7 +193,7 @@ function termColor(term, colors) {
155
193
  return `ref:${term.ref.kind === 'method' ? policyObjectKey(term.ref) : definitionRefIndex(term.ref)}`;
156
194
  return term.kind;
157
195
  }
158
- function collectFirstUseKeys(value, colors, result, prefix = '$') {
196
+ function collectFirstUseKeys(value, colors, scoped, result, prefix = '$') {
159
197
  if ('sameNode' in value) {
160
198
  for (const term of [value.sameNode.left, value.sameNode.right]) {
161
199
  if (term.kind !== 'variable')
@@ -182,15 +220,15 @@ function collectFirstUseKeys(value, colors, result, prefix = '$') {
182
220
  return;
183
221
  }
184
222
  if ('exists' in value) {
185
- collectFirstUseKeys(value.exists.where, colors, result, `${prefix}:exists`);
223
+ collectFirstUseKeys(value.exists.where, colors, scoped, result, `${prefix}:exists`);
186
224
  return;
187
225
  }
188
226
  const key = 'allOf' in value ? 'allOf' : 'anyOf';
189
227
  const operands = 'allOf' in value ? value.allOf : value.anyOf;
190
228
  const children = operands
191
- .map((child) => [patternSignature(child, colors), child])
192
- .sort(([left], [right]) => compareUtf8Bytes(left, right));
193
- children.forEach(([, child], index) => collectFirstUseKeys(child, colors, result, `${prefix}:${key}:${index}`));
229
+ .map((child) => [patternSignature(child, colors), patternSignature(child, scoped), child])
230
+ .sort(([left, leftScoped], [right, rightScoped]) => compareUtf8Bytes(left, right) || compareUtf8Bytes(leftScoped, rightScoped));
231
+ children.forEach(([, , child], index) => collectFirstUseKeys(child, colors, scoped, result, `${prefix}:${key}:${index}`));
194
232
  }
195
233
  function patternSignature(value, colors) {
196
234
  if ('sameNode' in value)
@@ -0,0 +1,9 @@
1
+ import type { ValueProgram } from '../compile/model.js';
2
+ /**
3
+ * Whether `outer` admits every value `inner` admits, under the one V1 evaluator.
4
+ *
5
+ * `true` is a proof. `false` only means that no rule proved inclusion: a caller must then decide
6
+ * the values themselves. Each rule relates one assertion of `outer` to evidence carried by
7
+ * `inner`; a finite `inner` is decided exactly by evaluating its candidates.
8
+ */
9
+ export declare function valueProgramIncludes(outer: ValueProgram, inner: ValueProgram): boolean;