@astrale-os/kernel-dsl 0.1.4 → 0.1.5
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.
- package/dist/builders/bitmask.js +1 -0
- package/dist/builders/bitmask.js.map +1 -0
- package/dist/builders/data.js +1 -0
- package/dist/builders/data.js.map +1 -0
- package/dist/builders/definition/base.js +1 -0
- package/dist/builders/definition/base.js.map +1 -0
- package/dist/builders/definition/edge-class.js +1 -0
- package/dist/builders/definition/edge-class.js.map +1 -0
- package/dist/builders/definition/edge-interface.js +1 -0
- package/dist/builders/definition/edge-interface.js.map +1 -0
- package/dist/builders/definition/node-class.js +1 -0
- package/dist/builders/definition/node-class.js.map +1 -0
- package/dist/builders/definition/node-interface.js +1 -0
- package/dist/builders/definition/node-interface.js.map +1 -0
- package/dist/builders/function.d.ts +16 -2
- package/dist/builders/function.d.ts.map +1 -1
- package/dist/builders/function.js +1 -1
- package/dist/builders/function.js.map +1 -0
- package/dist/builders/policy.d.ts +10 -0
- package/dist/builders/policy.d.ts.map +1 -0
- package/dist/builders/policy.js +28 -0
- package/dist/builders/policy.js.map +1 -0
- package/dist/builders/prop.js +1 -0
- package/dist/builders/prop.js.map +1 -0
- package/dist/builders/ref.js +1 -0
- package/dist/builders/ref.js.map +1 -0
- package/dist/grammar/definition/base.js +1 -0
- package/dist/grammar/definition/base.js.map +1 -0
- package/dist/grammar/definition/discriminants.js +1 -0
- package/dist/grammar/definition/discriminants.js.map +1 -0
- package/dist/grammar/definition/edge-class.js +1 -0
- package/dist/grammar/definition/edge-class.js.map +1 -0
- package/dist/grammar/definition/edge-interface.js +1 -0
- package/dist/grammar/definition/edge-interface.js.map +1 -0
- package/dist/grammar/definition/node-class.d.ts +5 -0
- package/dist/grammar/definition/node-class.d.ts.map +1 -1
- package/dist/grammar/definition/node-class.js +1 -0
- package/dist/grammar/definition/node-class.js.map +1 -0
- package/dist/grammar/definition/node-interface.js +1 -0
- package/dist/grammar/definition/node-interface.js.map +1 -0
- package/dist/grammar/facets/constraints.js +1 -0
- package/dist/grammar/facets/constraints.js.map +1 -0
- package/dist/grammar/facets/content.js +1 -0
- package/dist/grammar/facets/content.js.map +1 -0
- package/dist/grammar/facets/endpoints.js +1 -0
- package/dist/grammar/facets/endpoints.js.map +1 -0
- package/dist/grammar/facets/indexes.js +1 -0
- package/dist/grammar/facets/indexes.js.map +1 -0
- package/dist/grammar/facets/properties.js +1 -0
- package/dist/grammar/facets/properties.js.map +1 -0
- package/dist/grammar/function/config.d.ts +5 -3
- package/dist/grammar/function/config.d.ts.map +1 -1
- package/dist/grammar/function/config.js +1 -0
- package/dist/grammar/function/config.js.map +1 -0
- package/dist/grammar/function/def.d.ts +3 -0
- package/dist/grammar/function/def.d.ts.map +1 -1
- package/dist/grammar/function/def.js +1 -0
- package/dist/grammar/function/def.js.map +1 -0
- package/dist/grammar/function/inheritance.js +1 -0
- package/dist/grammar/function/inheritance.js.map +1 -0
- package/dist/grammar/function/output.js +1 -0
- package/dist/grammar/function/output.js.map +1 -0
- package/dist/grammar/policy/callable.d.ts +40 -0
- package/dist/grammar/policy/callable.d.ts.map +1 -0
- package/dist/grammar/policy/callable.js +2 -0
- package/dist/grammar/policy/callable.js.map +1 -0
- package/dist/grammar/policy/def.d.ts +27 -0
- package/dist/grammar/policy/def.d.ts.map +1 -0
- package/dist/grammar/policy/def.js +2 -0
- package/dist/grammar/policy/def.js.map +1 -0
- package/dist/grammar/policy/pattern.d.ts +34 -0
- package/dist/grammar/policy/pattern.d.ts.map +1 -0
- package/dist/grammar/policy/pattern.js +2 -0
- package/dist/grammar/policy/pattern.js.map +1 -0
- package/dist/grammar/values/bitmask.js +1 -0
- package/dist/grammar/values/bitmask.js.map +1 -0
- package/dist/grammar/values/data.js +1 -0
- package/dist/grammar/values/data.js.map +1 -0
- package/dist/grammar/values/ref.js +1 -0
- package/dist/grammar/values/ref.js.map +1 -0
- package/dist/grammar/values/self.js +1 -0
- package/dist/grammar/values/self.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/inference/content.js +1 -0
- package/dist/inference/content.js.map +1 -0
- package/dist/inference/index.js +1 -0
- package/dist/inference/index.js.map +1 -0
- package/dist/inference/inheritance.js +1 -0
- package/dist/inference/inheritance.js.map +1 -0
- package/dist/inference/input.js +1 -0
- package/dist/inference/input.js.map +1 -0
- package/dist/inference/methods.js +1 -0
- package/dist/inference/methods.js.map +1 -0
- package/dist/inference/properties.js +1 -0
- package/dist/inference/properties.js.map +1 -0
- package/dist/inference/schema.js +1 -0
- package/dist/inference/schema.js.map +1 -0
- package/dist/instance/core/define.js +1 -0
- package/dist/instance/core/define.js.map +1 -0
- package/dist/instance/core/edge.js +1 -0
- package/dist/instance/core/edge.js.map +1 -0
- package/dist/instance/core/endpoint.js +1 -0
- package/dist/instance/core/endpoint.js.map +1 -0
- package/dist/instance/core/node.js +1 -0
- package/dist/instance/core/node.js.map +1 -0
- package/dist/instance/core/path.js +1 -0
- package/dist/instance/core/path.js.map +1 -0
- package/dist/instance/core/self.js +1 -0
- package/dist/instance/core/self.js.map +1 -0
- package/dist/instance/core/types.js +1 -0
- package/dist/instance/core/types.js.map +1 -0
- package/dist/instance/seed/define.js +1 -0
- package/dist/instance/seed/define.js.map +1 -0
- package/dist/instance/seed/types.js +1 -0
- package/dist/instance/seed/types.js.map +1 -0
- package/dist/ir/classes.d.ts +6 -0
- package/dist/ir/classes.d.ts.map +1 -1
- package/dist/ir/classes.js +1 -0
- package/dist/ir/classes.js.map +1 -0
- package/dist/ir/domain.js +1 -0
- package/dist/ir/domain.js.map +1 -0
- package/dist/ir/endpoints.js +1 -0
- package/dist/ir/endpoints.js.map +1 -0
- package/dist/ir/functions.d.ts +15 -0
- package/dist/ir/functions.d.ts.map +1 -1
- package/dist/ir/functions.js +1 -0
- package/dist/ir/functions.js.map +1 -0
- package/dist/ir/index.d.ts +2 -1
- package/dist/ir/index.d.ts.map +1 -1
- package/dist/ir/index.js +1 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/ir.d.ts +12 -0
- package/dist/ir/ir.d.ts.map +1 -1
- package/dist/ir/ir.js +1 -0
- package/dist/ir/ir.js.map +1 -0
- package/dist/ir/json-schema.js +1 -0
- package/dist/ir/json-schema.js.map +1 -0
- package/dist/ir/policies.d.ts +74 -0
- package/dist/ir/policies.d.ts.map +1 -0
- package/dist/ir/policies.js +2 -0
- package/dist/ir/policies.js.map +1 -0
- package/dist/schema/classify.js +1 -0
- package/dist/schema/classify.js.map +1 -0
- package/dist/schema/define.d.ts +2 -0
- package/dist/schema/define.d.ts.map +1 -1
- package/dist/schema/define.js +20 -2
- package/dist/schema/define.js.map +1 -0
- package/dist/schema/error.js +1 -0
- package/dist/schema/error.js.map +1 -0
- package/dist/schema/refs.js +1 -0
- package/dist/schema/refs.js.map +1 -0
- package/dist/schema/resolve/index.js +1 -0
- package/dist/schema/resolve/index.js.map +1 -0
- package/dist/schema/resolve/methods.js +1 -0
- package/dist/schema/resolve/methods.js.map +1 -0
- package/dist/schema/resolve/properties.js +1 -0
- package/dist/schema/resolve/properties.js.map +1 -0
- package/dist/schema/resolve/self.js +1 -0
- package/dist/schema/resolve/self.js.map +1 -0
- package/dist/schema/resolve/thunks.js +1 -0
- package/dist/schema/resolve/thunks.js.map +1 -0
- package/dist/schema/schema.d.ts +3 -0
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +1 -0
- package/dist/schema/schema.js.map +1 -0
- package/dist/schema/validators/context.d.ts +4 -0
- package/dist/schema/validators/context.d.ts.map +1 -1
- package/dist/schema/validators/context.js +1 -0
- package/dist/schema/validators/context.js.map +1 -0
- package/dist/schema/validators/endpoints.js +1 -0
- package/dist/schema/validators/endpoints.js.map +1 -0
- package/dist/schema/validators/index.d.ts +1 -0
- package/dist/schema/validators/index.d.ts.map +1 -1
- package/dist/schema/validators/index.js +4 -0
- package/dist/schema/validators/index.js.map +1 -0
- package/dist/schema/validators/indexes.js +1 -0
- package/dist/schema/validators/indexes.js.map +1 -0
- package/dist/schema/validators/inheritance.js +1 -0
- package/dist/schema/validators/inheritance.js.map +1 -0
- package/dist/schema/validators/methods.js +1 -0
- package/dist/schema/validators/methods.js.map +1 -0
- package/dist/schema/validators/names.js +1 -0
- package/dist/schema/validators/names.js.map +1 -0
- package/dist/schema/validators/policies.d.ts +4 -0
- package/dist/schema/validators/policies.d.ts.map +1 -0
- package/dist/schema/validators/policies.js +61 -0
- package/dist/schema/validators/policies.js.map +1 -0
- package/dist/schema/validators/ref-targets.js +1 -0
- package/dist/schema/validators/ref-targets.js.map +1 -0
- package/dist/serializer/context.d.ts +3 -0
- package/dist/serializer/context.d.ts.map +1 -1
- package/dist/serializer/context.js +57 -6
- package/dist/serializer/context.js.map +1 -0
- package/dist/serializer/helpers.js +1 -0
- package/dist/serializer/helpers.js.map +1 -0
- package/dist/serializer/index.js +1 -0
- package/dist/serializer/index.js.map +1 -0
- package/dist/serializer/policies.d.ts +34 -0
- package/dist/serializer/policies.d.ts.map +1 -0
- package/dist/serializer/policies.js +837 -0
- package/dist/serializer/policies.js.map +1 -0
- package/dist/serializer/policy-typing.d.ts +56 -0
- package/dist/serializer/policy-typing.d.ts.map +1 -0
- package/dist/serializer/policy-typing.js +490 -0
- package/dist/serializer/policy-typing.js.map +1 -0
- package/dist/serializer/serialize.js +1 -0
- package/dist/serializer/serialize.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { isEdgeClass, isNode, isNodeClass } from '../grammar/definition/discriminants.js';
|
|
2
|
+
import { SchemaValidationError } from '../schema/error.js';
|
|
3
|
+
import { buildDescriptorMap, findDefTransitive } from '../schema/refs.js';
|
|
4
|
+
import { getRefMeta, getZodDef, getZodTypeName, hasRefTag } from './helpers.js';
|
|
5
|
+
import { PolicyTyping } from './policy-typing.js';
|
|
6
|
+
const PATTERN_BINDING = Symbol('astrale.policy.pattern-binding');
|
|
7
|
+
const PATTERN_EXPRESSION = Symbol('astrale.policy.pattern-expression');
|
|
8
|
+
const CHECK_BINDING = Symbol('astrale.policy.check-binding');
|
|
9
|
+
const CHECK_EXPRESSION = Symbol('astrale.policy.check-expression');
|
|
10
|
+
const LIMITS = Object.freeze({
|
|
11
|
+
depth: 4,
|
|
12
|
+
branches: 8,
|
|
13
|
+
variablesPerExists: 4,
|
|
14
|
+
edgesPerBranch: 6,
|
|
15
|
+
repeat: 3,
|
|
16
|
+
domains: 4,
|
|
17
|
+
callableLeaves: 8,
|
|
18
|
+
});
|
|
19
|
+
function fail(field, message, expected, received) {
|
|
20
|
+
throw new SchemaValidationError(message, field, expected, received);
|
|
21
|
+
}
|
|
22
|
+
function isDefinition(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && '__kind' in value;
|
|
24
|
+
}
|
|
25
|
+
function canonicalJson(value) {
|
|
26
|
+
if (value === null || typeof value !== 'object')
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
30
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
31
|
+
return `{${entries
|
|
32
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
33
|
+
.join(',')}}`;
|
|
34
|
+
}
|
|
35
|
+
function uniqueSorted(values, key) {
|
|
36
|
+
const byKey = new Map();
|
|
37
|
+
for (const value of values)
|
|
38
|
+
byKey.set(key(value), value);
|
|
39
|
+
return [...byKey.entries()]
|
|
40
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
41
|
+
.map(([, value]) => value);
|
|
42
|
+
}
|
|
43
|
+
function permutations(values) {
|
|
44
|
+
if (values.length <= 1)
|
|
45
|
+
return [[...values]];
|
|
46
|
+
const result = [];
|
|
47
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
48
|
+
const value = values[index];
|
|
49
|
+
const rest = [...values.slice(0, index), ...values.slice(index + 1)];
|
|
50
|
+
for (const tail of permutations(rest))
|
|
51
|
+
result.push([value, ...tail]);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Canonicalize alpha-equivalent exists() binders before assigning public IDs.
|
|
57
|
+
* Each exists scope has at most four variables, so exhaustively comparing its
|
|
58
|
+
* 24 possible declaration orders is bounded by the Sirius structural limit.
|
|
59
|
+
*/
|
|
60
|
+
function alphaNormalizePattern(expression, environment = new Map(), depth = 0) {
|
|
61
|
+
const keyNode = (node) => {
|
|
62
|
+
if (node.kind !== 'variable')
|
|
63
|
+
return node;
|
|
64
|
+
const token = environment.get(node.id);
|
|
65
|
+
if (!token)
|
|
66
|
+
fail('policies', `Policy variable ${node.id} has no active declaration`);
|
|
67
|
+
return { kind: 'variable', id: token };
|
|
68
|
+
};
|
|
69
|
+
if ('source' in expression) {
|
|
70
|
+
return {
|
|
71
|
+
ir: expression,
|
|
72
|
+
key: {
|
|
73
|
+
source: keyNode(expression.source),
|
|
74
|
+
class: expression.class,
|
|
75
|
+
target: keyNode(expression.target),
|
|
76
|
+
...(expression.repeat ? { repeat: expression.repeat } : {}),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if ('exists' in expression) {
|
|
81
|
+
let best;
|
|
82
|
+
for (const ordering of permutations(expression.exists.nodes)) {
|
|
83
|
+
const scoped = new Map(environment);
|
|
84
|
+
const keyedNodes = ordering.map((node, index) => {
|
|
85
|
+
const token = `scope-${depth}-variable-${index}`;
|
|
86
|
+
scoped.set(node.variable.id, token);
|
|
87
|
+
return { variable: { kind: 'variable', id: token }, class: node.class };
|
|
88
|
+
});
|
|
89
|
+
const where = alphaNormalizePattern(expression.exists.where, scoped, depth + 1);
|
|
90
|
+
const key = { exists: { nodes: keyedNodes, where: where.key } };
|
|
91
|
+
const candidate = {
|
|
92
|
+
ir: {
|
|
93
|
+
exists: {
|
|
94
|
+
nodes: ordering,
|
|
95
|
+
where: where.ir,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
key,
|
|
99
|
+
encoded: canonicalJson(key),
|
|
100
|
+
};
|
|
101
|
+
if (!best || candidate.encoded < best.encoded)
|
|
102
|
+
best = candidate;
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
const operator = 'allOf' in expression ? 'allOf' : 'anyOf';
|
|
107
|
+
const operands = 'allOf' in expression ? expression.allOf : expression.anyOf;
|
|
108
|
+
const canonical = operands.map((operand) => alphaNormalizePattern(operand, environment, depth));
|
|
109
|
+
const byKey = new Map();
|
|
110
|
+
for (const operand of canonical)
|
|
111
|
+
byKey.set(canonicalJson(operand.key), operand);
|
|
112
|
+
const sorted = [...byKey.entries()]
|
|
113
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
114
|
+
.map(([, operand]) => operand);
|
|
115
|
+
const irOperands = sorted.map((operand) => operand.ir);
|
|
116
|
+
const keyOperands = sorted.map((operand) => operand.key);
|
|
117
|
+
return operator === 'allOf'
|
|
118
|
+
? { ir: { allOf: irOperands }, key: { allOf: keyOperands } }
|
|
119
|
+
: { ir: { anyOf: irOperands }, key: { anyOf: keyOperands } };
|
|
120
|
+
}
|
|
121
|
+
function makePatternBinding(node) {
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
[PATTERN_BINDING]: node,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function makePatternExpression(ir) {
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
[PATTERN_EXPRESSION]: ir,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function getPatternExpression(value, field) {
|
|
132
|
+
if (typeof value !== 'object' || value === null || !(PATTERN_EXPRESSION in value)) {
|
|
133
|
+
return fail(field, `Policy builder '${field}' must return an expression created by edge(), exists(), allOf() or anyOf()`, 'closed Policy Pattern expression', typeof value);
|
|
134
|
+
}
|
|
135
|
+
return value[PATTERN_EXPRESSION];
|
|
136
|
+
}
|
|
137
|
+
function makeCheckBinding(object, target) {
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
[CHECK_BINDING]: { object, target },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function makeCheckExpression(ir) {
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
[CHECK_EXPRESSION]: ir,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function getCheckExpression(value, field) {
|
|
148
|
+
if (typeof value !== 'object' || value === null || !(CHECK_EXPRESSION in value)) {
|
|
149
|
+
return fail(field, `Callable Policy '${field}' must return an expression created by check(), allOf() or anyOf()`, 'closed callable Policy expression', typeof value);
|
|
150
|
+
}
|
|
151
|
+
return value[CHECK_EXPRESSION];
|
|
152
|
+
}
|
|
153
|
+
function normalizePattern(expression) {
|
|
154
|
+
if ('source' in expression) {
|
|
155
|
+
const repeat = expression.repeat;
|
|
156
|
+
if (repeat?.min === 1 && repeat.max === 1) {
|
|
157
|
+
const { repeat: _omitted, ...edge } = expression;
|
|
158
|
+
return edge;
|
|
159
|
+
}
|
|
160
|
+
return expression;
|
|
161
|
+
}
|
|
162
|
+
if ('exists' in expression) {
|
|
163
|
+
const nodes = [...expression.exists.nodes].sort((left, right) => left.variable.id - right.variable.id);
|
|
164
|
+
return {
|
|
165
|
+
exists: {
|
|
166
|
+
nodes,
|
|
167
|
+
where: normalizePattern(expression.exists.where),
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if ('allOf' in expression) {
|
|
172
|
+
const operands = [];
|
|
173
|
+
for (const operand of expression.allOf) {
|
|
174
|
+
const normalized = normalizePattern(operand);
|
|
175
|
+
if ('allOf' in normalized)
|
|
176
|
+
operands.push(...normalized.allOf);
|
|
177
|
+
else
|
|
178
|
+
operands.push(normalized);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
allOf: uniqueSorted(operands, canonicalJson),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const operands = [];
|
|
185
|
+
for (const operand of expression.anyOf) {
|
|
186
|
+
const normalized = normalizePattern(operand);
|
|
187
|
+
if ('anyOf' in normalized)
|
|
188
|
+
operands.push(...normalized.anyOf);
|
|
189
|
+
else
|
|
190
|
+
operands.push(normalized);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
anyOf: uniqueSorted(operands, canonicalJson),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function normalizeCheck(expression) {
|
|
197
|
+
if ('check' in expression)
|
|
198
|
+
return expression;
|
|
199
|
+
if ('allOf' in expression) {
|
|
200
|
+
const operands = [];
|
|
201
|
+
for (const operand of expression.allOf) {
|
|
202
|
+
const normalized = normalizeCheck(operand);
|
|
203
|
+
if ('allOf' in normalized)
|
|
204
|
+
operands.push(...normalized.allOf);
|
|
205
|
+
else
|
|
206
|
+
operands.push(normalized);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
allOf: uniqueSorted(operands, canonicalJson),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const operands = [];
|
|
213
|
+
for (const operand of expression.anyOf) {
|
|
214
|
+
const normalized = normalizeCheck(operand);
|
|
215
|
+
if ('anyOf' in normalized)
|
|
216
|
+
operands.push(...normalized.anyOf);
|
|
217
|
+
else
|
|
218
|
+
operands.push(normalized);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
anyOf: uniqueSorted(operands, canonicalJson),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function patternDepth(expression) {
|
|
225
|
+
if ('source' in expression)
|
|
226
|
+
return 1;
|
|
227
|
+
if ('exists' in expression)
|
|
228
|
+
return 1 + patternDepth(expression.exists.where);
|
|
229
|
+
const operands = 'allOf' in expression ? expression.allOf : expression.anyOf;
|
|
230
|
+
return 1 + Math.max(...operands.map(patternDepth));
|
|
231
|
+
}
|
|
232
|
+
function collectPatternDomains(expression, into) {
|
|
233
|
+
if ('source' in expression) {
|
|
234
|
+
into.add(expression.class.origin);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if ('exists' in expression) {
|
|
238
|
+
for (const node of expression.exists.nodes)
|
|
239
|
+
into.add(node.class.origin);
|
|
240
|
+
collectPatternDomains(expression.exists.where, into);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const operands = 'allOf' in expression ? expression.allOf : expression.anyOf;
|
|
244
|
+
for (const operand of operands)
|
|
245
|
+
collectPatternDomains(operand, into);
|
|
246
|
+
}
|
|
247
|
+
function mergeBranches(left, right) {
|
|
248
|
+
return {
|
|
249
|
+
edges: [...left.edges, ...right.edges],
|
|
250
|
+
variables: new Set([...left.variables, ...right.variables]),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function patternBranches(expression, activeVariables = new Set()) {
|
|
254
|
+
if ('source' in expression) {
|
|
255
|
+
return [{ edges: [expression], variables: new Set(activeVariables) }];
|
|
256
|
+
}
|
|
257
|
+
if ('exists' in expression) {
|
|
258
|
+
const next = new Set(activeVariables);
|
|
259
|
+
for (const node of expression.exists.nodes)
|
|
260
|
+
next.add(node.variable.id);
|
|
261
|
+
return patternBranches(expression.exists.where, next);
|
|
262
|
+
}
|
|
263
|
+
if ('anyOf' in expression) {
|
|
264
|
+
const result = [];
|
|
265
|
+
for (const operand of expression.anyOf) {
|
|
266
|
+
result.push(...patternBranches(operand, activeVariables));
|
|
267
|
+
if (result.length > LIMITS.branches)
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
let result = [{ edges: [], variables: new Set(activeVariables) }];
|
|
273
|
+
for (const operand of expression.allOf) {
|
|
274
|
+
const operandBranches = patternBranches(operand, activeVariables);
|
|
275
|
+
const next = [];
|
|
276
|
+
for (const left of result) {
|
|
277
|
+
for (const right of operandBranches) {
|
|
278
|
+
next.push(mergeBranches(left, right));
|
|
279
|
+
if (next.length > LIMITS.branches)
|
|
280
|
+
return next;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
result = next;
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
function endpointKey(node) {
|
|
288
|
+
switch (node.kind) {
|
|
289
|
+
case 'subject':
|
|
290
|
+
return 'subject';
|
|
291
|
+
case 'object':
|
|
292
|
+
return 'object';
|
|
293
|
+
case 'variable':
|
|
294
|
+
return `variable:${node.id}`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function validateBranchConnectivity(branch, field) {
|
|
298
|
+
const adjacency = new Map();
|
|
299
|
+
const required = new Set(['subject', 'object']);
|
|
300
|
+
for (const id of branch.variables)
|
|
301
|
+
required.add(`variable:${id}`);
|
|
302
|
+
const connect = (left, right) => {
|
|
303
|
+
const leftSet = adjacency.get(left) ?? new Set();
|
|
304
|
+
leftSet.add(right);
|
|
305
|
+
adjacency.set(left, leftSet);
|
|
306
|
+
const rightSet = adjacency.get(right) ?? new Set();
|
|
307
|
+
rightSet.add(left);
|
|
308
|
+
adjacency.set(right, rightSet);
|
|
309
|
+
};
|
|
310
|
+
for (const edge of branch.edges) {
|
|
311
|
+
const source = endpointKey(edge.source);
|
|
312
|
+
const target = endpointKey(edge.target);
|
|
313
|
+
required.add(source);
|
|
314
|
+
required.add(target);
|
|
315
|
+
connect(source, target);
|
|
316
|
+
}
|
|
317
|
+
const visited = new Set();
|
|
318
|
+
const queue = ['subject'];
|
|
319
|
+
while (queue.length > 0) {
|
|
320
|
+
const current = queue.shift();
|
|
321
|
+
if (visited.has(current))
|
|
322
|
+
continue;
|
|
323
|
+
visited.add(current);
|
|
324
|
+
for (const adjacent of adjacency.get(current) ?? [])
|
|
325
|
+
queue.push(adjacent);
|
|
326
|
+
}
|
|
327
|
+
const disconnected = [...required].filter((vertex) => !visited.has(vertex));
|
|
328
|
+
if (disconnected.length > 0) {
|
|
329
|
+
fail(field, `Every normalized Policy branch must connect subject, object and every scoped variable; disconnected: ${disconnected.join(', ')}`, 'one connected relational proof', 'disconnected Pattern branch');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function validateVariableScopes(expression, field, active = new Set(), declared = new Set()) {
|
|
333
|
+
if ('source' in expression) {
|
|
334
|
+
for (const endpoint of [expression.source, expression.target]) {
|
|
335
|
+
if (endpoint.kind === 'variable' && !active.has(endpoint.id)) {
|
|
336
|
+
fail(field, `Policy variable ${endpoint.id} escaped the exists() callback that declared it`, 'variable used only inside its declaring exists().where', `out-of-scope variable ${endpoint.id}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if ('exists' in expression) {
|
|
342
|
+
if (expression.exists.nodes.length === 0) {
|
|
343
|
+
fail(field, 'exists() must declare at least one variable Node');
|
|
344
|
+
}
|
|
345
|
+
if (expression.exists.nodes.length > LIMITS.variablesPerExists) {
|
|
346
|
+
fail(field, `exists() declares ${expression.exists.nodes.length} variables; V1 allows ${LIMITS.variablesPerExists}`);
|
|
347
|
+
}
|
|
348
|
+
const next = new Set(active);
|
|
349
|
+
for (const node of expression.exists.nodes) {
|
|
350
|
+
const id = node.variable.id;
|
|
351
|
+
if (declared.has(id)) {
|
|
352
|
+
fail(field, `Policy variable ${id} is declared more than once`);
|
|
353
|
+
}
|
|
354
|
+
declared.add(id);
|
|
355
|
+
next.add(id);
|
|
356
|
+
}
|
|
357
|
+
validateVariableScopes(expression.exists.where, field, next, declared);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const operands = 'allOf' in expression ? expression.allOf : expression.anyOf;
|
|
361
|
+
for (const operand of operands)
|
|
362
|
+
validateVariableScopes(operand, field, active, declared);
|
|
363
|
+
}
|
|
364
|
+
function rewriteVariableIds(expression, mapping = new Map(), counter = { value: 0 }) {
|
|
365
|
+
const rewriteNode = (node) => {
|
|
366
|
+
if (node.kind !== 'variable')
|
|
367
|
+
return node;
|
|
368
|
+
const rewritten = mapping.get(node.id);
|
|
369
|
+
if (rewritten === undefined) {
|
|
370
|
+
fail('policies', `Policy variable ${node.id} has no active declaration`);
|
|
371
|
+
}
|
|
372
|
+
return { kind: 'variable', id: rewritten };
|
|
373
|
+
};
|
|
374
|
+
if ('source' in expression) {
|
|
375
|
+
return {
|
|
376
|
+
...expression,
|
|
377
|
+
source: rewriteNode(expression.source),
|
|
378
|
+
target: rewriteNode(expression.target),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if ('exists' in expression) {
|
|
382
|
+
const scoped = new Map(mapping);
|
|
383
|
+
const nodes = expression.exists.nodes.map((node) => {
|
|
384
|
+
const id = counter.value++;
|
|
385
|
+
scoped.set(node.variable.id, id);
|
|
386
|
+
return { variable: { kind: 'variable', id }, class: node.class };
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
exists: {
|
|
390
|
+
nodes,
|
|
391
|
+
where: rewriteVariableIds(expression.exists.where, scoped, counter),
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if ('allOf' in expression) {
|
|
396
|
+
return {
|
|
397
|
+
allOf: expression.allOf.map((operand) => rewriteVariableIds(operand, mapping, counter)),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
anyOf: expression.anyOf.map((operand) => rewriteVariableIds(operand, mapping, counter)),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function validatePattern(expression, field, ownerOrigin) {
|
|
405
|
+
validateVariableScopes(expression, field);
|
|
406
|
+
const depth = patternDepth(expression);
|
|
407
|
+
if (depth > LIMITS.depth) {
|
|
408
|
+
fail(field, `Policy Pattern depth ${depth} exceeds the V1 limit ${LIMITS.depth}`);
|
|
409
|
+
}
|
|
410
|
+
const branches = patternBranches(expression);
|
|
411
|
+
if (branches.length > LIMITS.branches) {
|
|
412
|
+
fail(field, `Policy Pattern has more than ${LIMITS.branches} normalized branches`);
|
|
413
|
+
}
|
|
414
|
+
for (const branch of branches) {
|
|
415
|
+
if (branch.edges.length > LIMITS.edgesPerBranch) {
|
|
416
|
+
fail(field, `Policy branch has ${branch.edges.length} edge predicates; V1 allows ${LIMITS.edgesPerBranch}`);
|
|
417
|
+
}
|
|
418
|
+
validateBranchConnectivity(branch, field);
|
|
419
|
+
}
|
|
420
|
+
const domains = new Set([ownerOrigin]);
|
|
421
|
+
collectPatternDomains(expression, domains);
|
|
422
|
+
if (domains.size > LIMITS.domains) {
|
|
423
|
+
fail(field, `Policy references ${domains.size} Domains; V1 allows ${LIMITS.domains}`);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
depth,
|
|
427
|
+
edgeCounts: branches.map((branch) => branch.edges.length),
|
|
428
|
+
domains,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function callableDepth(expression) {
|
|
432
|
+
if ('check' in expression)
|
|
433
|
+
return 1;
|
|
434
|
+
const operands = 'allOf' in expression ? expression.allOf : expression.anyOf;
|
|
435
|
+
return 1 + Math.max(...operands.map(callableDepth));
|
|
436
|
+
}
|
|
437
|
+
function callableBranches(expression) {
|
|
438
|
+
if ('check' in expression)
|
|
439
|
+
return 1;
|
|
440
|
+
if ('allOf' in expression) {
|
|
441
|
+
return expression.allOf.reduce((total, operand) => total * callableBranches(operand), 1);
|
|
442
|
+
}
|
|
443
|
+
return expression.anyOf.reduce((total, operand) => total + callableBranches(operand), 0);
|
|
444
|
+
}
|
|
445
|
+
function callableLeaves(expression) {
|
|
446
|
+
if ('check' in expression)
|
|
447
|
+
return 1;
|
|
448
|
+
const operands = 'allOf' in expression ? expression.allOf : expression.anyOf;
|
|
449
|
+
return operands.reduce((total, operand) => total + callableLeaves(operand), 0);
|
|
450
|
+
}
|
|
451
|
+
function validateCallable(expression, field) {
|
|
452
|
+
const depth = callableDepth(expression);
|
|
453
|
+
const branches = callableBranches(expression);
|
|
454
|
+
const leaves = callableLeaves(expression);
|
|
455
|
+
if (depth > LIMITS.depth) {
|
|
456
|
+
fail(field, `Callable Policy depth ${depth} exceeds the V1 limit ${LIMITS.depth}`);
|
|
457
|
+
}
|
|
458
|
+
if (branches > LIMITS.branches) {
|
|
459
|
+
fail(field, `Callable Policy has ${branches} normalized branches; V1 allows ${LIMITS.branches}`);
|
|
460
|
+
}
|
|
461
|
+
if (leaves > LIMITS.callableLeaves) {
|
|
462
|
+
fail(field, `Callable Policy has ${leaves} check leaves; V1 allows ${LIMITS.callableLeaves}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function zodObjectShape(schema) {
|
|
466
|
+
const def = getZodDef(schema);
|
|
467
|
+
if (!def || !['ZodObject', 'object'].includes(getZodTypeName(def) ?? ''))
|
|
468
|
+
return null;
|
|
469
|
+
const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
|
|
470
|
+
return shape && typeof shape === 'object' ? shape : null;
|
|
471
|
+
}
|
|
472
|
+
export class PolicyCompiler {
|
|
473
|
+
schema;
|
|
474
|
+
localDefs;
|
|
475
|
+
importedDefs = [];
|
|
476
|
+
policyNames = new Map();
|
|
477
|
+
declarations = new Map();
|
|
478
|
+
measures = new Map();
|
|
479
|
+
nextVariableId = 0;
|
|
480
|
+
typing;
|
|
481
|
+
constructor(schema) {
|
|
482
|
+
this.schema = schema;
|
|
483
|
+
this.localDefs = buildDescriptorMap(schema);
|
|
484
|
+
for (const imported of schema.imports) {
|
|
485
|
+
this.importedDefs.push({ origin: imported.domain, map: buildDescriptorMap(imported) });
|
|
486
|
+
}
|
|
487
|
+
for (const [name, definition] of Object.entries(schema.policies ?? {})) {
|
|
488
|
+
this.policyNames.set(definition, name);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
get hasStandalonePolicies() {
|
|
492
|
+
return Object.values(this.schema.functions).some((fn) => Boolean(fn.config.policy));
|
|
493
|
+
}
|
|
494
|
+
compileDeclarations() {
|
|
495
|
+
const result = {};
|
|
496
|
+
for (const [name] of Object.entries(this.schema.policies ?? {})) {
|
|
497
|
+
result[name] = this.compileNamed(name, []);
|
|
498
|
+
}
|
|
499
|
+
for (const name of Object.keys(result))
|
|
500
|
+
this.measureNamed(name, []);
|
|
501
|
+
this.typing = new PolicyTyping(this.schema, result);
|
|
502
|
+
this.typing.validateDeclarations();
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
compileNodePolicies(definition, field) {
|
|
506
|
+
const configured = definition.config.policies;
|
|
507
|
+
if (!configured)
|
|
508
|
+
return undefined;
|
|
509
|
+
if (!configured.read)
|
|
510
|
+
return {};
|
|
511
|
+
const ref = this.resolvePolicyRef(configured.read, `${field}.read`);
|
|
512
|
+
this.policyTyping().assertAccepts(ref, this.policyTyping().contractOf(definition, `${field}.read`), `${field}.read`);
|
|
513
|
+
return { read: ref };
|
|
514
|
+
}
|
|
515
|
+
compileMethodPolicy(definition, owner, field) {
|
|
516
|
+
const callback = definition.config.policy;
|
|
517
|
+
if (!callback)
|
|
518
|
+
return undefined;
|
|
519
|
+
if (!isNode(owner)) {
|
|
520
|
+
return fail(field, `Application Policies are supported on Node Methods only; '${owner.__kind}' is not a Node definition`);
|
|
521
|
+
}
|
|
522
|
+
const params = this.buildInputBindings(definition.config.params ?? {}, `${field}.input`);
|
|
523
|
+
const builder = {
|
|
524
|
+
input: params,
|
|
525
|
+
params,
|
|
526
|
+
...this.callableOperators(field),
|
|
527
|
+
};
|
|
528
|
+
if (!definition.config.static) {
|
|
529
|
+
builder.self = makeCheckBinding({ kind: 'self' }, owner);
|
|
530
|
+
}
|
|
531
|
+
let built;
|
|
532
|
+
try {
|
|
533
|
+
built = callback(builder);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
if (error instanceof SchemaValidationError)
|
|
537
|
+
throw error;
|
|
538
|
+
return fail(field, `Failed to build callable Policy '${field}': ${String(error)}`);
|
|
539
|
+
}
|
|
540
|
+
const normalized = normalizeCheck(getCheckExpression(built, field));
|
|
541
|
+
validateCallable(normalized, field);
|
|
542
|
+
return normalized;
|
|
543
|
+
}
|
|
544
|
+
compileFunctionPolicy(definition, field) {
|
|
545
|
+
const callback = definition.config.policy;
|
|
546
|
+
if (!callback)
|
|
547
|
+
return undefined;
|
|
548
|
+
const shape = zodObjectShape(definition.config.input);
|
|
549
|
+
if (!shape) {
|
|
550
|
+
return fail(`${field}.input`, `A standalone Function with a Policy must use one direct Zod object input`, 'z.object({...})', 'non-object input');
|
|
551
|
+
}
|
|
552
|
+
const builder = {
|
|
553
|
+
input: this.buildInputBindings(shape, `${field}.input`),
|
|
554
|
+
...this.callableOperators(field),
|
|
555
|
+
};
|
|
556
|
+
let built;
|
|
557
|
+
try {
|
|
558
|
+
built = callback(builder);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (error instanceof SchemaValidationError)
|
|
562
|
+
throw error;
|
|
563
|
+
return fail(field, `Failed to build callable Policy '${field}': ${String(error)}`);
|
|
564
|
+
}
|
|
565
|
+
const normalized = normalizeCheck(getCheckExpression(built, field));
|
|
566
|
+
validateCallable(normalized, field);
|
|
567
|
+
return normalized;
|
|
568
|
+
}
|
|
569
|
+
compileNamed(name, stack) {
|
|
570
|
+
const cached = this.declarations.get(name);
|
|
571
|
+
if (cached)
|
|
572
|
+
return cached;
|
|
573
|
+
if (stack.includes(name)) {
|
|
574
|
+
return fail(`policies.${name}`, `Named Policy composition cycle: ${[...stack, name].join(' -> ')}`);
|
|
575
|
+
}
|
|
576
|
+
const definition = this.schema.policies?.[name];
|
|
577
|
+
if (!definition)
|
|
578
|
+
return fail(`policies.${name}`, `Unknown local Policy '${name}'`);
|
|
579
|
+
const config = definition.input;
|
|
580
|
+
let declaration;
|
|
581
|
+
if ('match' in config) {
|
|
582
|
+
this.nextVariableId = 0;
|
|
583
|
+
let built;
|
|
584
|
+
try {
|
|
585
|
+
built = config.match(this.patternBuilder(`policies.${name}.match`));
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (error instanceof SchemaValidationError)
|
|
589
|
+
throw error;
|
|
590
|
+
return fail(`policies.${name}.match`, `Failed to build Policy '${name}': ${String(error)}`);
|
|
591
|
+
}
|
|
592
|
+
const raw = getPatternExpression(built, `policies.${name}.match`);
|
|
593
|
+
const normalized = normalizePattern(raw);
|
|
594
|
+
validateVariableScopes(normalized, `policies.${name}.match`);
|
|
595
|
+
const alphaNormalized = alphaNormalizePattern(normalized).ir;
|
|
596
|
+
const canonical = normalizePattern(rewriteVariableIds(alphaNormalized));
|
|
597
|
+
this.measures.set(name, validatePattern(canonical, `policies.${name}.match`, this.schema.domain));
|
|
598
|
+
declaration =
|
|
599
|
+
config.description !== undefined
|
|
600
|
+
? { description: config.description, match: canonical }
|
|
601
|
+
: { match: canonical };
|
|
602
|
+
}
|
|
603
|
+
else if (config.__type === 'policy-composition') {
|
|
604
|
+
if (config.policies.length === 0) {
|
|
605
|
+
return fail(`policies.${name}`, `Named Policy composition must be non-empty`);
|
|
606
|
+
}
|
|
607
|
+
const refs = config.policies.map((operand) => {
|
|
608
|
+
const ref = this.resolvePolicyRef(operand, `policies.${name}.${config.operator}`);
|
|
609
|
+
this.compileNamed(ref.name, [...stack, name]);
|
|
610
|
+
return ref;
|
|
611
|
+
});
|
|
612
|
+
const normalized = uniqueSorted(refs, (ref) => `${ref.origin}\u0000${ref.kind}\u0000${ref.name}`);
|
|
613
|
+
declaration = config.operator === 'allOf' ? { allOf: normalized } : { anyOf: normalized };
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
return fail(`policies.${name}`, `Policy '${name}' must contain exactly one match or named composition expression`);
|
|
617
|
+
}
|
|
618
|
+
this.declarations.set(name, declaration);
|
|
619
|
+
return declaration;
|
|
620
|
+
}
|
|
621
|
+
measureNamed(name, stack) {
|
|
622
|
+
const cached = this.measures.get(name);
|
|
623
|
+
if (cached)
|
|
624
|
+
return cached;
|
|
625
|
+
if (stack.includes(name)) {
|
|
626
|
+
return fail(`policies.${name}`, `Named Policy composition cycle at '${name}'`);
|
|
627
|
+
}
|
|
628
|
+
const declaration = this.declarations.get(name) ?? this.compileNamed(name, stack);
|
|
629
|
+
if ('match' in declaration)
|
|
630
|
+
return this.measures.get(name);
|
|
631
|
+
const operator = 'allOf' in declaration ? 'allOf' : 'anyOf';
|
|
632
|
+
const refs = 'allOf' in declaration ? declaration.allOf : declaration.anyOf;
|
|
633
|
+
const operands = refs.map((ref) => this.measureNamed(ref.name, [...stack, name]));
|
|
634
|
+
const depth = 1 + Math.max(...operands.map((operand) => operand.depth));
|
|
635
|
+
let edgeCounts;
|
|
636
|
+
if (operator === 'anyOf') {
|
|
637
|
+
edgeCounts = operands.flatMap((operand) => operand.edgeCounts);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
edgeCounts = [0];
|
|
641
|
+
for (const operand of operands) {
|
|
642
|
+
edgeCounts = edgeCounts.flatMap((left) => operand.edgeCounts.map((right) => left + right));
|
|
643
|
+
if (edgeCounts.length > LIMITS.branches)
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const domains = new Set([this.schema.domain]);
|
|
648
|
+
for (const operand of operands) {
|
|
649
|
+
for (const origin of operand.domains)
|
|
650
|
+
domains.add(origin);
|
|
651
|
+
}
|
|
652
|
+
if (depth > LIMITS.depth) {
|
|
653
|
+
fail(`policies.${name}`, `Expanded named Policy depth ${depth} exceeds the V1 limit ${LIMITS.depth}`);
|
|
654
|
+
}
|
|
655
|
+
if (edgeCounts.length > LIMITS.branches) {
|
|
656
|
+
fail(`policies.${name}`, `Expanded named Policy has more than ${LIMITS.branches} normalized branches`);
|
|
657
|
+
}
|
|
658
|
+
if (Math.max(...edgeCounts) > LIMITS.edgesPerBranch) {
|
|
659
|
+
fail(`policies.${name}`, `Expanded named Policy exceeds ${LIMITS.edgesPerBranch} edge predicates in one branch`);
|
|
660
|
+
}
|
|
661
|
+
if (domains.size > LIMITS.domains) {
|
|
662
|
+
fail(`policies.${name}`, `Expanded named Policy references ${domains.size} Domains; V1 allows ${LIMITS.domains}`);
|
|
663
|
+
}
|
|
664
|
+
const measure = { depth, edgeCounts, domains };
|
|
665
|
+
this.measures.set(name, measure);
|
|
666
|
+
return measure;
|
|
667
|
+
}
|
|
668
|
+
patternBuilder(field) {
|
|
669
|
+
const subject = makePatternBinding({ kind: 'subject' });
|
|
670
|
+
const object = makePatternBinding({ kind: 'object' });
|
|
671
|
+
const edge = (input) => {
|
|
672
|
+
if (!input || typeof input !== 'object')
|
|
673
|
+
return fail(field, 'edge() expects an object');
|
|
674
|
+
const edgeClass = this.resolveLazyDefinition(input.class, `${field}.edge.class`);
|
|
675
|
+
if (!isDefinition(edgeClass) || !isEdgeClass(edgeClass)) {
|
|
676
|
+
return fail(`${field}.edge.class`, `Policy edge() requires an exact Edge Class`);
|
|
677
|
+
}
|
|
678
|
+
let repeat = input.repeat;
|
|
679
|
+
if (repeat) {
|
|
680
|
+
const valid = Number.isInteger(repeat.min) &&
|
|
681
|
+
Number.isInteger(repeat.max) &&
|
|
682
|
+
repeat.min >= 0 &&
|
|
683
|
+
repeat.min <= repeat.max &&
|
|
684
|
+
repeat.max <= LIMITS.repeat;
|
|
685
|
+
if (!valid) {
|
|
686
|
+
return fail(`${field}.edge.repeat`, `Policy repeat must satisfy 0 <= min <= max <= ${LIMITS.repeat}`);
|
|
687
|
+
}
|
|
688
|
+
if (repeat.min === 1 && repeat.max === 1)
|
|
689
|
+
repeat = undefined;
|
|
690
|
+
}
|
|
691
|
+
return makePatternExpression({
|
|
692
|
+
source: this.patternEndpoint(input.source, `${field}.edge.source`),
|
|
693
|
+
class: this.resolveDefinitionRef(edgeClass, 'edge-class', `${field}.edge.class`),
|
|
694
|
+
target: this.patternEndpoint(input.target, `${field}.edge.target`),
|
|
695
|
+
...(repeat ? { repeat } : {}),
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
const allOf = (first, ...rest) => makePatternExpression({
|
|
699
|
+
allOf: [first, ...rest].map((value) => getPatternExpression(value, field)),
|
|
700
|
+
});
|
|
701
|
+
const anyOf = (first, ...rest) => makePatternExpression({
|
|
702
|
+
anyOf: [first, ...rest].map((value) => getPatternExpression(value, field)),
|
|
703
|
+
});
|
|
704
|
+
const exists = (build) => {
|
|
705
|
+
if (typeof build !== 'function')
|
|
706
|
+
return fail(`${field}.exists`, 'exists() expects a callback');
|
|
707
|
+
const nodes = [];
|
|
708
|
+
const where = build({
|
|
709
|
+
node: (definition) => {
|
|
710
|
+
const nodeClass = this.resolveLazyDefinition(definition, `${field}.exists.node`);
|
|
711
|
+
if (!isDefinition(nodeClass) || !isNodeClass(nodeClass)) {
|
|
712
|
+
return fail(`${field}.exists.node`, `exists().node() requires an exact Node Class`);
|
|
713
|
+
}
|
|
714
|
+
if (nodes.length >= LIMITS.variablesPerExists) {
|
|
715
|
+
return fail(`${field}.exists.nodes`, `exists() may declare at most ${LIMITS.variablesPerExists} variable Nodes`);
|
|
716
|
+
}
|
|
717
|
+
const variable = { kind: 'variable', id: this.nextVariableId++ };
|
|
718
|
+
nodes.push({
|
|
719
|
+
variable,
|
|
720
|
+
class: this.resolveDefinitionRef(nodeClass, 'node-class', `${field}.exists.node`),
|
|
721
|
+
});
|
|
722
|
+
return makePatternBinding(variable);
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
if (nodes.length === 0) {
|
|
726
|
+
return fail(`${field}.exists`, `exists() must declare at least one variable Node`);
|
|
727
|
+
}
|
|
728
|
+
return makePatternExpression({
|
|
729
|
+
exists: {
|
|
730
|
+
nodes: nodes,
|
|
731
|
+
where: getPatternExpression(where, `${field}.exists.where`),
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
};
|
|
735
|
+
return Object.freeze({ subject, object, edge, exists, allOf, anyOf });
|
|
736
|
+
}
|
|
737
|
+
patternEndpoint(value, field) {
|
|
738
|
+
if (typeof value === 'object' && value !== null && PATTERN_BINDING in value) {
|
|
739
|
+
return value[PATTERN_BINDING];
|
|
740
|
+
}
|
|
741
|
+
return fail(field, `Policy edge endpoint must be subject, object, or an exists() variable`);
|
|
742
|
+
}
|
|
743
|
+
callableOperators(field) {
|
|
744
|
+
const check = (policy, object) => {
|
|
745
|
+
if (typeof object !== 'object' || object === null || !(CHECK_BINDING in object)) {
|
|
746
|
+
return fail(`${field}.check`, `check() object must be self or one direct ref() input field`);
|
|
747
|
+
}
|
|
748
|
+
const binding = object[CHECK_BINDING];
|
|
749
|
+
this.assertDirectNode(binding.target, `${field}.check.object`);
|
|
750
|
+
const ref = this.resolvePolicyRef(policy, `${field}.check`);
|
|
751
|
+
this.policyTyping().assertAccepts(ref, this.policyTyping().contractOf(binding.target, `${field}.check.object`), `${field}.check`);
|
|
752
|
+
return makeCheckExpression({
|
|
753
|
+
check: ref,
|
|
754
|
+
object: binding.object,
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
const allOf = (first, ...rest) => makeCheckExpression({
|
|
758
|
+
allOf: [first, ...rest].map((value) => getCheckExpression(value, field)),
|
|
759
|
+
});
|
|
760
|
+
const anyOf = (first, ...rest) => makeCheckExpression({
|
|
761
|
+
anyOf: [first, ...rest].map((value) => getCheckExpression(value, field)),
|
|
762
|
+
});
|
|
763
|
+
return { check, allOf, anyOf };
|
|
764
|
+
}
|
|
765
|
+
buildInputBindings(shape, field) {
|
|
766
|
+
const result = {};
|
|
767
|
+
for (const [name, schema] of Object.entries(shape)) {
|
|
768
|
+
if (!hasRefTag(schema))
|
|
769
|
+
continue;
|
|
770
|
+
const meta = getRefMeta(schema);
|
|
771
|
+
if (!meta)
|
|
772
|
+
continue;
|
|
773
|
+
const target = this.assertDirectNode(meta.target, `${field}.${name}`);
|
|
774
|
+
result[name] = makeCheckBinding({ kind: 'input', field: name }, target);
|
|
775
|
+
}
|
|
776
|
+
return Object.freeze(result);
|
|
777
|
+
}
|
|
778
|
+
assertDirectNode(value, field) {
|
|
779
|
+
if (!('__kind' in value) || !isNode(value)) {
|
|
780
|
+
return fail(field, `Callable Policy objects must be direct ref() values to Node definitions`);
|
|
781
|
+
}
|
|
782
|
+
const definition = value;
|
|
783
|
+
this.resolveAnyDefinition(definition, field);
|
|
784
|
+
return definition;
|
|
785
|
+
}
|
|
786
|
+
resolvePolicyRef(input, field) {
|
|
787
|
+
const definition = this.resolveLazyPolicy(input, field);
|
|
788
|
+
const name = this.policyNames.get(definition);
|
|
789
|
+
if (!name) {
|
|
790
|
+
return fail(field, `Policy uses must reference a named Policy registered in this schema`, 'local named Policy', 'unregistered or foreign Policy');
|
|
791
|
+
}
|
|
792
|
+
return { origin: this.schema.domain, kind: 'policy', name };
|
|
793
|
+
}
|
|
794
|
+
resolveLazyPolicy(input, field) {
|
|
795
|
+
const value = this.resolveLazyDefinition(input, field);
|
|
796
|
+
if (typeof value !== 'object' || value === null || value.__type !== 'policy') {
|
|
797
|
+
return fail(field, `Policy reference must resolve to a value created by definePolicy()`);
|
|
798
|
+
}
|
|
799
|
+
return value;
|
|
800
|
+
}
|
|
801
|
+
resolveLazyDefinition(input, field) {
|
|
802
|
+
if (typeof input !== 'function')
|
|
803
|
+
return input;
|
|
804
|
+
try {
|
|
805
|
+
return input();
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
return fail(field, `Failed to resolve lazy Policy reference: ${String(error)}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
policyTyping() {
|
|
812
|
+
return (this.typing ?? fail('policies', 'Policy declarations must compile before Policy use sites'));
|
|
813
|
+
}
|
|
814
|
+
resolveDefinitionRef(definition, kind, field) {
|
|
815
|
+
if (definition.__kind !== kind) {
|
|
816
|
+
return fail(field, `Expected ${kind}, received ${definition.__kind}`);
|
|
817
|
+
}
|
|
818
|
+
const resolved = this.resolveAnyDefinition(definition, field);
|
|
819
|
+
return { origin: resolved.origin, kind, name: resolved.descriptor.name };
|
|
820
|
+
}
|
|
821
|
+
resolveAnyDefinition(definition, field) {
|
|
822
|
+
const local = this.localDefs.get(definition);
|
|
823
|
+
if (local)
|
|
824
|
+
return { origin: this.schema.domain, descriptor: local };
|
|
825
|
+
for (const imported of this.importedDefs) {
|
|
826
|
+
const descriptor = imported.map.get(definition);
|
|
827
|
+
if (descriptor)
|
|
828
|
+
return { origin: imported.origin, descriptor };
|
|
829
|
+
}
|
|
830
|
+
const transitive = findDefTransitive(this.schema, definition);
|
|
831
|
+
if (transitive) {
|
|
832
|
+
return fail(field, `Policy relation refs may use only the owning Domain or an exact direct dependency; '${transitive.origin}' is transitive via ${transitive.path.join(' -> ')}`);
|
|
833
|
+
}
|
|
834
|
+
return fail(field, `Policy references a definition not registered in this schema or its imports`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=policies.js.map
|