avouch 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +356 -0
  4. package/SPEC.md +379 -0
  5. package/dist/cli/avouch.d.ts +2 -0
  6. package/dist/cli/avouch.js +68 -0
  7. package/dist/cli/check.d.ts +86 -0
  8. package/dist/cli/check.js +333 -0
  9. package/dist/cli/facts.d.ts +3 -0
  10. package/dist/cli/facts.js +157 -0
  11. package/dist/cli/gate.d.ts +3 -0
  12. package/dist/cli/gate.js +149 -0
  13. package/dist/cli/init.d.ts +21 -0
  14. package/dist/cli/init.js +282 -0
  15. package/dist/cli/jsonl.d.ts +5 -0
  16. package/dist/cli/jsonl.js +20 -0
  17. package/dist/cli/query.d.ts +2 -0
  18. package/dist/cli/query.js +138 -0
  19. package/dist/cli/reconcile.d.ts +9 -0
  20. package/dist/cli/reconcile.js +131 -0
  21. package/dist/src/checker.d.ts +3 -0
  22. package/dist/src/checker.js +1136 -0
  23. package/dist/src/contract.d.ts +135 -0
  24. package/dist/src/contract.js +3 -0
  25. package/dist/src/coverage.d.ts +65 -0
  26. package/dist/src/coverage.js +237 -0
  27. package/dist/src/evaluate.d.ts +66 -0
  28. package/dist/src/evaluate.js +535 -0
  29. package/dist/src/expr.d.ts +209 -0
  30. package/dist/src/expr.js +497 -0
  31. package/dist/src/index.d.ts +11 -0
  32. package/dist/src/index.js +11 -0
  33. package/dist/src/ledger.d.ts +96 -0
  34. package/dist/src/ledger.js +241 -0
  35. package/dist/src/markdown.d.ts +66 -0
  36. package/dist/src/markdown.js +701 -0
  37. package/dist/src/patch.d.ts +6 -0
  38. package/dist/src/patch.js +37 -0
  39. package/dist/src/query.d.ts +333 -0
  40. package/dist/src/query.js +739 -0
  41. package/dist/src/reconcile.d.ts +61 -0
  42. package/dist/src/reconcile.js +151 -0
  43. package/dist/src/stage1-check.d.ts +31 -0
  44. package/dist/src/stage1-check.js +329 -0
  45. package/dist/src/touched.d.ts +1 -0
  46. package/dist/src/touched.js +9 -0
  47. package/dist/src/unknown.d.ts +10 -0
  48. package/dist/src/unknown.js +173 -0
  49. package/docs/adapter-guide.md +187 -0
  50. package/docs/markdown-convention.md +635 -0
  51. package/ontology.schema.json +1757 -0
  52. package/package.json +57 -0
  53. package/templates/agents-block.md +12 -0
  54. package/vocab.ttl +524 -0
@@ -0,0 +1,535 @@
1
+ // Decision evaluation: evalValue / evalPredicate (two levels, Kleene logic) and decide (invocation layer, then decision table).
2
+ // Deterministic: no wall clock, no randomness, no global mutable state; `now` comes from the caller.
3
+ // Invalid input (ontology, snapshot, scope argument) throws; it is never a result.
4
+ import { checkPermissionWhen, checkPredicate, fitsType, isPureLink, own, projectedFields, RESERVED_NAMES, resolvesInDomain, TIMESTAMP_MAX, TIMESTAMP_MIN } from "./expr.js";
5
+ // The fixed invocation result set of the format (defined in ./expr). decide never returns the first two (the idempotency
6
+ // envelope is not implemented here: it has no idempotency key).
7
+ export { INVOCATION_RESULTS } from "./expr.js";
8
+ // Thrown when evaluation reads a derived property that has no expression; decide turns it into INDETERMINATE.
9
+ export class UndefinedDerived extends Error {
10
+ derived;
11
+ constructor(derived) {
12
+ super(`derived property ${derived} has no expression`);
13
+ this.derived = derived;
14
+ this.name = "UndefinedDerived";
15
+ }
16
+ }
17
+ // A field of a snapshot row. An absent key, or the value undefined, is an incomplete snapshot and throws; null is a value.
18
+ function field(row, key, what) {
19
+ if (!Object.hasOwn(row, key) || row[key] === undefined)
20
+ throw new Error(`snapshot ${what} has no ${key}`);
21
+ return row[key];
22
+ }
23
+ // A field that holds an id (a foreign-key via, the scope field): a string or null; another value is a corrupt snapshot.
24
+ function idField(row, key, what, t) {
25
+ const v = field(row, key, what);
26
+ if (v !== null && typeof v !== "string")
27
+ throw new Error(`snapshot ${what}: ${key} = ${JSON.stringify(v)} is not a string id`);
28
+ if (v !== null && t !== undefined && !fitsType(v, t))
29
+ throw new Error(`snapshot ${what}: ${key} = ${JSON.stringify(v)} does not fit its declared type`);
30
+ return v;
31
+ }
32
+ // One evaluation session: caches filtered rows per type and derived values per (id, row id).
33
+ class Session {
34
+ ont;
35
+ env;
36
+ rows = new Map();
37
+ links = new Map();
38
+ derivedCache = new Map();
39
+ constructor(ont, env) {
40
+ this.ont = ont;
41
+ this.env = env;
42
+ }
43
+ mode(type) {
44
+ const s = this.ont.scope;
45
+ if (s === undefined)
46
+ return "none";
47
+ if (s.root?.type === type)
48
+ return "root";
49
+ if (s.global?.some((g) => g.type === type))
50
+ return "global";
51
+ return "tenant";
52
+ }
53
+ // A null scope field matches no scope, also when the scope is null.
54
+ inScope(v) {
55
+ return v !== null && v === this.env.scope;
56
+ }
57
+ visible(type, row) {
58
+ const m = this.mode(type);
59
+ if (m === "tenant")
60
+ return this.inScope(idField(row, this.ont.scope.by, `${type} row ${String(row.id)}`, own(this.ont.objectTypes[type]?.properties, this.ont.scope.by)?.type));
61
+ if (m === "root")
62
+ return row.id === this.env.scope;
63
+ return true;
64
+ }
65
+ allRows(type) {
66
+ const rows = own(this.env.snapshot.objects, type);
67
+ if (rows === undefined)
68
+ throw new Error(`snapshot lacks object type ${type}`);
69
+ for (const r of rows)
70
+ if (typeof r.id !== "string")
71
+ throw new Error(`a ${type} row has no string id`);
72
+ return rows;
73
+ }
74
+ // Rows of `type` after the scope filter, by id. Lookups by id always run on filtered rows: ids repeat across tenants.
75
+ byId(type) {
76
+ let m = this.rows.get(type);
77
+ if (m === undefined) {
78
+ m = new Map();
79
+ for (const r of this.allRows(type)) {
80
+ if (!this.visible(type, r))
81
+ continue;
82
+ if (m.has(r.id))
83
+ throw new Error(`snapshot has two ${type} rows with id ${String(r.id)} in one scope`);
84
+ m.set(r.id, r);
85
+ }
86
+ this.rows.set(type, m);
87
+ }
88
+ return m;
89
+ }
90
+ linkRows(link) {
91
+ let rows = this.links.get(link.id);
92
+ if (rows === undefined) {
93
+ const all = own(this.env.snapshot.links, link.id);
94
+ if (all === undefined)
95
+ throw new Error(`snapshot lacks link ${link.id}`);
96
+ for (const r of all)
97
+ for (const k of ["from", "to"]) {
98
+ const v = field(r, k, `${link.id} link row`);
99
+ if (typeof v !== "string")
100
+ throw new Error(`snapshot ${link.id} link row: ${k} = ${JSON.stringify(v)} is not a string id`);
101
+ }
102
+ const by = this.ont.scope?.by;
103
+ const tenantEnd = this.mode(link.from) === "tenant" || this.mode(link.to) === "tenant";
104
+ rows = by !== undefined && tenantEnd ? all.filter((r) => this.inScope(idField(r, by, `${link.id} link row`))) : all;
105
+ this.links.set(link.id, rows);
106
+ }
107
+ return rows;
108
+ }
109
+ isPureLink(link) {
110
+ return isPureLink(this.ont, link);
111
+ }
112
+ cachedDerived(id, row, compute) {
113
+ const k = `${id}\u0000${String(row.id)}`;
114
+ if (!this.derivedCache.has(k))
115
+ this.derivedCache.set(k, compute());
116
+ return this.derivedCache.get(k);
117
+ }
118
+ }
119
+ const paramType = (ont, action, name) => action === undefined ? undefined : own(ont.actionTypes[action]?.parameters, name)?.type;
120
+ function objVar(s, f, name) {
121
+ if (Object.hasOwn(f.vars, name))
122
+ return f.vars[name];
123
+ if (f.input === true)
124
+ return undefined; // before binding: no lookup, readRef gives the given value
125
+ const t = paramType(s.ont, f.action, name);
126
+ if (t === undefined || typeof t === "string" || !("ref" in t))
127
+ return undefined;
128
+ const v = own(s.env.bindings, name);
129
+ if (v === null || v === undefined)
130
+ return { type: t.ref, row: null };
131
+ const row = s.byId(t.ref).get(v);
132
+ if (row === undefined)
133
+ throw new Error(`binding ${name} = ${String(v)} is not a visible ${t.ref}`);
134
+ return { type: t.ref, row };
135
+ }
136
+ function readRef(s, f, path) {
137
+ const parts = path.split(".");
138
+ if (parts.length > 2 || parts.some((p) => p === ""))
139
+ throw new Error(`ref ${path}: at most one dot`);
140
+ const [head, prop] = parts;
141
+ if (head === "now") {
142
+ if (prop !== undefined)
143
+ throw new Error("now has no properties");
144
+ return s.env.now;
145
+ }
146
+ if (head === "actor") {
147
+ if (prop === "id")
148
+ return s.env.actor.id;
149
+ if (prop === "keys")
150
+ return s.env.actor.keys;
151
+ if (prop === "kind")
152
+ return s.env.actor.kind;
153
+ throw new Error(`actor.${prop ?? ""} is not readable; use actor.id, actor.keys or actor.kind`);
154
+ }
155
+ const o = objVar(s, f, head);
156
+ if (prop === undefined) {
157
+ if (o !== undefined)
158
+ return o.row === null ? null : o.row.id;
159
+ if (f.action === undefined)
160
+ throw new Error(`${head} is not visible in a derived property expression`);
161
+ return own(s.env.bindings, head) ?? null;
162
+ }
163
+ if (o === undefined)
164
+ throw new Error(`${head} is not an object binding`);
165
+ if (o.row === null)
166
+ return null;
167
+ const v = field(o.row, prop, `${o.type} row ${String(o.row.id)}`);
168
+ // A non-null value must fit the declared type; otherwise the snapshot is malformed.
169
+ const t = own(s.ont.objectTypes[o.type]?.properties, prop)?.type;
170
+ if (v !== null && t !== undefined && !fitsType(v, t))
171
+ throw new Error(`snapshot ${o.type} row ${String(o.row.id)}: ${prop} = ${JSON.stringify(v)} does not fit its declared type`);
172
+ return v;
173
+ }
174
+ // Out of range means malformed input (snapshot, duration, now): it throws, it is not U.
175
+ function inRange(t, where) {
176
+ if (!(t >= TIMESTAMP_MIN && t <= TIMESTAMP_MAX))
177
+ throw new Error(`${where}: timestamp out of supported range (${t})`);
178
+ return t;
179
+ }
180
+ function dateIn(t, tz) {
181
+ const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(t));
182
+ const get = (k) => parts.find((x) => x.type === k).value;
183
+ // Timestamps are bounded so the year is 1..9999; pad it to 4 digits so dates compare as strings.
184
+ return `${get("year").padStart(4, "0")}-${get("month")}-${get("day")}`;
185
+ }
186
+ function value(s, f, e) {
187
+ if ("ref" in e)
188
+ return readRef(s, f, e.ref);
189
+ if ("lit" in e)
190
+ return e.lit;
191
+ if ("plus" in e) {
192
+ const [a, b] = evalAll([() => value(s, f, e.plus[0]), () => value(s, f, e.plus[1])]);
193
+ if (a === null || b === null)
194
+ return null;
195
+ const r = a + 1000 * b;
196
+ return inRange(r, "plus");
197
+ }
198
+ if ("dateIn" in e) {
199
+ const [a, b] = evalAll([() => value(s, f, e.dateIn[0]), () => value(s, f, e.dateIn[1])]);
200
+ if (a === null || b === null)
201
+ return null;
202
+ inRange(a, "dateIn");
203
+ return dateIn(a, b);
204
+ }
205
+ if ("derived" in e) {
206
+ const o = objVar(s, f, e.derived.of.ref);
207
+ if (o === undefined)
208
+ throw new Error(`${e.derived.of.ref} is not an object binding`);
209
+ if (o.row === null)
210
+ return null;
211
+ const id = e.derived.id;
212
+ const dp = own(s.ont.derivedProperties, id);
213
+ if (dp === undefined)
214
+ throw new Error(`derived property ${id} is not declared`);
215
+ const expr = dp.expr;
216
+ if (expr === undefined)
217
+ throw new UndefinedDerived(id);
218
+ const row = o.row;
219
+ const frame = { action: undefined, vars: { self: { type: dp.of, row } } };
220
+ // A predicate root (a Boolean derived property): T / F / U give true / false / null.
221
+ return s.cachedDerived(id, row, () => (isValueNode(expr) ? value(s, frame, expr) : fromTV(predicate(s, frame, expr))));
222
+ }
223
+ throw new Error(`not a value expression: ${Object.keys(e)[0]}`);
224
+ }
225
+ const isValueNode = (e) => "ref" in e || "lit" in e || "plus" in e || "dateIn" in e || "derived" in e;
226
+ const fromTV = (x) => (x === "T" ? true : x === "F" ? false : null);
227
+ // A boolean value in a predicate position: true / false / null give T / F / U.
228
+ function toTV(v) {
229
+ if (v === null)
230
+ return "U";
231
+ if (typeof v !== "boolean")
232
+ throw new Error(`a predicate position holds a non-boolean value ${JSON.stringify(v)}`);
233
+ return v ? "T" : "F";
234
+ }
235
+ const and3 = (xs) => (xs.includes("F") ? "F" : xs.includes("U") ? "U" : "T");
236
+ const or3 = (xs) => (xs.includes("T") ? "T" : xs.includes("U") ? "U" : "F");
237
+ const not3 = (x) => (x === "T" ? "F" : x === "F" ? "T" : "U");
238
+ const tv = (b) => (b ? "T" : "F");
239
+ function navigate(s, f, n) {
240
+ const { from, link: linkId, dir } = n.nav;
241
+ const o = objVar(s, f, from.ref);
242
+ if (o === undefined)
243
+ throw new Error(`${from.ref} is not an object binding`);
244
+ const link = s.ont.linkTypes.find((l) => l.id === linkId);
245
+ if (link === undefined)
246
+ throw new Error(`link ${linkId} is not declared`);
247
+ const target = dir === "forward" ? link.to : link.from;
248
+ if (o.row === null)
249
+ return [];
250
+ const fromRow = o.row;
251
+ const targets = s.byId(target);
252
+ const pick = (ids) => ids.flatMap((id) => (typeof id === "string" && targets.has(id) ? [{ type: target, row: targets.get(id) }] : []));
253
+ if (s.isPureLink(link)) {
254
+ const rows = s.linkRows(link);
255
+ const end = (r, k) => field(r, k, `${link.id} link row`);
256
+ return dir === "forward"
257
+ ? pick(rows.filter((r) => end(r, "from") === fromRow.id).map((r) => end(r, "to")))
258
+ : pick(rows.filter((r) => end(r, "to") === fromRow.id).map((r) => end(r, "from")));
259
+ }
260
+ if (dir === "forward") {
261
+ const v = idField(fromRow, link.via, `${link.from} row ${String(fromRow.id)}`, own(s.ont.objectTypes[link.from]?.properties, link.via)?.type);
262
+ return v === null ? [] : pick([v]);
263
+ }
264
+ return [...targets.values()]
265
+ .filter((r) => idField(r, link.via, `${link.from} row ${String(r.id)}`, own(s.ont.objectTypes[link.from]?.properties, link.via)?.type) === fromRow.id)
266
+ .map((row) => ({ type: target, row }));
267
+ }
268
+ const COMPARE = ["eq", "neq", "lt", "lte", "gt", "gte"];
269
+ // Evaluates every thunk, then: the first error that is not UndefinedDerived (an invalid snapshot) is thrown; else the
270
+ // first UndefinedDerived in order is thrown; else the values are returned. So an invalid snapshot is never masked.
271
+ function evalAll(thunks) {
272
+ const out = [];
273
+ const errs = [];
274
+ for (const t of thunks)
275
+ try {
276
+ out.push(t());
277
+ }
278
+ catch (err) {
279
+ errs.push(err);
280
+ }
281
+ const invalid = errs.findIndex((err) => !(err instanceof UndefinedDerived));
282
+ if (invalid !== -1)
283
+ throw errs[invalid];
284
+ if (errs.length > 0)
285
+ throw errs[0];
286
+ return out;
287
+ }
288
+ // No short circuit: all operands of every multi-operand node (comparisons, in, subsetOf, plus, dateIn, and, or) and all
289
+ // elements of a quantifier are evaluated (evalAll); an invalid-snapshot error takes precedence over an undefined derived
290
+ // property; the reported derived property is the first in operand order.
291
+ // An undefined derived property makes the condition undefined only when evaluation reaches it: a derived property of a
292
+ // null optional ref is null without reading its expression, and the `where` of an empty quantifier is never evaluated.
293
+ function predicate(s, f, e) {
294
+ for (const op of COMPARE)
295
+ if (op in e) {
296
+ const [x, y] = e[op];
297
+ const [a, b] = evalAll([() => value(s, f, x), () => value(s, f, y)]);
298
+ if (a === null || b === null)
299
+ return "U";
300
+ switch (op) {
301
+ case "eq": return tv(a === b);
302
+ case "neq": return tv(a !== b);
303
+ case "lt": return tv(a < b);
304
+ case "lte": return tv(a <= b);
305
+ case "gt": return tv(a > b);
306
+ case "gte": return tv(a >= b);
307
+ }
308
+ }
309
+ if ("and" in e)
310
+ return and3(evalAll(e.and.map((x) => () => predicate(s, f, x))));
311
+ if ("or" in e)
312
+ return or3(evalAll(e.or.map((x) => () => predicate(s, f, x))));
313
+ if ("not" in e)
314
+ return not3(predicate(s, f, e.not[0]));
315
+ if ("in" in e) {
316
+ const [a, set] = evalAll([() => value(s, f, e.in[0]), () => value(s, f, e.in[1])]);
317
+ if (a === null || set === null)
318
+ return "U";
319
+ return tv(set.includes(a));
320
+ }
321
+ if ("subsetOf" in e) {
322
+ const [a, b] = evalAll([() => value(s, f, e.subsetOf[0]), () => value(s, f, e.subsetOf[1])]);
323
+ if (a === null || b === null)
324
+ return "U";
325
+ const big = b;
326
+ return tv(a.every((x) => big.includes(x)));
327
+ }
328
+ if ("isNull" in e)
329
+ return tv(value(s, f, e.isNull[0]) === null);
330
+ if ("isNotNull" in e)
331
+ return tv(value(s, f, e.isNotNull[0]) !== null);
332
+ const quant = (q) => evalAll(navigate(s, f, q.in).map((el) => () => (q.where === undefined ? "T" : predicate(s, { ...f, vars: { ...f.vars, [q.as]: el } }, q.where))));
333
+ // Quantifiers are two-valued: an element counts only when its predicate is T.
334
+ if ("exists" in e)
335
+ return tv(quant(e.exists).includes("T"));
336
+ if ("none" in e)
337
+ return tv(!quant(e.none).includes("T"));
338
+ if ("all" in e)
339
+ return tv(quant(e.all).every((x) => x === "T"));
340
+ if ("ref" in e || "derived" in e)
341
+ return toTV(value(s, f, e));
342
+ throw new Error(`not a predicate expression: ${Object.keys(e)[0]}`);
343
+ }
344
+ const topFrame = (env) => ({ action: env.action, vars: {} });
345
+ // Value expressions. `bindings` are taken as having passed decide's shape and binding layers: a ref with
346
+ // `unresolved: domain` whose id does not resolve is passed as null, as decide binds it.
347
+ export function evalValue(ont, expr, env) {
348
+ return value(new Session(ont, env), topFrame(env), expr);
349
+ }
350
+ // Predicate expressions: T, F or U.
351
+ export function evalPredicate(ont, expr, env) {
352
+ return predicate(new Session(ont, env), topFrame(env), expr);
353
+ }
354
+ const kindAdmitted = (pr, actor) => pr === undefined || pr.kinds.includes(actor.kind);
355
+ // Only conditional keys missing: conditional_when says whether they are needed (F: not needed, T: needed, U or absent: undecided).
356
+ function altState(a, actor, when) {
357
+ if (!kindAdmitted(a.principals, actor))
358
+ return "deny";
359
+ if (!a.keys.every((k) => actor.keys.includes(k)))
360
+ return "deny";
361
+ if ((a.conditional_keys ?? []).every((k) => actor.keys.includes(k)))
362
+ return "pass";
363
+ const w = a.conditional_when === undefined ? "U" : when(a.conditional_when);
364
+ return w === "F" ? "pass" : w === "T" ? "deny" : "undecided";
365
+ }
366
+ // Shape (no snapshot reads) and permission. Returns a result to stop with, or undefined to go on.
367
+ function admit(ont, actionId, bindings, actor, when) {
368
+ const action = ont.actionTypes[actionId];
369
+ const params = action.parameters ?? {};
370
+ const unknown = Object.keys(bindings)
371
+ .filter((k) => bindings[k] !== undefined && !Object.hasOwn(params, k))
372
+ .sort()[0];
373
+ if (unknown !== undefined)
374
+ return { layer: "invocation", result: "INVALID_BINDING", param: unknown, reason: "unknown_param" };
375
+ for (const [name, p] of Object.entries(params)) {
376
+ const v = own(bindings, name);
377
+ if (v === null || v === undefined) {
378
+ // A projected ref parameter is always given: null means the projection found nothing (not_found at the binding step).
379
+ const projected = projectedFields(p) !== null;
380
+ if (v === null ? p.optional === true || projected : p.optional === true && !projected)
381
+ continue;
382
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "missing" };
383
+ }
384
+ if (!fitsType(v, p.type))
385
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "type" };
386
+ if (Array.isArray(p.values) && !p.values.includes(v))
387
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "type" };
388
+ }
389
+ const perm = action.permission;
390
+ if (perm === "unknown")
391
+ return { layer: "indeterminate", reason: "permission_unknown" };
392
+ if ("none" in perm)
393
+ return kindAdmitted(perm.principals, actor) ? undefined : { layer: "invocation", result: "PERMISSION_DENIED" };
394
+ const states = ("any_of" in perm ? perm.any_of : [perm]).map((a) => altState(a, actor, when));
395
+ if (states.includes("pass"))
396
+ return undefined;
397
+ if (states.every((x) => x === "deny"))
398
+ return { layer: "invocation", result: "PERMISSION_DENIED" };
399
+ return { layer: "indeterminate", reason: "permission_condition" };
400
+ }
401
+ const condId = (row) => ("passes" in row.when ? row.when.passes : row.when.fails);
402
+ // Binding and scope, then the decision table.
403
+ function resolve(s, actionId) {
404
+ const action = s.ont.actionTypes[actionId];
405
+ // A ref parameter binds one object; each element of a set-of-ref parameter is a referenced object too (array order).
406
+ // A scalar ref with `unresolved: domain` binds to null when its id is not visible.
407
+ const unresolved = []; // unresolved: domain parameters whose id did not resolve
408
+ for (const [name, p] of Object.entries(action.parameters ?? {})) {
409
+ const t = p.type;
410
+ const isSet = typeof t !== "string" && "set" in t;
411
+ const elem = isSet ? t.set : t;
412
+ if (typeof elem === "string" || !("ref" in elem))
413
+ continue;
414
+ const v = own(s.env.bindings, name);
415
+ if (v === null && p.optional !== true && projectedFields(p) !== null)
416
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "not_found" };
417
+ if (v === null || v === undefined)
418
+ continue;
419
+ for (const id of isSet ? v : [v]) {
420
+ if (s.byId(elem.ref).has(id))
421
+ continue;
422
+ // Absent and out of scope give the same null, so existence in another tenant is not revealed.
423
+ if (resolvesInDomain(p)) {
424
+ unresolved.push(name);
425
+ break;
426
+ }
427
+ if (s.allRows(elem.ref).some((r) => r.id === id))
428
+ return { layer: "invocation", result: "SCOPE_DENIED", param: name };
429
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "not_found" };
430
+ }
431
+ }
432
+ const b = s.env.bindings;
433
+ const d = unresolved.length === 0 ? s : new Session(s.ont, { ...s.env, bindings: Object.fromEntries(Object.keys(action.parameters ?? {}).filter((n) => Object.hasOwn(b, n)).map((n) => [n, unresolved.includes(n) ? null : b[n]])) });
434
+ const decision = action.decision;
435
+ if (decision === undefined)
436
+ return { layer: "indeterminate", reason: "no_decision_table" };
437
+ const frame = topFrame(d.env);
438
+ const cache = new Map();
439
+ const disposition = (result) => own(s.ont.dispositions, result).class; // checked by decide before evaluation
440
+ for (const [i, row] of decision.rows.entries()) {
441
+ const id = condId(row);
442
+ const cond = action.conditions.find((c) => c.id === id); // checked by decide before evaluation
443
+ let v = cache.get(id);
444
+ if (v === undefined) {
445
+ if (cond.expr === undefined)
446
+ v = {};
447
+ else
448
+ try {
449
+ v = predicate(d, frame, cond.expr);
450
+ }
451
+ catch (err) {
452
+ if (!(err instanceof UndefinedDerived))
453
+ throw err;
454
+ v = { undefinedDerived: err.derived };
455
+ }
456
+ cache.set(id, v);
457
+ }
458
+ if (typeof v !== "string")
459
+ return v.undefinedDerived === undefined
460
+ ? { layer: "indeterminate", reason: "unspecified", row: i, condition: id }
461
+ : { layer: "indeterminate", reason: "unspecified", row: i, condition: id, derived: v.undefinedDerived };
462
+ if ("passes" in row.when ? v === "T" : v !== "T")
463
+ return { layer: "domain", result: row.result, class: disposition(row.result), row: i, ...(row.next !== undefined ? { next: row.next } : {}) };
464
+ }
465
+ const o = decision.otherwise;
466
+ if (o === undefined)
467
+ return { layer: "indeterminate", reason: "no_row_matched" };
468
+ return { layer: "domain", result: o.result, class: disposition(o.result), row: "otherwise", ...(o.next !== undefined ? { next: o.next } : {}) };
469
+ }
470
+ // decide(action, snapshot, scope, bindings, actor, now) of the format, plus the ontology and the action id.
471
+ // Order: shape -> permission -> (envelope, not here) -> binding and scope -> decision.
472
+ export function decide(ont, actionId, snapshot, scope, bindings, actor, now) {
473
+ const action = own(ont.actionTypes, actionId);
474
+ if (action === undefined)
475
+ throw new Error(`unknown action ${actionId}`);
476
+ if (!Number.isSafeInteger(now))
477
+ throw new Error(`now must be a safe integer, found ${now}`);
478
+ if (!(typeof actor?.id === "string" && Array.isArray(actor.keys) && actor.keys.every((k) => typeof k === "string") && typeof actor.kind === "string"))
479
+ throw new Error("actor must be {id: string, keys: string[], kind: string}");
480
+ inRange(now, "now");
481
+ if (ont.scope !== undefined && scope === null)
482
+ throw new Error("the ontology declares scope, so the scope argument must not be null");
483
+ if (ont.scope === undefined && scope !== null)
484
+ throw new Error("the ontology declares no scope, so the scope argument must be null");
485
+ const root = ont.scope?.root?.type;
486
+ if (root !== undefined && ont.scope?.global?.some((g) => g.type === root))
487
+ throw new Error(`${root} is both global and root`);
488
+ for (const [name, p] of Object.entries(action.parameters ?? {})) {
489
+ if (p.type === undefined)
490
+ throw new Error(`parameter ${name} has no type`);
491
+ if (RESERVED_NAMES.includes(name))
492
+ throw new Error(`parameter ${name} uses a reserved name`);
493
+ }
494
+ const noDisposition = (result) => own(ont.dispositions, result)?.class === undefined;
495
+ for (const [i, row] of (action.decision?.rows ?? []).entries()) {
496
+ const id = condId(row);
497
+ if (!action.conditions.some((c) => c.id === id))
498
+ throw new Error(`decision row ${i} names unknown condition ${id}`);
499
+ if (noDisposition(row.result))
500
+ throw new Error(`result ${row.result} has no disposition`);
501
+ }
502
+ const o = action.decision?.otherwise;
503
+ if (o !== undefined && noDisposition(o.result))
504
+ throw new Error(`result ${o.result} has no disposition`);
505
+ for (const c of action.conditions)
506
+ if (c.expr !== undefined)
507
+ checkPredicate(ont, c.expr, { action: actionId, locals: {} });
508
+ // A conditional_when only with conditional_keys (else the alternative would pass unread), and over request inputs only.
509
+ const perm = action.permission;
510
+ if (perm !== "unknown")
511
+ for (const a of ("any_of" in perm ? perm.any_of : [perm]))
512
+ if (a.conditional_when !== undefined) {
513
+ const ck = a.conditional_keys;
514
+ if (a.none !== undefined || !Array.isArray(ck) || ck.length === 0)
515
+ throw new Error("permission conditional_when needs conditional_keys");
516
+ checkPermissionWhen(ont, a.conditional_when, actionId);
517
+ }
518
+ // One session for both steps: the permission step reads no snapshot (the frame flag input; checkPermissionWhen above).
519
+ const s = new Session(ont, { action: actionId, snapshot, scope, bindings, actor, now });
520
+ const early = admit(ont, actionId, bindings, actor, (e) => predicate(s, { action: actionId, vars: {}, input: true }, e));
521
+ if (early !== undefined)
522
+ return early;
523
+ return resolve(s, actionId);
524
+ }
525
+ // The outward projection. SCOPE_DENIED and not_found map to one value, so a caller cannot learn that an id exists in
526
+ // another tenant. Other reasons (such as type) are reported as they are.
527
+ export function externalResult(r) {
528
+ if (r.layer === "domain")
529
+ return { result: r.result };
530
+ if (r.result === "SCOPE_DENIED")
531
+ return { result: "INVALID_BINDING", param: r.param, reason: "not_found" };
532
+ if (r.result === "INVALID_BINDING")
533
+ return { result: r.result, param: r.param, reason: r.reason };
534
+ return { result: r.result };
535
+ }