@astrale-os/kernel-dsl 0.2.0-beta.29 → 0.2.0-beta.30

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.
@@ -14,6 +14,7 @@ declare const CALLABLE_OUTPUT: unique symbol;
14
14
  declare const CALLABLE_PROJECTION: unique symbol;
15
15
  declare const CALLABLE_PROJECTION_VALUE: unique symbol;
16
16
  export interface CallablePolicyContext {
17
+ sameNode(left: PolicyCheckObject, right: PolicyCheckObject): PolicyCheckExpression;
17
18
  readonly input: Readonly<Record<string, Extract<PolicyCheckObject, {
18
19
  readonly kind: 'input';
19
20
  }>>>;
@@ -70,34 +70,38 @@ export class V1CallableCompiler {
70
70
  },
71
71
  });
72
72
  const self = callableSelf();
73
- return Object.freeze({
74
- input,
75
- self,
76
- ref: (member) => callableRef(this.context.resolvePolicyObject(member, `${placement.pointer}/policy/ref`)),
77
- check: (policy, object) => {
78
- const resolvedObject = callablePolicyObject(object);
79
- let admittedObject = resolvedObject;
80
- if (resolvedObject.kind === 'self') {
81
- if (placement.kind === 'function' || isStatic) {
82
- fail('BUILDER_POLICY_SELF', `${placement.pointer}/policy`, 'self is available only to a non-static Node Method.');
83
- }
73
+ const admitObject = (object) => {
74
+ const resolvedObject = callablePolicyObject(object);
75
+ let admittedObject = resolvedObject;
76
+ if (resolvedObject.kind === 'self') {
77
+ if (placement.kind === 'function' || isStatic) {
78
+ fail('BUILDER_POLICY_SELF', `${placement.pointer}/policy`, 'self is available only to a non-static Node Method.');
84
79
  }
85
- else if (resolvedObject.kind === 'input') {
86
- if (isRequiredDirectNodePath(inputSchema, resolvedObject.field)) {
87
- // The field contract is admitted below as-is.
88
- }
89
- else {
90
- fail('BUILDER_POLICY_INPUT', `${placement.pointer}/policy`, `input.${resolvedObject.field} is not one direct required single-Node path field.`);
91
- }
80
+ }
81
+ else if (resolvedObject.kind === 'input') {
82
+ if (isRequiredDirectNodePath(inputSchema, resolvedObject.field)) {
83
+ // The field contract is admitted below as-is.
92
84
  }
93
85
  else {
94
- admittedObject = callableRef(this.context.resolvePolicyObject(resolvedObject.ref, `${placement.pointer}/policy/ref`));
86
+ fail('BUILDER_POLICY_INPUT', `${placement.pointer}/policy`, `input.${resolvedObject.field} is not one direct required single-Node path field.`);
95
87
  }
96
- return {
97
- check: this.context.resolvePolicy(policy, `${placement.pointer}/policy/check`),
98
- object: admittedObject,
99
- };
100
- },
88
+ }
89
+ else {
90
+ admittedObject = callableRef(this.context.resolvePolicyObject(resolvedObject.ref, `${placement.pointer}/policy/ref`));
91
+ }
92
+ return admittedObject;
93
+ };
94
+ return Object.freeze({
95
+ input,
96
+ self,
97
+ ref: (member) => callableRef(this.context.resolvePolicyObject(member, `${placement.pointer}/policy/ref`)),
98
+ sameNode: (left, right) => ({
99
+ sameNode: { left: admitObject(left), right: admitObject(right) },
100
+ }),
101
+ check: (policy, object) => ({
102
+ check: this.context.resolvePolicy(policy, `${placement.pointer}/policy/check`),
103
+ object: admitObject(object),
104
+ }),
101
105
  allOf: (...expressions) => {
102
106
  nonEmpty(expressions, `${placement.pointer}/policy/allOf`);
103
107
  return { allOf: expressions };
@@ -20,6 +20,7 @@ export class V1PolicyCompiler {
20
20
  };
21
21
  }
22
22
  const match = input.match(createPatternContext({
23
+ resolveObject: (member, refPointer) => this.context.resolvePolicyObject(member, refPointer),
23
24
  resolveEdgeClass: (definition, refPointer) => this.context.resolveEdgeClass(definition, refPointer),
24
25
  resolveNodeClass: (definition, refPointer) => this.context.resolveNodeClass(definition, refPointer),
25
26
  pointer,
@@ -1,5 +1,6 @@
1
1
  import type { ClassRef } from '../../addressing/index.js';
2
- import type { PolicyCheckExpression, PolicyPatternExpression, PolicyPatternTerm as CanonicalPolicyPatternTerm } from '../../language/policy/model/policy.js';
2
+ import type { PolicyCheckExpression, PolicyPatternExpression, PolicyPatternTerm as CanonicalPolicyPatternTerm, PolicyObjectRef } from '../../language/policy/model/policy.js';
3
+ import type { PolicyObjectInput } from '../callable/authoring.js';
3
4
  import type { EdgeClass, NodeClass } from '../class/authoring.js';
4
5
  import type { AuthoringValue } from '../schema/handle.js';
5
6
  import type { Lazy } from '../schema/lazy.js';
@@ -21,6 +22,8 @@ export interface PolicyExistsContext {
21
22
  readonly node: PolicyNodeBinder;
22
23
  }
23
24
  export interface PolicyPatternContext {
25
+ ref(member: PolicyObjectInput): CanonicalPolicyPatternTerm;
26
+ sameNode(left: CanonicalPolicyPatternTerm, right: CanonicalPolicyPatternTerm): PolicyPatternExpression;
24
27
  readonly subject: CanonicalPolicyPatternTerm;
25
28
  readonly object: CanonicalPolicyPatternTerm;
26
29
  readonly source: CanonicalPolicyPatternTerm;
@@ -50,6 +53,7 @@ export interface PolicyBuilder {
50
53
  export declare function policyDeclaration(value: Policy): PolicyDeclaration;
51
54
  export declare const policy: PolicyBuilder;
52
55
  export declare function createPatternContext(options: {
56
+ readonly resolveObject: (value: PolicyObjectInput, pointer: string) => PolicyObjectRef;
53
57
  readonly resolveEdgeClass: (value: Lazy<EdgeClass>, pointer: string) => ClassRef;
54
58
  readonly resolveNodeClass: (value: Lazy<NodeClass>, pointer: string) => ClassRef;
55
59
  readonly pointer: string;
@@ -32,6 +32,11 @@ export function createPatternContext(options) {
32
32
  const source = Object.freeze({ kind: 'source' });
33
33
  const target = Object.freeze({ kind: 'target' });
34
34
  const context = {
35
+ ref: (member) => ({
36
+ kind: 'ref',
37
+ ref: options.resolveObject(member, `${options.pointer}/match/ref`),
38
+ }),
39
+ sameNode: (left, right) => ({ sameNode: { left, right } }),
35
40
  subject,
36
41
  object,
37
42
  source,
@@ -421,6 +421,14 @@ function policyPattern(input, depth, path) {
421
421
  if (depth > 64)
422
422
  invalid('Policy pattern depth', undefined, path);
423
423
  const value = object(input, 'Policy pattern', path);
424
+ if (Object.hasOwn(value, 'sameNode')) {
425
+ exact(value, ['sameNode'], 'Policy equality', path);
426
+ const pair = object(value.sameNode, 'Policy equality', `${path}/sameNode`);
427
+ exact(pair, ['left', 'right'], 'Policy equality operands', `${path}/sameNode`);
428
+ policyTerm(pair.left, false, `${path}/sameNode/left`);
429
+ policyTerm(pair.right, false, `${path}/sameNode/right`);
430
+ return;
431
+ }
424
432
  if (Object.hasOwn(value, 'exists')) {
425
433
  exact(value, ['exists'], 'Policy exists', path);
426
434
  const existsPath = `${path}/exists`;
@@ -484,6 +492,11 @@ function policyPattern(input, depth, path) {
484
492
  }
485
493
  function policyTerm(input, variableOnly, path) {
486
494
  const value = object(input, 'Policy pattern term', path);
495
+ if (!variableOnly && value.kind === 'ref') {
496
+ exact(value, ['kind', 'ref'], 'Policy reference term', path);
497
+ definitionRef(value.ref, undefined, 'Policy reference term', `${path}/ref`);
498
+ return;
499
+ }
487
500
  if (value.kind === 'variable') {
488
501
  exact(value, ['kind', 'id'], 'Policy variable term', path);
489
502
  integer(value.id, 'Policy variable id', `${path}/id`);
@@ -502,23 +515,18 @@ function policyCheck(input, depth, path) {
502
515
  if (depth > 64)
503
516
  invalid('Callable Policy depth', undefined, path);
504
517
  const value = object(input, 'Callable Policy', path);
518
+ if (Object.hasOwn(value, 'sameNode')) {
519
+ exact(value, ['sameNode'], 'Callable equality', path);
520
+ const pair = object(value.sameNode, 'Callable equality', `${path}/sameNode`);
521
+ exact(pair, ['left', 'right'], 'Callable equality operands', `${path}/sameNode`);
522
+ policyCheckObject(pair.left, `${path}/sameNode/left`);
523
+ policyCheckObject(pair.right, `${path}/sameNode/right`);
524
+ return;
525
+ }
505
526
  if (Object.hasOwn(value, 'check')) {
506
527
  exact(value, ['check', 'object'], 'Callable Policy check', path);
507
528
  definitionRef(value.check, 'policy', 'Callable Policy ref', `${path}/check`);
508
- const objectPath = `${path}/object`;
509
- const target = object(value.object, 'Callable Policy object', objectPath);
510
- if (target.kind === 'self')
511
- exact(target, ['kind'], 'Callable Policy self', objectPath);
512
- else if (target.kind === 'input') {
513
- exact(target, ['kind', 'field'], 'Callable Policy input', objectPath);
514
- memberName(target.field, 'Callable Policy input field', `${objectPath}/field`);
515
- }
516
- else if (target.kind === 'ref') {
517
- exact(target, ['kind', 'ref'], 'Callable Policy object ref', objectPath);
518
- definitionRef(target.ref, undefined, 'Callable Policy object ref', `${objectPath}/ref`);
519
- }
520
- else
521
- invalid('Callable Policy object', undefined, `${objectPath}/kind`);
529
+ policyCheckObject(value.object, `${path}/object`);
522
530
  return;
523
531
  }
524
532
  const member = Object.hasOwn(value, 'allOf') ? 'allOf' : 'anyOf';
@@ -530,6 +538,21 @@ function policyCheck(input, depth, path) {
530
538
  for (const [index, expression] of expressions.entries())
531
539
  policyCheck(expression, depth + 1, `${memberPath}/${index}`);
532
540
  }
541
+ function policyCheckObject(input, objectPath) {
542
+ const target = object(input, 'Callable Policy object', objectPath);
543
+ if (target.kind === 'self')
544
+ exact(target, ['kind'], 'Callable Policy self', objectPath);
545
+ else if (target.kind === 'input') {
546
+ exact(target, ['kind', 'field'], 'Callable Policy input', objectPath);
547
+ memberName(target.field, 'Callable Policy input field', `${objectPath}/field`);
548
+ }
549
+ else if (target.kind === 'ref') {
550
+ exact(target, ['kind', 'ref'], 'Callable Policy object ref', objectPath);
551
+ definitionRef(target.ref, undefined, 'Callable Policy object ref', `${objectPath}/ref`);
552
+ }
553
+ else
554
+ invalid('Callable Policy object', undefined, `${objectPath}/kind`);
555
+ }
533
556
  function coreEndpoint(input, label, path) {
534
557
  const value = object(input, label, path);
535
558
  if (value.kind === 'domain')
@@ -347,6 +347,11 @@ function validatePolicyExpression(context, entry, expression, path, local) {
347
347
  }
348
348
  }
349
349
  function validatePattern(context, expression, path, scope, declared) {
350
+ if ('sameNode' in expression) {
351
+ validatePolicyTerm(expression.sameNode.left, `${path}/sameNode/left`, scope);
352
+ validatePolicyTerm(expression.sameNode.right, `${path}/sameNode/right`, scope);
353
+ return;
354
+ }
350
355
  if ('source' in expression) {
351
356
  if (context.classes.get(Key.of(expression.class))?.value.kind !== 'edge')
352
357
  fail(`${path}/class`, 'Policy pattern Class must resolve to an Edge Class.');
@@ -410,21 +415,22 @@ function validatePolicyCycles(entry, local, entryIndex) {
410
415
  visit(name);
411
416
  }
412
417
  function validateCallablePolicy(context, entry, expression, inputIndex, fields, instanceMethod, path) {
413
- if ('check' in expression) {
414
- if (expression.check.origin !== entry.origin ||
415
- context.policies.get(Key.of(expression.check)) === undefined)
418
+ if ('check' in expression || 'sameNode' in expression) {
419
+ if ('check' in expression &&
420
+ (expression.check.origin !== entry.origin ||
421
+ context.policies.get(Key.of(expression.check)) === undefined))
416
422
  fail(`${path}/check`, 'Callable Policy must reference a local named Policy.');
417
- const object = expression.object;
418
- if (object.kind === 'self' && !instanceMethod)
419
- fail(`${path}/object`, 'self is valid only for an instance Method.');
420
- if (object.kind === 'input') {
421
- if (fields === undefined ||
422
- fields[object.field] === undefined ||
423
- !isSingleNodePathField(context, entry, inputIndex, object.field))
424
- fail(`${path}/object`, 'Callable Policy input target must be a direct required single-Node Path field.');
423
+ const objects = 'check' in expression ? { object: expression.object } : expression.sameNode;
424
+ for (const [side, object] of Object.entries(objects)) {
425
+ if (object.kind === 'self' && !instanceMethod)
426
+ fail(`${path}/${side}`, 'self is valid only for an instance Method.');
427
+ if (object.kind === 'input' &&
428
+ (fields === undefined ||
429
+ fields[object.field] === undefined ||
430
+ !isSingleNodePathField(context, entry, inputIndex, object.field))) {
431
+ fail(`${path}/${side}`, 'Callable Policy input target must be a direct required single-Node Path field.');
432
+ }
425
433
  }
426
- // validateClosure already establishes that object refs name existing local
427
- // or exact direct-dependency Definitions. Named Policy execution stays local.
428
434
  return;
429
435
  }
430
436
  const children = 'allOf' in expression ? expression.allOf : expression.anyOf;
@@ -555,6 +561,8 @@ function validateCallablePolicyTyping(context, typing, entry, expression, input,
555
561
  fail(path, 'Callable Policy checks exceed the V1 depth, branch, or leaf budget.');
556
562
  }
557
563
  const visit = (value, pointer) => {
564
+ if ('sameNode' in value)
565
+ return;
558
566
  if (!('check' in value)) {
559
567
  const children = 'allOf' in value ? value.allOf : value.anyOf;
560
568
  children.forEach((child, index) => visit(child, `${pointer}/${index}`));
@@ -668,11 +676,15 @@ class CompiledPolicyTyping {
668
676
  const terms = new Set(use.kind === 'object' ? ['subject', 'object'] : ['subject', 'source', 'target']);
669
677
  for (const key of branch.variables.keys())
670
678
  terms.add(key);
679
+ for (const key of branch.terms.keys())
680
+ terms.add(key);
671
681
  for (const edge of branch.edges) {
672
682
  terms.add(termKey(edge.pattern.source, edge.namespace));
673
683
  terms.add(termKey(edge.pattern.target, edge.namespace));
674
684
  }
675
685
  const union = new CompiledUnionFind(terms);
686
+ for (const [left, right] of branch.equalities)
687
+ union.join(left, right);
676
688
  for (const variant of variants)
677
689
  for (const [left, right] of variant.equalities)
678
690
  union.join(left, right);
@@ -716,6 +728,11 @@ class CompiledPolicyTyping {
716
728
  ? this.applyExact(candidates, contract.ref)
717
729
  : uniqueCompiledCandidates(this.allNodeClasses.flatMap((ref) => this.applyExact(candidates, ref)));
718
730
  }
731
+ const referenced = branch.terms.get(term);
732
+ if (referenced?.kind === 'ref') {
733
+ const contract = callableObjectContracts(this.context, this.entry, 0, undefined, referenced)?.[0];
734
+ return contract?.kind === 'exact' ? this.applyExact(candidates, contract.ref) : [];
735
+ }
719
736
  const selector = branch.variables.get(term);
720
737
  if (selector === undefined)
721
738
  return [];
@@ -870,8 +887,25 @@ function measureCompiledPolicy(origin, name, normalized, visiting) {
870
887
  return combineCompiledMeasures('allOf' in expression ? 'allOf' : 'anyOf', children);
871
888
  }
872
889
  function measureCompiledPattern(pattern, origin) {
890
+ if ('sameNode' in pattern)
891
+ return {
892
+ depth: 1,
893
+ edgeCounts: [1],
894
+ domains: new Set([
895
+ origin,
896
+ ...[pattern.sameNode.left, pattern.sameNode.right].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
897
+ ]),
898
+ };
873
899
  if ('source' in pattern)
874
- return { depth: 1, edgeCounts: [1], domains: new Set([origin, pattern.class.origin]) };
900
+ return {
901
+ depth: 1,
902
+ edgeCounts: [1],
903
+ domains: new Set([
904
+ origin,
905
+ pattern.class.origin,
906
+ ...[pattern.source, pattern.target].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
907
+ ]),
908
+ };
875
909
  if ('exists' in pattern) {
876
910
  const child = measureCompiledPattern(pattern.exists.where, origin);
877
911
  return {
@@ -1,5 +1,7 @@
1
1
  import type { PolicyEdgePattern, PolicyPatternExpression, PolicyPatternTerm, PolicyVariableSelector } from '../model/policy.js';
2
2
  export interface PatternBranch {
3
+ readonly terms: ReadonlyMap<string, PolicyPatternTerm>;
4
+ readonly equalities: readonly (readonly [string, string])[];
3
5
  readonly edges: readonly PatternEdgeUse[];
4
6
  readonly variables: ReadonlyMap<string, PolicyVariableSelector>;
5
7
  readonly requiredVariables: ReadonlySet<string>;
@@ -1,3 +1,4 @@
1
+ import { definitionRefIndex } from '../../../addressing/ordering.js';
1
2
  import { policyVariableSelector } from '../model/policy.js';
2
3
  import { BRANCH_OVERFLOW } from './budgets.js';
3
4
  export function branchesOf(pattern, namespace) {
@@ -26,14 +27,7 @@ export function isConnected(branch) {
26
27
  const mode = targetMode(branch);
27
28
  if (mode === undefined)
28
29
  return false;
29
- const vertices = new Set(['subject']);
30
- for (const edge of branch.edges) {
31
- for (const term of [edge.pattern.source, edge.pattern.target]) {
32
- if (term.kind === 'object' || term.kind === 'source' || term.kind === 'target') {
33
- vertices.add(term.kind);
34
- }
35
- }
36
- }
30
+ const vertices = new Set(['subject', ...branch.terms.keys()]);
37
31
  for (const variable of branch.requiredVariables)
38
32
  vertices.add(variable);
39
33
  const union = new UnionFind(vertices);
@@ -44,6 +38,8 @@ export function isConnected(branch) {
44
38
  union.add(target);
45
39
  union.join(source, target);
46
40
  }
41
+ for (const [left, right] of branch.equalities)
42
+ union.join(left, right);
47
43
  const root = union.find('subject');
48
44
  return [...vertices].every((vertex) => union.find(vertex) === root);
49
45
  }
@@ -51,12 +47,10 @@ export function targetMode(branch) {
51
47
  let object = false;
52
48
  let source = false;
53
49
  let target = false;
54
- for (const edge of branch.edges) {
55
- for (const term of [edge.pattern.source, edge.pattern.target]) {
56
- object ||= term.kind === 'object';
57
- source ||= term.kind === 'source';
58
- target ||= term.kind === 'target';
59
- }
50
+ for (const term of branch.terms.values()) {
51
+ object ||= term.kind === 'object';
52
+ source ||= term.kind === 'source';
53
+ target ||= term.kind === 'target';
60
54
  }
61
55
  if (object && !source && !target)
62
56
  return 'object';
@@ -65,11 +59,42 @@ export function targetMode(branch) {
65
59
  return undefined;
66
60
  }
67
61
  export function termKey(term, namespace) {
68
- return term.kind === 'variable' ? variableKey(namespace, term.id) : term.kind;
62
+ return term.kind === 'variable'
63
+ ? variableKey(namespace, term.id)
64
+ : term.kind === 'ref'
65
+ ? `ref:${definitionRefIndex(term.ref)}`
66
+ : term.kind;
69
67
  }
70
68
  function branchesForNode(pattern, namespace, variables) {
71
69
  if ('source' in pattern) {
72
- return [{ edges: [{ pattern, namespace }], variables, requiredVariables: new Set(), namespace }];
70
+ return [
71
+ {
72
+ terms: new Map([pattern.source, pattern.target].map((term) => [termKey(term, namespace), term])),
73
+ equalities: [],
74
+ edges: [{ pattern, namespace }],
75
+ variables,
76
+ requiredVariables: new Set(),
77
+ namespace,
78
+ },
79
+ ];
80
+ }
81
+ if ('sameNode' in pattern) {
82
+ const terms = new Map([pattern.sameNode.left, pattern.sameNode.right].map((term) => [
83
+ termKey(term, namespace),
84
+ term,
85
+ ]));
86
+ return [
87
+ {
88
+ terms,
89
+ equalities: [
90
+ [termKey(pattern.sameNode.left, namespace), termKey(pattern.sameNode.right, namespace)],
91
+ ],
92
+ edges: [],
93
+ variables,
94
+ requiredVariables: new Set(),
95
+ namespace,
96
+ },
97
+ ];
73
98
  }
74
99
  if ('exists' in pattern) {
75
100
  const direct = new Set(pattern.exists.nodes.map((node) => variableKey(namespace, node.variable.id)));
@@ -82,10 +107,19 @@ function branchesForNode(pattern, namespace, variables) {
82
107
  return 'allOf' in pattern ? branchProduct(children) : capBranches(children.flat());
83
108
  }
84
109
  function emptyBranch() {
85
- return { edges: [], variables: new Map(), requiredVariables: new Set(), namespace: '$' };
110
+ return {
111
+ terms: new Map(),
112
+ equalities: [],
113
+ edges: [],
114
+ variables: new Map(),
115
+ requiredVariables: new Set(),
116
+ namespace: '$',
117
+ };
86
118
  }
87
119
  function mergeBranches(left, right) {
88
120
  return {
121
+ terms: new Map([...left.terms, ...right.terms]),
122
+ equalities: [...left.equalities, ...right.equalities],
89
123
  edges: [...left.edges, ...right.edges],
90
124
  variables: new Map([...left.variables, ...right.variables]),
91
125
  requiredVariables: new Set([...left.requiredVariables, ...right.requiredVariables]),
@@ -8,7 +8,7 @@ export const MAX_CHECK_DEPTH = 4;
8
8
  export const MAX_CHECK_LEAVES = 8;
9
9
  export const BRANCH_OVERFLOW = MAX_BRANCHES + 1;
10
10
  export function measureChecks(expression) {
11
- if ('check' in expression)
11
+ if ('check' in expression || 'sameNode' in expression)
12
12
  return { depth: 1, branches: 1, leaves: 1 };
13
13
  const children = ('allOf' in expression ? expression.allOf : expression.anyOf).map(measureChecks);
14
14
  const branches = 'allOf' in expression
@@ -10,7 +10,17 @@ export type PolicyPatternTerm = {
10
10
  } | {
11
11
  readonly kind: 'variable';
12
12
  readonly id: number;
13
+ } | {
14
+ readonly kind: 'ref';
15
+ readonly ref: PolicyObjectRef;
13
16
  };
17
+ /** Identity equality; both terms must resolve to one valid Node. */
18
+ export interface PolicySameNode<Term> {
19
+ readonly sameNode: {
20
+ readonly left: Term;
21
+ readonly right: Term;
22
+ };
23
+ }
14
24
  export interface ExactPolicyVariable {
15
25
  readonly variable: {
16
26
  readonly kind: 'variable';
@@ -53,7 +63,7 @@ export interface PolicyEdgePattern {
53
63
  readonly target: PolicyPatternTerm;
54
64
  readonly repeat?: PolicyRepeat;
55
65
  }
56
- export type PolicyPatternExpression = PolicyEdgePattern | {
66
+ export type PolicyPatternExpression = PolicyEdgePattern | PolicySameNode<PolicyPatternTerm> | {
57
67
  readonly exists: {
58
68
  readonly nodes: readonly PolicyVariable[];
59
69
  readonly where: PolicyPatternExpression;
@@ -89,7 +99,7 @@ export type PolicyCheckObject = {
89
99
  export type PolicyCheckExpression<Policy = PolicyRef, Object = PolicyCheckObject> = {
90
100
  readonly check: Policy;
91
101
  readonly object: Object;
92
- } | {
102
+ } | PolicySameNode<Object> | {
93
103
  readonly allOf: readonly [
94
104
  PolicyCheckExpression<Policy, Object>,
95
105
  ...PolicyCheckExpression<Policy, Object>[]
@@ -18,6 +18,8 @@ export function normalizePolicyPattern(value) {
18
18
  export function normalizePolicyChecks(value) {
19
19
  if ('check' in value)
20
20
  return value;
21
+ if ('sameNode' in value)
22
+ return { sameNode: normalizeEquality(value.sameNode) };
21
23
  if ('allOf' in value) {
22
24
  const children = value.allOf.flatMap((child) => {
23
25
  const normalized = normalizePolicyChecks(child);
@@ -37,6 +39,8 @@ function policyCheckComposition(values) {
37
39
  return values;
38
40
  }
39
41
  function normalizePatternNode(value) {
42
+ if ('sameNode' in value)
43
+ return { sameNode: normalizeEquality(value.sameNode) };
40
44
  if (isEdge(value)) {
41
45
  const repeat = value.repeat;
42
46
  if (repeat?.min === 1 && repeat.max === 1) {
@@ -115,6 +119,14 @@ function collectDeclarations(value, result) {
115
119
  collectDeclarations(child, result);
116
120
  }
117
121
  function collectUses(value, target, colors, result) {
122
+ if ('sameNode' in value) {
123
+ const { left, right } = value.sameNode;
124
+ if (left.kind === 'variable' && left.id === target)
125
+ result.push(`same:${termColor(right, colors)}`);
126
+ if (right.kind === 'variable' && right.id === target)
127
+ result.push(`same:${termColor(left, colors)}`);
128
+ return;
129
+ }
118
130
  if (isEdge(value)) {
119
131
  if (value.source.kind === 'variable' && value.source.id === target) {
120
132
  result.push(`s:${definitionRefIndex(value.class)}:${termColor(value.target, colors)}`);
@@ -133,9 +145,22 @@ function collectUses(value, target, colors, result) {
133
145
  function termColor(term, colors) {
134
146
  if (term.kind === 'variable')
135
147
  return `variable:${colors.get(term.id) ?? '?'}`;
148
+ if (term.kind === 'ref')
149
+ return `ref:${definitionRefIndex(term.ref)}`;
136
150
  return term.kind;
137
151
  }
138
152
  function collectFirstUseKeys(value, colors, result, prefix = '$') {
153
+ if ('sameNode' in value) {
154
+ for (const term of [value.sameNode.left, value.sameNode.right]) {
155
+ if (term.kind !== 'variable')
156
+ continue;
157
+ const key = `${prefix}:same`;
158
+ const current = result.get(term.id);
159
+ if (current === undefined || key < current)
160
+ result.set(term.id, key);
161
+ }
162
+ return;
163
+ }
139
164
  if (isEdge(value)) {
140
165
  for (const [position, term] of [
141
166
  ['source', value.source],
@@ -160,6 +185,8 @@ function collectFirstUseKeys(value, colors, result, prefix = '$') {
160
185
  children.forEach((child, index) => collectFirstUseKeys(child, colors, result, `${prefix}:${key}:${index}`));
161
186
  }
162
187
  function patternSignature(value, colors) {
188
+ if ('sameNode' in value)
189
+ return `same:${[termColor(value.sameNode.left, colors), termColor(value.sameNode.right, colors)].sort(compareUtf8Bytes).join(':')}`;
163
190
  if (isEdge(value)) {
164
191
  return `edge:${definitionRefIndex(value.class)}:${termColor(value.source, colors)}:${termColor(value.target, colors)}:${value.repeat?.min ?? 1}:${value.repeat?.max ?? 1}`;
165
192
  }
@@ -177,6 +204,13 @@ function patternSignature(value, colors) {
177
204
  .join('|')}`;
178
205
  }
179
206
  function rewriteVariables(value, ids) {
207
+ if ('sameNode' in value)
208
+ return {
209
+ sameNode: {
210
+ left: rewriteTerm(value.sameNode.left, ids),
211
+ right: rewriteTerm(value.sameNode.right, ids),
212
+ },
213
+ };
180
214
  if (isEdge(value)) {
181
215
  return {
182
216
  ...value,
@@ -234,3 +268,8 @@ function compareUtf8Bytes(left, right) {
234
268
  function isEdge(value) {
235
269
  return 'source' in value;
236
270
  }
271
+ function normalizeEquality(value) {
272
+ return compareUtf8Bytes(json.serialize(value.left), json.serialize(value.right)) <= 0
273
+ ? value
274
+ : { left: value.right, right: value.left };
275
+ }
@@ -79,7 +79,16 @@ function collectPolicyExpressionRefs(expression, add) {
79
79
  refs.forEach(add);
80
80
  }
81
81
  function collectPolicyPatternRefs(expression, add) {
82
+ if ('sameNode' in expression) {
83
+ for (const term of [expression.sameNode.left, expression.sameNode.right])
84
+ if (term.kind === 'ref')
85
+ add(term.ref);
86
+ return;
87
+ }
82
88
  if ('source' in expression) {
89
+ for (const term of [expression.source, expression.target])
90
+ if (term.kind === 'ref')
91
+ add(term.ref);
83
92
  add(expression.class);
84
93
  return;
85
94
  }
@@ -96,6 +105,12 @@ function collectPolicyPatternRefs(expression, add) {
96
105
  children.forEach((child) => collectPolicyPatternRefs(child, add));
97
106
  }
98
107
  function collectPolicyCheckRefs(expression, add) {
108
+ if ('sameNode' in expression) {
109
+ for (const term of [expression.sameNode.left, expression.sameNode.right])
110
+ if (term.kind === 'ref')
111
+ add(term.ref);
112
+ return;
113
+ }
99
114
  if ('check' in expression) {
100
115
  add(expression.check);
101
116
  if (expression.object.kind === 'ref')
@@ -101,6 +101,7 @@ const policyPatternNodeSchema = discriminatedUnion('kind', [
101
101
  policyPatternSourceSchema,
102
102
  policyPatternTargetSchema,
103
103
  policyPatternVariableSchema,
104
+ strictObject({ kind: literal('ref'), ref: policyObjectRefSchema }),
104
105
  ]);
105
106
  const policyVariableSelectorSchema = discriminatedUnion('kind', [
106
107
  strictObject({ kind: literal('any') }),
@@ -115,6 +116,9 @@ const policyRepeatSchema = strictObject({
115
116
  max: number().int().min(0).max(3),
116
117
  });
117
118
  const policyPatternExpressionSchema = lazy(() => union([
119
+ strictObject({
120
+ sameNode: strictObject({ left: policyPatternNodeSchema, right: policyPatternNodeSchema }),
121
+ }),
118
122
  strictObject({
119
123
  source: policyPatternNodeSchema,
120
124
  class: classRefSchema,
@@ -146,6 +150,9 @@ const policyCheckObjectSchema = discriminatedUnion('kind', [
146
150
  }),
147
151
  ]);
148
152
  const policyCheckExpressionSchema = lazy(() => union([
153
+ strictObject({
154
+ sameNode: strictObject({ left: policyCheckObjectSchema, right: policyCheckObjectSchema }),
155
+ }),
149
156
  strictObject({
150
157
  check: policyRefSchema,
151
158
  object: policyCheckObjectSchema,
@@ -16,7 +16,7 @@ export function validatePolicyBudgets(context) {
16
16
  }
17
17
  const edgeMaximum = Math.max(0, ...measure.edgeCounts);
18
18
  if (edgeMaximum > MAX_EDGES) {
19
- context.add('PL_BUDGET', pointer, `A normalized branch contains ${edgeMaximum} Edge predicates; the maximum is ${MAX_EDGES}.`);
19
+ context.add('PL_BUDGET', pointer, `A normalized branch contains ${edgeMaximum} graph predicates; the maximum is ${MAX_EDGES}.`);
20
20
  }
21
21
  if (measure.domains.size > MAX_DOMAINS) {
22
22
  context.add('PL_BUDGET', pointer, `Expanded Pattern references ${measure.domains.size} Domains; the maximum is ${MAX_DOMAINS}.`);
@@ -46,11 +46,24 @@ function measurePolicy(context, name, visiting) {
46
46
  return combineMeasures('allOf' in declaration ? 'allOf' : 'anyOf', children);
47
47
  }
48
48
  function measurePattern(pattern, ownOrigin) {
49
+ if ('sameNode' in pattern)
50
+ return {
51
+ depth: 1,
52
+ edgeCounts: [1],
53
+ domains: new Set([
54
+ ownOrigin,
55
+ ...[pattern.sameNode.left, pattern.sameNode.right].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
56
+ ]),
57
+ };
49
58
  if (isEdge(pattern)) {
50
59
  return {
51
60
  depth: 1,
52
61
  edgeCounts: [1],
53
- domains: new Set([ownOrigin, pattern.class.origin]),
62
+ domains: new Set([
63
+ ownOrigin,
64
+ pattern.class.origin,
65
+ ...[pattern.source, pattern.target].flatMap((term) => term.kind === 'ref' ? [term.ref.origin] : []),
66
+ ]),
54
67
  };
55
68
  }
56
69
  if ('exists' in pattern) {
@@ -0,0 +1,4 @@
1
+ import type { PolicyObjectRef } from '../../../language/policy/model/policy.js';
2
+ import type { PolicyValidationContext } from './context.js';
3
+ import type { ObjectContract } from './typing.js';
4
+ export declare function referenceContracts(validation: PolicyValidationContext, ref: PolicyObjectRef, pointer: string): readonly ObjectContract[] | undefined;
@@ -0,0 +1,64 @@
1
+ import { Key } from '../../../addressing/index.js';
2
+ import { definitionRefIndex } from '../../../addressing/ordering.js';
3
+ const formatDefinitionRef = Key.of;
4
+ const KERNEL_ORIGIN = 'kernel.astrale.ai';
5
+ const PROJECTION_CLASS = nodeClassRef('Class');
6
+ const PROJECTION_FUNCTION = nodeClassRef('Function');
7
+ const PROJECTION_POLICY = nodeClassRef('Policy');
8
+ const PROJECTION_VIEW = nodeClassRef('View');
9
+ export function referenceContracts(validation, ref, pointer) {
10
+ const candidate = validation.environment.candidate;
11
+ const local = ref.origin === candidate.origin;
12
+ const direct = Object.values(candidate.dependencies).some((entry) => entry.origin === ref.origin);
13
+ const owner = local
14
+ ? candidate
15
+ : direct
16
+ ? validation.environment.dependencies.get(ref.origin)
17
+ : undefined;
18
+ if (owner === undefined)
19
+ return invalidPolicyObjectRef(validation, ref, pointer);
20
+ switch (ref.kind) {
21
+ case 'class': {
22
+ const resolved = validation.environment.resolve(ref);
23
+ return resolved?.ref.kind === ref.kind
24
+ ? [{ kind: 'exact', ref: PROJECTION_CLASS }]
25
+ : invalidPolicyObjectRef(validation, ref, pointer);
26
+ }
27
+ case 'function':
28
+ return owner.functions[ref.name] === undefined
29
+ ? invalidPolicyObjectRef(validation, ref, pointer)
30
+ : [{ kind: 'exact', ref: PROJECTION_FUNCTION }];
31
+ case 'policy':
32
+ return owner.policies[ref.name] === undefined
33
+ ? invalidPolicyObjectRef(validation, ref, pointer)
34
+ : [{ kind: 'exact', ref: PROJECTION_POLICY }];
35
+ case 'view':
36
+ return owner.views[ref.name] === undefined
37
+ ? invalidPolicyObjectRef(validation, ref, pointer)
38
+ : [{ kind: 'exact', ref: PROJECTION_VIEW }];
39
+ case 'core': {
40
+ const node = owner.core.nodes[ref.name];
41
+ if (node === undefined)
42
+ return invalidPolicyObjectRef(validation, ref, pointer);
43
+ const resolved = validation.environment.resolve(node.class);
44
+ const analyzed = validation.analysis.definitions.get(definitionRefIndex(node.class));
45
+ // Core validation owns invalid Core Class diagnostics. Avoid adding Policy noise for
46
+ // a Node that cannot materialize in the first place.
47
+ if (node.class.origin !== owner.origin ||
48
+ resolved?.ref.kind !== 'class' ||
49
+ resolved.kind !== 'node' ||
50
+ analyzed === undefined) {
51
+ return undefined;
52
+ }
53
+ return [{ kind: 'exact', ref: node.class }];
54
+ }
55
+ }
56
+ return invalidPolicyObjectRef(validation, ref, pointer);
57
+ }
58
+ function invalidPolicyObjectRef(validation, ref, pointer) {
59
+ validation.add('PL_REF', `${pointer}/ref`, `${formatDefinitionRef(ref)} is not an exact local or direct-dependency projected Policy object.`, formatDefinitionRef(ref));
60
+ return undefined;
61
+ }
62
+ function nodeClassRef(name) {
63
+ return { origin: KERNEL_ORIGIN, kind: 'class', name };
64
+ }
@@ -3,6 +3,7 @@ import { policyVariableClass } from '../../../language/policy/model/policy.js';
3
3
  import { normalizePolicyExpression, normalizePolicyPattern, } from '../../../language/policy/normalization/normalize.js';
4
4
  import { MAX_REPEAT, MAX_VARIABLES } from './budgets.js';
5
5
  import { escapePointer } from './context.js';
6
+ import { referenceContracts } from './objects.js';
6
7
  export function validatePolicyReferences(context) {
7
8
  for (const [name, declaration] of Object.entries(context.environment.candidate.policies)) {
8
9
  const pointer = `/policies/${escapePointer(name)}`;
@@ -44,6 +45,11 @@ function validateMatch(context, name, pattern, pointer) {
44
45
  }
45
46
  }
46
47
  function validatePatternNode(context, pattern, pointer, scope, declarations, uses) {
48
+ if ('sameNode' in pattern) {
49
+ validateTerm(context, pattern.sameNode.left, `${pointer}/sameNode/left`, scope, uses);
50
+ validateTerm(context, pattern.sameNode.right, `${pointer}/sameNode/right`, scope, uses);
51
+ return;
52
+ }
47
53
  if (isEdge(pattern)) {
48
54
  context.resolveEdge(pattern.class, `${pointer}/class`);
49
55
  validateTerm(context, pattern.source, `${pointer}/source`, scope, uses);
@@ -93,6 +99,8 @@ function validatePatternNode(context, pattern, pointer, scope, declarations, use
93
99
  values.forEach((child, index) => validatePatternNode(context, child, `${pointer}/${key}/${index}`, scope, declarations, uses));
94
100
  }
95
101
  function validateTerm(context, term, pointer, scope, uses) {
102
+ if (term.kind === 'ref')
103
+ referenceContracts(context, term.ref, pointer);
96
104
  if (term.kind === 'variable') {
97
105
  if (!scope.has(term.id)) {
98
106
  context.add('PL_VARIABLE_SCOPE', pointer, `Variable ${term.id} is free, escaped, or outside its defining exists scope.`);
@@ -2,6 +2,7 @@ import { definitionRefIndex } from '../../../addressing/ordering.js';
2
2
  import { branchesOf, branchProduct, capBranches, targetMode, termKey, } from '../../../language/policy/analysis/branches.js';
3
3
  import { MAX_REPEAT } from './budgets.js';
4
4
  import { escapePointer } from './context.js';
5
+ import { referenceContracts } from './objects.js';
5
6
  export class PolicyTyping {
6
7
  context;
7
8
  constructor(context) {
@@ -83,11 +84,15 @@ export class PolicyTyping {
83
84
  const terms = new Set(use.kind === 'object' ? ['subject', 'object'] : ['subject', 'source', 'target']);
84
85
  for (const key of branch.variables.keys())
85
86
  terms.add(key);
87
+ for (const key of branch.terms.keys())
88
+ terms.add(key);
86
89
  for (const edge of branch.edges) {
87
90
  terms.add(termKey(edge.pattern.source, edge.namespace));
88
91
  terms.add(termKey(edge.pattern.target, edge.namespace));
89
92
  }
90
93
  const union = new UnionFind(terms);
94
+ for (const [left, right] of branch.equalities)
95
+ union.join(left, right);
91
96
  for (const variant of variants) {
92
97
  for (const [left, right] of variant.equalities)
93
98
  union.join(left, right);
@@ -136,6 +141,11 @@ export class PolicyTyping {
136
141
  }
137
142
  return candidates;
138
143
  }
144
+ const referenced = branch.terms.get(term);
145
+ if (referenced?.kind === 'ref') {
146
+ const contract = referenceContracts(this.context, referenced.ref, '/policies')?.[0];
147
+ return contract?.kind === 'exact' ? this.#applyExact(candidates, contract.ref) : [];
148
+ }
139
149
  const selector = branch.variables.get(term);
140
150
  if (selector === undefined)
141
151
  return [];
@@ -4,13 +4,9 @@ import { normalizePolicyChecks } from '../../../language/policy/normalization/no
4
4
  import { GRAPH_NODE_PATH_SCHEMA_ID } from '../../../language/value/syntax/node-path/schema.js';
5
5
  import { MAX_BRANCHES, MAX_CHECK_DEPTH, MAX_CHECK_LEAVES, measureChecks } from './budgets.js';
6
6
  import { escapePointer } from './context.js';
7
+ import { referenceContracts } from './objects.js';
7
8
  import { contractOfNodeDefinition } from './typing.js';
8
9
  const formatDefinitionRef = Key.of;
9
- const KERNEL_ORIGIN = 'kernel.astrale.ai';
10
- const PROJECTION_CLASS = nodeClassRef('Class');
11
- const PROJECTION_FUNCTION = nodeClassRef('Function');
12
- const PROJECTION_POLICY = nodeClassRef('Policy');
13
- const PROJECTION_VIEW = nodeClassRef('View');
14
10
  export function validateReadUses(context, typing) {
15
11
  validateClassPolicyUses(context, typing, 'read');
16
12
  }
@@ -95,6 +91,18 @@ function validateChecks(validation, typing, expression, context) {
95
91
  visitChecks(validation, typing, normalized, context, context.pointer);
96
92
  }
97
93
  function visitChecks(validation, typing, expression, context, pointer) {
94
+ if ('sameNode' in expression) {
95
+ for (const [side, object] of Object.entries(expression.sameNode)) {
96
+ const objectPointer = `${pointer}/sameNode/${side}`;
97
+ if (object.kind === 'self')
98
+ selfContracts(validation, context, objectPointer);
99
+ else if (object.kind === 'input')
100
+ inputContracts(validation, context, object.field, objectPointer);
101
+ else
102
+ referenceContracts(validation, object.ref, objectPointer);
103
+ }
104
+ return;
105
+ }
98
106
  if ('check' in expression) {
99
107
  if (!validation.resolvePolicy(expression.check, `${pointer}/check`))
100
108
  return;
@@ -120,59 +128,6 @@ function visitChecks(validation, typing, expression, context, pointer) {
120
128
  }
121
129
  values.forEach((child, index) => visitChecks(validation, typing, child, context, `${pointer}/${key}/${index}`));
122
130
  }
123
- function referenceContracts(validation, ref, pointer) {
124
- const candidate = validation.environment.candidate;
125
- const local = ref.origin === candidate.origin;
126
- const direct = Object.values(candidate.dependencies).some((entry) => entry.origin === ref.origin);
127
- const owner = local
128
- ? candidate
129
- : direct
130
- ? validation.environment.dependencies.get(ref.origin)
131
- : undefined;
132
- if (owner === undefined)
133
- return invalidPolicyObjectRef(validation, ref, pointer);
134
- switch (ref.kind) {
135
- case 'class': {
136
- const resolved = validation.environment.resolve(ref);
137
- return resolved?.ref.kind === ref.kind
138
- ? [{ kind: 'exact', ref: PROJECTION_CLASS }]
139
- : invalidPolicyObjectRef(validation, ref, pointer);
140
- }
141
- case 'function':
142
- return owner.functions[ref.name] === undefined
143
- ? invalidPolicyObjectRef(validation, ref, pointer)
144
- : [{ kind: 'exact', ref: PROJECTION_FUNCTION }];
145
- case 'policy':
146
- return owner.policies[ref.name] === undefined
147
- ? invalidPolicyObjectRef(validation, ref, pointer)
148
- : [{ kind: 'exact', ref: PROJECTION_POLICY }];
149
- case 'view':
150
- return owner.views[ref.name] === undefined
151
- ? invalidPolicyObjectRef(validation, ref, pointer)
152
- : [{ kind: 'exact', ref: PROJECTION_VIEW }];
153
- case 'core': {
154
- const node = owner.core.nodes[ref.name];
155
- if (node === undefined)
156
- return invalidPolicyObjectRef(validation, ref, pointer);
157
- const resolved = validation.environment.resolve(node.class);
158
- const analyzed = validation.analysis.definitions.get(definitionRefIndex(node.class));
159
- // Core validation owns invalid Core Class diagnostics. Avoid adding Policy noise for
160
- // a Node that cannot materialize in the first place.
161
- if (node.class.origin !== owner.origin ||
162
- resolved?.ref.kind !== 'class' ||
163
- resolved.kind !== 'node' ||
164
- analyzed === undefined) {
165
- return undefined;
166
- }
167
- return [contractOfNodeDefinition(node.class)];
168
- }
169
- }
170
- return invalidPolicyObjectRef(validation, ref, pointer);
171
- }
172
- function invalidPolicyObjectRef(validation, ref, pointer) {
173
- validation.add('PL_REF', `${pointer}/ref`, `${formatDefinitionRef(ref)} is not an exact local or direct-dependency projected Policy object.`, formatDefinitionRef(ref));
174
- return undefined;
175
- }
176
131
  function selfContracts(validation, context, pointer) {
177
132
  if (!context.instanceMethod || context.receiver === undefined) {
178
133
  validation.add('PL_USE', pointer, 'self is valid only for a Node instance Method.');
@@ -238,6 +193,3 @@ function checkOperands(value) {
238
193
  ? { key: 'allOf', values: value.allOf }
239
194
  : { key: 'anyOf', values: value.anyOf };
240
195
  }
241
- function nodeClassRef(name) {
242
- return { origin: KERNEL_ORIGIN, kind: 'class', name };
243
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-dsl",
3
- "version": "0.2.0-beta.29",
3
+ "version": "0.2.0-beta.30",
4
4
  "description": "Astrale Kernel DSL - Schema authoring, inference, and IR serialization",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {