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

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.
@@ -6,6 +6,7 @@ import { isMethodExecutable } from '../../language/callable/executable.js';
6
6
  import { policyObjectKey, policyObjectOrigin, policyVariableClass, projectedMemberClass, } from '../../language/index.js';
7
7
  import { branchesOf, branchProduct, capBranches, isConnected, targetMode, termKey, } from '../../language/policy/analysis/branches.js';
8
8
  import { BRANCH_OVERFLOW, MAX_BRANCHES, MAX_CHECK_DEPTH, MAX_CHECK_LEAVES, MAX_DOMAINS, MAX_EDGES, MAX_PATTERN_DEPTH, MAX_REPEAT, MAX_VARIABLES, measureChecks, } from '../../language/policy/analysis/budgets.js';
9
+ import { admittedCheckClasses, neverAdmittedClasses, } from '../../language/policy/analysis/checks.js';
9
10
  import { normalizePolicyExpression } from '../../language/policy/normalization/normalize.js';
10
11
  import { bindValueProgram } from '../../language/value/compile/load.js';
11
12
  import { validateValueProgram } from '../../language/value/evaluate/program.js';
@@ -538,6 +539,7 @@ function validateCallablePolicyTyping(context, typing, entry, expression, input,
538
539
  measure.leaves > MAX_CHECK_LEAVES) {
539
540
  fail(path, 'Callable Policy checks exceed the V1 depth, branch, or leaf budget.');
540
541
  }
542
+ const leaves = new Map();
541
543
  const visit = (value, pointer) => {
542
544
  if ('sameNode' in value)
543
545
  return;
@@ -547,12 +549,23 @@ function validateCallablePolicyTyping(context, typing, entry, expression, input,
547
549
  return;
548
550
  }
549
551
  const contracts = callableObjectContracts(context, entry, input, receiver, value.object);
550
- if (contracts !== undefined &&
551
- contracts.some((contract) => !typing.policyAccepts(value.check.name, contract))) {
552
+ if (contracts === undefined)
553
+ return;
554
+ const typings = new Map(contracts.map((contract) => [
555
+ contract.kind === 'exact' ? Key.of(contract.ref) : 'default',
556
+ typing.policyBranchTyping(value.check.name, contract),
557
+ ]));
558
+ const admitted = admittedCheckClasses(typings);
559
+ if (admitted === undefined) {
552
560
  fail(pointer, 'Named Policy is incompatible with the callable object contract.');
553
561
  }
562
+ leaves.set(value, { accepted: [...typings.keys()], admitted });
554
563
  };
555
564
  visit(expression, path);
565
+ const [never] = neverAdmittedClasses(expression, leaves);
566
+ if (never !== undefined) {
567
+ fail(path, `Callable Policy checks can never admit ${never[0]} of Class ${never[1]}.`);
568
+ }
556
569
  }
557
570
  function callableObjectContracts(context, entry, input, receiver, object) {
558
571
  if (object.kind === 'self') {
@@ -629,9 +642,11 @@ class CompiledPolicyTyping {
629
642
  return 'allOf' in declaration ? branchProduct(children) : capBranches(children.flat());
630
643
  }
631
644
  policyAccepts(name, object) {
645
+ return this.policyBranchTyping(name, object)?.every(Boolean) ?? false;
646
+ }
647
+ policyBranchTyping(name, object) {
632
648
  const branches = this.expandedBranches(name, new Set(), `$use:${name}`);
633
- return (branches !== undefined &&
634
- branches.every((branch) => this.branchIsTyped(branch, { kind: 'object', object })));
649
+ return branches?.map((branch) => this.branchIsTyped(branch, { kind: 'object', object }));
635
650
  }
636
651
  policyAcceptsEdge(name, source, target) {
637
652
  const branches = this.expandedBranches(name, new Set(), `$edge-use:${name}`);
@@ -0,0 +1,28 @@
1
+ import type { PolicyCheckExpression } from '../model/policy.js';
2
+ export type PolicyCheckLeaf = Extract<PolicyCheckExpression, {
3
+ readonly check: unknown;
4
+ }>;
5
+ /** How one check leaf types against the Classes its object accepts. */
6
+ export interface CheckLeafClasses {
7
+ /** Every Class the leaf's object accepts. */
8
+ readonly accepted: readonly string[];
9
+ /** The accepted Classes at least one normalized branch of the leaf's Policy is typed for. */
10
+ readonly admitted: ReadonlySet<string>;
11
+ }
12
+ /**
13
+ * The Classes a leaf admits, from the typing of each normalized branch of its Policy for each
14
+ * accepted Class of its object. `undefined` when the Policy does not expand, or when one branch is
15
+ * typed for no accepted Class: that branch can never match this object, so it is refused rather
16
+ * than silently erased.
17
+ */
18
+ export declare function admittedCheckClasses(typing: ReadonlyMap<string, readonly boolean[] | undefined>): ReadonlySet<string> | undefined;
19
+ /**
20
+ * The accepted Classes of each object that the whole check tree can never admit.
21
+ *
22
+ * The tree is read in disjunctive normal form. A conjunction admits Class C of object O when every
23
+ * leaf over O in it admits C and every other object it checks keeps a Class that all of its leaves
24
+ * over that object admit; an object the conjunction does not check keeps every accepted Class.
25
+ * `sameNode` compares identities, not Classes, so it never removes a Class here. A tree over the
26
+ * check budget is already refused, so it is not expanded.
27
+ */
28
+ export declare function neverAdmittedClasses(expression: PolicyCheckExpression, leaves: ReadonlyMap<PolicyCheckLeaf, CheckLeafClasses>): readonly (readonly [object: string, className: string])[];
@@ -0,0 +1,80 @@
1
+ import { policyObjectKey } from '../model/policy.js';
2
+ import { MAX_BRANCHES, measureChecks } from './budgets.js';
3
+ /** Names a callable check object. Every leaf over one object sees the same Node at runtime. */
4
+ function checkObjectName(object) {
5
+ if (object.kind === 'self')
6
+ return 'self';
7
+ if (object.kind === 'input')
8
+ return `input.${object.field}`;
9
+ return policyObjectKey(object.ref);
10
+ }
11
+ /**
12
+ * The Classes a leaf admits, from the typing of each normalized branch of its Policy for each
13
+ * accepted Class of its object. `undefined` when the Policy does not expand, or when one branch is
14
+ * typed for no accepted Class: that branch can never match this object, so it is refused rather
15
+ * than silently erased.
16
+ */
17
+ export function admittedCheckClasses(typing) {
18
+ const rows = [...typing];
19
+ if (!rows.every((row) => row[1] !== undefined)) {
20
+ return undefined;
21
+ }
22
+ const branches = Math.max(0, ...rows.map(([, row]) => row.length));
23
+ for (let index = 0; index < branches; index++) {
24
+ if (!rows.some(([, row]) => row[index] === true))
25
+ return undefined;
26
+ }
27
+ return new Set(rows.filter(([, row]) => row.some(Boolean)).map(([name]) => name));
28
+ }
29
+ /**
30
+ * The accepted Classes of each object that the whole check tree can never admit.
31
+ *
32
+ * The tree is read in disjunctive normal form. A conjunction admits Class C of object O when every
33
+ * leaf over O in it admits C and every other object it checks keeps a Class that all of its leaves
34
+ * over that object admit; an object the conjunction does not check keeps every accepted Class.
35
+ * `sameNode` compares identities, not Classes, so it never removes a Class here. A tree over the
36
+ * check budget is already refused, so it is not expanded.
37
+ */
38
+ export function neverAdmittedClasses(expression, leaves) {
39
+ if (measureChecks(expression).branches > MAX_BRANCHES)
40
+ return [];
41
+ const accepted = new Map();
42
+ for (const [leaf, classes] of leaves)
43
+ accepted.set(checkObjectName(leaf.object), classes.accepted);
44
+ const admitted = new Map();
45
+ for (const conjunction of disjunctiveForm(expression)) {
46
+ const constrained = new Map();
47
+ for (const leaf of conjunction) {
48
+ if (!('check' in leaf))
49
+ continue;
50
+ const classes = leaves.get(leaf);
51
+ if (classes === undefined)
52
+ continue;
53
+ const object = checkObjectName(leaf.object);
54
+ const previous = constrained.get(object);
55
+ constrained.set(object, previous === undefined
56
+ ? classes.admitted
57
+ : new Set([...previous].filter((name) => classes.admitted.has(name))));
58
+ }
59
+ if ([...constrained.values()].some((classes) => classes.size === 0))
60
+ continue;
61
+ for (const [object, all] of accepted) {
62
+ const reached = admitted.get(object) ?? new Set();
63
+ for (const name of constrained.get(object) ?? all)
64
+ reached.add(name);
65
+ admitted.set(object, reached);
66
+ }
67
+ }
68
+ return [...accepted].flatMap(([object, all]) => all
69
+ .filter((name) => admitted.get(object)?.has(name) !== true)
70
+ .map((name) => [object, name]));
71
+ }
72
+ function disjunctiveForm(expression) {
73
+ if ('check' in expression || 'sameNode' in expression)
74
+ return [[expression]];
75
+ if ('anyOf' in expression)
76
+ return expression.anyOf.flatMap(disjunctiveForm);
77
+ return expression.allOf
78
+ .map(disjunctiveForm)
79
+ .reduce((left, right) => left.flatMap((prefix) => right.map((suffix) => [...prefix, ...suffix])), [[]]);
80
+ }
@@ -12,6 +12,8 @@ export declare class PolicyTyping {
12
12
  constructor(context: PolicyValidationContext);
13
13
  validatePolicies(): void;
14
14
  policyAccepts(name: string, object: ObjectContract): boolean;
15
+ /** Whether each normalized branch of the Policy is typed for an object of this contract. */
16
+ policyBranchTyping(name: string, object: ObjectContract): readonly boolean[] | undefined;
15
17
  policyAcceptsEdge(name: string, source: ObjectContract, target: ObjectContract): boolean;
16
18
  concreteEndpointClasses(accepts: readonly ClassRef[]): readonly ClassRef[];
17
19
  endpointsOverlap(left: readonly ClassRef[], right: readonly ClassRef[]): boolean;
@@ -35,9 +35,13 @@ export class PolicyTyping {
35
35
  }
36
36
  }
37
37
  policyAccepts(name, object) {
38
+ return this.policyBranchTyping(name, object)?.every(Boolean) ?? false;
39
+ }
40
+ /** Whether each normalized branch of the Policy is typed for an object of this contract. */
41
+ policyBranchTyping(name, object) {
38
42
  const branches = this.#expandedBranches(name, new Set(), `$use:${name}`);
39
43
  const use = { kind: 'object', object };
40
- return branches !== undefined && branches.every((branch) => this.#branchIsTyped(branch, use));
44
+ return branches?.map((branch) => this.#branchIsTyped(branch, use));
41
45
  }
42
46
  policyAcceptsEdge(name, source, target) {
43
47
  const branches = this.#expandedBranches(name, new Set(), `$edge-use:${name}`);
@@ -1,6 +1,7 @@
1
1
  import { Key } from '../../../addressing/index.js';
2
2
  import { definitionRefIndex } from '../../../addressing/ordering.js';
3
3
  import { isBinaryInput } from '../../../language/callable/model/callable.js';
4
+ import { admittedCheckClasses, neverAdmittedClasses, } from '../../../language/policy/analysis/checks.js';
4
5
  import { normalizePolicyChecks } from '../../../language/policy/normalization/normalize.js';
5
6
  import { GRAPH_NODE_PATH_SCHEMA_ID } from '../../../language/value/syntax/node-path/schema.js';
6
7
  import { MAX_BRANCHES, MAX_CHECK_DEPTH, MAX_CHECK_LEAVES, measureChecks } from './budgets.js';
@@ -89,9 +90,13 @@ function validateChecks(validation, typing, expression, context) {
89
90
  measure.leaves > MAX_CHECK_LEAVES) {
90
91
  validation.add('PL_BUDGET', context.pointer, `Callable checks require depth <= ${MAX_CHECK_DEPTH}, branches <= ${MAX_BRANCHES}, and leaves <= ${MAX_CHECK_LEAVES}; got ${measure.depth}, ${measure.branches}, ${measure.leaves}.`);
91
92
  }
92
- visitChecks(validation, typing, normalized, context, context.pointer);
93
+ const leaves = new Map();
94
+ visitChecks(validation, typing, normalized, context, context.pointer, leaves);
95
+ for (const [object, className] of neverAdmittedClasses(normalized, leaves)) {
96
+ validation.add('PL_USE', context.pointer, `Callable checks can never admit ${object} of Class ${className}.`, className);
97
+ }
93
98
  }
94
- function visitChecks(validation, typing, expression, context, pointer) {
99
+ function visitChecks(validation, typing, expression, context, pointer, leaves) {
95
100
  if ('sameNode' in expression) {
96
101
  for (const [side, object] of Object.entries(expression.sameNode)) {
97
102
  const objectPointer = `${pointer}/sameNode/${side}`;
@@ -115,19 +120,26 @@ function visitChecks(validation, typing, expression, context, pointer) {
115
120
  : referenceContracts(validation, expression.object.ref, objectPointer);
116
121
  if (contracts === undefined)
117
122
  return;
118
- for (const contract of contracts) {
119
- if (!typing.policyAccepts(expression.check.name, contract)) {
120
- validation.add('PL_USE', pointer, `${formatDefinitionRef(expression.check)} is incompatible with the callable object contract.`, formatDefinitionRef(expression.check));
121
- break;
122
- }
123
+ const typings = new Map(contracts.map((contract) => [
124
+ contractName(contract),
125
+ typing.policyBranchTyping(expression.check.name, contract),
126
+ ]));
127
+ const admitted = admittedCheckClasses(typings);
128
+ if (admitted === undefined) {
129
+ validation.add('PL_USE', pointer, `${formatDefinitionRef(expression.check)} is incompatible with the callable object contract.`, formatDefinitionRef(expression.check));
130
+ return;
123
131
  }
132
+ leaves.set(expression, { accepted: [...typings.keys()], admitted });
124
133
  return;
125
134
  }
126
135
  const { key, values } = checkOperands(expression);
127
136
  if (values.length === 0) {
128
137
  validation.add('PL_COMPOSITION', `${pointer}/${key}`, `${key} must be non-empty.`);
129
138
  }
130
- values.forEach((child, index) => visitChecks(validation, typing, child, context, `${pointer}/${key}/${index}`));
139
+ values.forEach((child, index) => visitChecks(validation, typing, child, context, `${pointer}/${key}/${index}`, leaves));
140
+ }
141
+ function contractName(contract) {
142
+ return contract.kind === 'exact' ? formatDefinitionRef(contract.ref) : 'default';
131
143
  }
132
144
  function selfContracts(validation, context, pointer) {
133
145
  if (!context.instanceMethod || context.receiver === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-dsl",
3
- "version": "0.2.0-beta.42",
3
+ "version": "0.2.0-beta.43",
4
4
  "description": "Astrale Kernel DSL - Schema authoring, inference, and IR serialization",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {