@githonllc/avouch 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,135 @@
1
+ export type RuleId = "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "R8" | "R9" | "R10" | "R11" | "R12";
2
+ export declare const RULES: readonly RuleId[];
3
+ /** A name as a whole identifier: not preceded or followed by a letter, a digit or `_`. */
4
+ export declare const IDENT: (s: string) => RegExp;
5
+ export interface Source {
6
+ anchor: string;
7
+ locator?: string;
8
+ }
9
+ export interface Fact<T> {
10
+ value: T;
11
+ source: Source;
12
+ }
13
+ /** Adapter finding (parse error, duplicate row, harness error). Reported and waived exactly like a violation. */
14
+ export interface Diagnostic {
15
+ rule: RuleId;
16
+ kind: string;
17
+ key: string;
18
+ msg: string;
19
+ }
20
+ export interface Violation {
21
+ rule: string;
22
+ kind: string;
23
+ key: string;
24
+ msg: string;
25
+ }
26
+ export interface Report {
27
+ violations: Violation[];
28
+ waived: Violation[];
29
+ info: string[];
30
+ }
31
+ export type SectionLookup = {
32
+ title: string;
33
+ text: string;
34
+ } | "missing" | "ambiguous";
35
+ /** nodes = all states; initial = targets of initial pseudo-edges (exactly one expected, checked by R3); edges = "FROM->TO". */
36
+ export interface StateMachineFact {
37
+ nodes: string[];
38
+ initial: string[];
39
+ edges: string[];
40
+ }
41
+ /** Stable row identity supplied by the adapter (for example a SQLite rowid); never a key column that an update may change. */
42
+ export type RowId = string | number;
43
+ /** Net committed change of one store, row by row: an insert keeps its after image, an update its changed columns, a delete its before image. */
44
+ export interface StoreDelta {
45
+ inserted: {
46
+ row: RowId;
47
+ after: Record<string, unknown>;
48
+ }[];
49
+ updated: {
50
+ row: RowId;
51
+ changes: Record<string, {
52
+ before: unknown;
53
+ after: unknown;
54
+ }>;
55
+ }[];
56
+ deleted: {
57
+ row: RowId;
58
+ before: Record<string, unknown>;
59
+ }[];
60
+ }
61
+ /** Inputs the evaluation of one invocation depends on. Recorded only; no rule reads them yet. */
62
+ export interface EvaluationInputs {
63
+ bindings: unknown;
64
+ actor: unknown;
65
+ scope: unknown;
66
+ now: unknown;
67
+ }
68
+ /** Committed net change of one invocation: changed stores and the event types written. */
69
+ export interface InvocationDelta {
70
+ stores: Record<string, StoreDelta>;
71
+ events: string[];
72
+ }
73
+ /**
74
+ * One invocation of an operation, with an id that the harness generates. Plain records and arrays (no Map or Set), so the
75
+ * evidence serializes as JSON. Aggregates (changed stores, committed count) are derived by the checker, never stored.
76
+ * `delta` is non-null iff the outcome is committed (a rejected record has `delta: null`, not an omitted key); the checker
77
+ * reports any record that breaks this type (SPEC section 4) as an R7 info diagnostic and never throws on it.
78
+ */
79
+ export type Invocation = {
80
+ id: string;
81
+ rolledBackAttempts: number;
82
+ snapshot: string | null;
83
+ inputs: EvaluationInputs;
84
+ disposition?: string;
85
+ } & ({
86
+ outcome: "committed";
87
+ delta: InvocationDelta;
88
+ } | {
89
+ outcome: "rejected";
90
+ delta: null;
91
+ });
92
+ export interface PermissionRow {
93
+ keys: Set<string>;
94
+ text: string;
95
+ }
96
+ export interface Permissions {
97
+ catalog: Fact<Set<string>>;
98
+ rows: Fact<Map<string, PermissionRow>>;
99
+ quoteNamesKey(quote: string, key: string): boolean;
100
+ }
101
+ export type RuleSwitch = true | {
102
+ disabled: string;
103
+ };
104
+ export interface FormatConfig {
105
+ actionIdPattern: RegExp;
106
+ linkFieldPattern: RegExp;
107
+ stateProperty: string;
108
+ crossContextMechanisms: string[];
109
+ requiredSource: Partial<Record<RuleId, string>>;
110
+ scenarioCoverage?: "printed" | "enforced";
111
+ rules: Record<RuleId, RuleSwitch>;
112
+ idempotencyDeclarationRequired?: boolean;
113
+ evidenceRequired?: boolean;
114
+ }
115
+ export interface SourceFacts {
116
+ config: FormatConfig;
117
+ diagnostics: Diagnostic[];
118
+ info: string[];
119
+ section(anchor: string): SectionLookup;
120
+ containsTerm(term: string): boolean;
121
+ fieldList(anchor: string, objectType: string): Fact<Map<string, {
122
+ nullable: boolean;
123
+ }>> | null;
124
+ storeCatalog?: Fact<Set<string>>;
125
+ stateMachines?: Map<string, Fact<StateMachineFact | null>>;
126
+ transitionTriggers?: Map<string, Map<string, Fact<Set<string>>>>;
127
+ contextMembers?: Map<string, Fact<Set<string>>>;
128
+ eventCatalog?: Fact<Set<string>>;
129
+ scenarios?: {
130
+ ids: Set<string>;
131
+ cites: Map<string, Set<string>>;
132
+ };
133
+ permissions?: Permissions;
134
+ evidence?: Map<string, Invocation[]>;
135
+ }
@@ -0,0 +1,3 @@
1
+ export const RULES = ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12"];
2
+ /** A name as a whole identifier: not preceded or followed by a letter, a digit or `_`. */
3
+ export const IDENT = (s) => new RegExp(`(?<![A-Za-z0-9_])${s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_])`);
@@ -0,0 +1,66 @@
1
+ import { type DispositionClass, type Expr, type Stage1Ontology } from "./expr.js";
2
+ export type Value = string | number | boolean | null | readonly (string | number | boolean)[];
3
+ export type Row = Readonly<Record<string, Value>>;
4
+ export interface Snapshot {
5
+ objects: Record<string, readonly Row[]>;
6
+ links?: Record<string, readonly Row[]>;
7
+ }
8
+ export type Actor = {
9
+ id: string;
10
+ keys: readonly string[];
11
+ kind: string;
12
+ };
13
+ export type Bindings = Readonly<Record<string, Value | undefined>>;
14
+ export interface Env {
15
+ action: string;
16
+ snapshot: Snapshot;
17
+ scope: string | null;
18
+ bindings: Bindings;
19
+ actor: Actor;
20
+ now: number;
21
+ }
22
+ export type DecideResult = {
23
+ layer: "invocation";
24
+ result: "INVALID_BINDING";
25
+ param: string;
26
+ reason: "missing" | "unknown_param" | "type" | "not_found";
27
+ } | {
28
+ layer: "invocation";
29
+ result: "SCOPE_DENIED";
30
+ param: string;
31
+ } | {
32
+ layer: "invocation";
33
+ result: "PERMISSION_DENIED";
34
+ } | {
35
+ layer: "domain";
36
+ result: string;
37
+ class: DispositionClass;
38
+ row: number | "otherwise";
39
+ next?: unknown;
40
+ } | {
41
+ layer: "indeterminate";
42
+ reason: "unspecified";
43
+ row: number;
44
+ condition: string;
45
+ derived?: string;
46
+ } | {
47
+ layer: "indeterminate";
48
+ reason: "no_row_matched" | "no_decision_table" | "permission_condition" | "permission_unknown";
49
+ };
50
+ export { INVOCATION_RESULTS } from "./expr.js";
51
+ export declare class UndefinedDerived extends Error {
52
+ readonly derived: string;
53
+ constructor(derived: string);
54
+ }
55
+ type TV = "T" | "F" | "U";
56
+ export declare function evalValue(ont: Stage1Ontology, expr: Expr, env: Env): Value;
57
+ export declare function evalPredicate(ont: Stage1Ontology, expr: Expr, env: Env): TV;
58
+ export declare function decide(ont: Stage1Ontology, actionId: string, snapshot: Snapshot, scope: string | null, bindings: Bindings, actor: Actor, now: number): DecideResult;
59
+ export type ExternalResult = {
60
+ result: string;
61
+ param?: string;
62
+ reason?: "missing" | "unknown_param" | "type" | "not_found";
63
+ };
64
+ export declare function externalResult(r: Exclude<DecideResult, {
65
+ layer: "indeterminate";
66
+ }>): ExternalResult;
@@ -0,0 +1,478 @@
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 { checkPredicate, fitsType, own, RESERVED_NAMES, 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 is an incomplete snapshot and throws; null is a value.
18
+ function field(row, key, what) {
19
+ if (!Object.hasOwn(row, key))
20
+ throw new Error(`snapshot ${what} has no ${key}`);
21
+ return row[key];
22
+ }
23
+ // One evaluation session: caches filtered rows per type and derived values per (id, row id).
24
+ class Session {
25
+ ont;
26
+ env;
27
+ rows = new Map();
28
+ links = new Map();
29
+ derivedCache = new Map();
30
+ objTables;
31
+ constructor(ont, env) {
32
+ this.ont = ont;
33
+ this.env = env;
34
+ this.objTables = new Set(Object.values(ont.objectTypes).map((o) => o.datasource));
35
+ }
36
+ mode(type) {
37
+ const s = this.ont.scope;
38
+ if (s === undefined)
39
+ return "none";
40
+ if (s.root?.type === type)
41
+ return "root";
42
+ if (s.global?.some((g) => g.type === type))
43
+ return "global";
44
+ return "tenant";
45
+ }
46
+ visible(type, row) {
47
+ const m = this.mode(type);
48
+ if (m === "tenant")
49
+ return field(row, this.ont.scope.by, `${type} row ${String(row.id)}`) === this.env.scope;
50
+ if (m === "root")
51
+ return row.id === this.env.scope;
52
+ return true;
53
+ }
54
+ allRows(type) {
55
+ const rows = own(this.env.snapshot.objects, type) ?? [];
56
+ for (const r of rows)
57
+ if (typeof r.id !== "string")
58
+ throw new Error(`a ${type} row has no string id`);
59
+ return rows;
60
+ }
61
+ // Rows of `type` after the scope filter, by id. Lookups by id always run on filtered rows: ids repeat across tenants.
62
+ byId(type) {
63
+ let m = this.rows.get(type);
64
+ if (m === undefined) {
65
+ m = new Map();
66
+ for (const r of this.allRows(type)) {
67
+ if (!this.visible(type, r))
68
+ continue;
69
+ if (m.has(r.id))
70
+ throw new Error(`snapshot has two ${type} rows with id ${String(r.id)} in one scope`);
71
+ m.set(r.id, r);
72
+ }
73
+ this.rows.set(type, m);
74
+ }
75
+ return m;
76
+ }
77
+ linkRows(link) {
78
+ let rows = this.links.get(link.id);
79
+ if (rows === undefined) {
80
+ const all = own(this.env.snapshot.links, link.id) ?? [];
81
+ const by = this.ont.scope?.by;
82
+ const tenantEnd = this.mode(link.from) === "tenant" || this.mode(link.to) === "tenant";
83
+ rows = by !== undefined && tenantEnd ? all.filter((r) => field(r, by, `${link.id} link row`) === this.env.scope) : all;
84
+ this.links.set(link.id, rows);
85
+ }
86
+ return rows;
87
+ }
88
+ isPureLink(link) {
89
+ return link.table !== undefined && !this.objTables.has(link.table);
90
+ }
91
+ cachedDerived(id, row, compute) {
92
+ const k = `${id}\u0000${String(row.id)}`;
93
+ if (!this.derivedCache.has(k))
94
+ this.derivedCache.set(k, compute());
95
+ return this.derivedCache.get(k);
96
+ }
97
+ }
98
+ const paramType = (ont, action, name) => action === undefined ? undefined : own(ont.actionTypes[action]?.parameters, name)?.type;
99
+ function objVar(s, f, name) {
100
+ if (Object.hasOwn(f.vars, name))
101
+ return f.vars[name];
102
+ const t = paramType(s.ont, f.action, name);
103
+ if (t === undefined || typeof t === "string" || !("ref" in t))
104
+ return undefined;
105
+ const v = own(s.env.bindings, name);
106
+ if (v === null || v === undefined)
107
+ return { type: t.ref, row: null };
108
+ const row = s.byId(t.ref).get(v);
109
+ if (row === undefined)
110
+ throw new Error(`binding ${name} = ${String(v)} is not a visible ${t.ref}`);
111
+ return { type: t.ref, row };
112
+ }
113
+ function readRef(s, f, path) {
114
+ const parts = path.split(".");
115
+ if (parts.length > 2 || parts.some((p) => p === ""))
116
+ throw new Error(`ref ${path}: at most one dot`);
117
+ const [head, prop] = parts;
118
+ if (head === "now") {
119
+ if (prop !== undefined)
120
+ throw new Error("now has no properties");
121
+ return s.env.now;
122
+ }
123
+ if (head === "actor") {
124
+ if (prop === "id")
125
+ return s.env.actor.id;
126
+ if (prop === "keys")
127
+ return s.env.actor.keys;
128
+ throw new Error(`actor.${prop ?? ""} is not readable; use actor.id or actor.keys`);
129
+ }
130
+ const o = objVar(s, f, head);
131
+ if (prop === undefined) {
132
+ if (o !== undefined)
133
+ return o.row === null ? null : o.row.id;
134
+ if (f.action === undefined)
135
+ throw new Error(`${head} is not visible in a derived property expression`);
136
+ return own(s.env.bindings, head) ?? null;
137
+ }
138
+ if (o === undefined)
139
+ throw new Error(`${head} is not an object binding`);
140
+ if (o.row === null)
141
+ return null;
142
+ const v = field(o.row, prop, `${o.type} row ${String(o.row.id)}`);
143
+ // A non-null value must fit the declared type; otherwise the snapshot is malformed.
144
+ const t = own(s.ont.objectTypes[o.type]?.properties, prop)?.type;
145
+ if (v !== null && t !== undefined && !fitsType(v, t))
146
+ throw new Error(`snapshot ${o.type} row ${String(o.row.id)}: ${prop} = ${JSON.stringify(v)} does not fit its declared type`);
147
+ return v;
148
+ }
149
+ // Out of range means malformed input (snapshot, duration, now): it throws, it is not U.
150
+ function inRange(t, where) {
151
+ if (!(t >= TIMESTAMP_MIN && t <= TIMESTAMP_MAX))
152
+ throw new Error(`${where}: timestamp out of supported range (${t})`);
153
+ return t;
154
+ }
155
+ function dateIn(t, tz) {
156
+ const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(t));
157
+ const get = (k) => parts.find((x) => x.type === k).value;
158
+ // Timestamps are bounded so the year is 1..9999; pad it to 4 digits so dates compare as strings.
159
+ return `${get("year").padStart(4, "0")}-${get("month")}-${get("day")}`;
160
+ }
161
+ function value(s, f, e) {
162
+ if ("ref" in e)
163
+ return readRef(s, f, e.ref);
164
+ if ("lit" in e)
165
+ return e.lit;
166
+ if ("plus" in e) {
167
+ const [a, b] = evalAll([() => value(s, f, e.plus[0]), () => value(s, f, e.plus[1])]);
168
+ if (a === null || b === null)
169
+ return null;
170
+ const r = a + 1000 * b;
171
+ return inRange(r, "plus");
172
+ }
173
+ if ("dateIn" in e) {
174
+ const [a, b] = evalAll([() => value(s, f, e.dateIn[0]), () => value(s, f, e.dateIn[1])]);
175
+ if (a === null || b === null)
176
+ return null;
177
+ inRange(a, "dateIn");
178
+ return dateIn(a, b);
179
+ }
180
+ if ("derived" in e) {
181
+ const o = objVar(s, f, e.derived.of.ref);
182
+ if (o === undefined)
183
+ throw new Error(`${e.derived.of.ref} is not an object binding`);
184
+ if (o.row === null)
185
+ return null;
186
+ const id = e.derived.id;
187
+ const dp = own(s.ont.derivedProperties, id);
188
+ if (dp === undefined)
189
+ throw new Error(`derived property ${id} is not declared`);
190
+ const expr = dp.expr;
191
+ if (expr === undefined)
192
+ throw new UndefinedDerived(id);
193
+ const row = o.row;
194
+ const frame = { action: undefined, vars: { self: { type: dp.of, row } } };
195
+ // A predicate root (a Boolean derived property): T / F / U give true / false / null.
196
+ return s.cachedDerived(id, row, () => (isValueNode(expr) ? value(s, frame, expr) : fromTV(predicate(s, frame, expr))));
197
+ }
198
+ throw new Error(`not a value expression: ${Object.keys(e)[0]}`);
199
+ }
200
+ const isValueNode = (e) => "ref" in e || "lit" in e || "plus" in e || "dateIn" in e || "derived" in e;
201
+ const fromTV = (x) => (x === "T" ? true : x === "F" ? false : null);
202
+ // A boolean value in a predicate position: true / false / null give T / F / U.
203
+ function toTV(v) {
204
+ if (v === null)
205
+ return "U";
206
+ if (typeof v !== "boolean")
207
+ throw new Error(`a predicate position holds a non-boolean value ${JSON.stringify(v)}`);
208
+ return v ? "T" : "F";
209
+ }
210
+ const and3 = (xs) => (xs.includes("F") ? "F" : xs.includes("U") ? "U" : "T");
211
+ const or3 = (xs) => (xs.includes("T") ? "T" : xs.includes("U") ? "U" : "F");
212
+ const not3 = (x) => (x === "T" ? "F" : x === "F" ? "T" : "U");
213
+ const tv = (b) => (b ? "T" : "F");
214
+ function navigate(s, f, n) {
215
+ const { from, link: linkId, dir } = n.nav;
216
+ const o = objVar(s, f, from.ref);
217
+ if (o === undefined)
218
+ throw new Error(`${from.ref} is not an object binding`);
219
+ const link = s.ont.linkTypes.find((l) => l.id === linkId);
220
+ if (link === undefined)
221
+ throw new Error(`link ${linkId} is not declared`);
222
+ const target = dir === "forward" ? link.to : link.from;
223
+ if (o.row === null)
224
+ return [];
225
+ const fromRow = o.row;
226
+ const targets = s.byId(target);
227
+ const pick = (ids) => ids.flatMap((id) => (typeof id === "string" && targets.has(id) ? [{ type: target, row: targets.get(id) }] : []));
228
+ if (s.isPureLink(link)) {
229
+ const rows = s.linkRows(link);
230
+ const end = (r, k) => field(r, k, `${link.id} link row`);
231
+ return dir === "forward"
232
+ ? pick(rows.filter((r) => end(r, "from") === fromRow.id).map((r) => end(r, "to")))
233
+ : pick(rows.filter((r) => end(r, "to") === fromRow.id).map((r) => end(r, "from")));
234
+ }
235
+ if (dir === "forward") {
236
+ const v = field(fromRow, link.via, `${link.from} row ${String(fromRow.id)}`);
237
+ return v === null ? [] : pick([v]);
238
+ }
239
+ return [...targets.values()]
240
+ .filter((r) => field(r, link.via, `${link.from} row ${String(r.id)}`) === fromRow.id)
241
+ .map((row) => ({ type: target, row }));
242
+ }
243
+ const COMPARE = ["eq", "neq", "lt", "lte", "gt", "gte"];
244
+ // Evaluates every thunk, then: the first error that is not UndefinedDerived (an invalid snapshot) is thrown; else the
245
+ // first UndefinedDerived in order is thrown; else the values are returned. So an invalid snapshot is never masked.
246
+ function evalAll(thunks) {
247
+ const out = [];
248
+ const errs = [];
249
+ for (const t of thunks)
250
+ try {
251
+ out.push(t());
252
+ }
253
+ catch (err) {
254
+ errs.push(err);
255
+ }
256
+ const invalid = errs.findIndex((err) => !(err instanceof UndefinedDerived));
257
+ if (invalid !== -1)
258
+ throw errs[invalid];
259
+ if (errs.length > 0)
260
+ throw errs[0];
261
+ return out;
262
+ }
263
+ // No short circuit: all operands of every multi-operand node (comparisons, in, subsetOf, plus, dateIn, and, or) and all
264
+ // elements of a quantifier are evaluated (evalAll); an invalid-snapshot error takes precedence over an undefined derived
265
+ // property; the reported derived property is the first in operand order.
266
+ // An undefined derived property makes the condition undefined only when evaluation reaches it: a derived property of a
267
+ // null optional ref is null without reading its expression, and the `where` of an empty quantifier is never evaluated.
268
+ function predicate(s, f, e) {
269
+ for (const op of COMPARE)
270
+ if (op in e) {
271
+ const [x, y] = e[op];
272
+ const [a, b] = evalAll([() => value(s, f, x), () => value(s, f, y)]);
273
+ if (a === null || b === null)
274
+ return "U";
275
+ switch (op) {
276
+ case "eq": return tv(a === b);
277
+ case "neq": return tv(a !== b);
278
+ case "lt": return tv(a < b);
279
+ case "lte": return tv(a <= b);
280
+ case "gt": return tv(a > b);
281
+ case "gte": return tv(a >= b);
282
+ }
283
+ }
284
+ if ("and" in e)
285
+ return and3(evalAll(e.and.map((x) => () => predicate(s, f, x))));
286
+ if ("or" in e)
287
+ return or3(evalAll(e.or.map((x) => () => predicate(s, f, x))));
288
+ if ("not" in e)
289
+ return not3(predicate(s, f, e.not[0]));
290
+ if ("in" in e) {
291
+ const [a, set] = evalAll([() => value(s, f, e.in[0]), () => value(s, f, e.in[1])]);
292
+ if (a === null || set === null)
293
+ return "U";
294
+ return tv(set.includes(a));
295
+ }
296
+ if ("subsetOf" in e) {
297
+ const [a, b] = evalAll([() => value(s, f, e.subsetOf[0]), () => value(s, f, e.subsetOf[1])]);
298
+ if (a === null || b === null)
299
+ return "U";
300
+ const big = b;
301
+ return tv(a.every((x) => big.includes(x)));
302
+ }
303
+ if ("isNull" in e)
304
+ return tv(value(s, f, e.isNull[0]) === null);
305
+ if ("isNotNull" in e)
306
+ return tv(value(s, f, e.isNotNull[0]) !== null);
307
+ 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))));
308
+ // Quantifiers are two-valued: an element counts only when its predicate is T.
309
+ if ("exists" in e)
310
+ return tv(quant(e.exists).includes("T"));
311
+ if ("none" in e)
312
+ return tv(!quant(e.none).includes("T"));
313
+ if ("all" in e)
314
+ return tv(quant(e.all).every((x) => x === "T"));
315
+ if ("ref" in e || "derived" in e)
316
+ return toTV(value(s, f, e));
317
+ throw new Error(`not a predicate expression: ${Object.keys(e)[0]}`);
318
+ }
319
+ const topFrame = (env) => ({ action: env.action, vars: {} });
320
+ // Value expressions. `bindings` are taken as having passed decide's shape and binding layers.
321
+ export function evalValue(ont, expr, env) {
322
+ return value(new Session(ont, env), topFrame(env), expr);
323
+ }
324
+ // Predicate expressions: T, F or U.
325
+ export function evalPredicate(ont, expr, env) {
326
+ return predicate(new Session(ont, env), topFrame(env), expr);
327
+ }
328
+ const kindAdmitted = (pr, actor) => pr === undefined || pr.kinds.includes(actor.kind);
329
+ function altState(a, actor) {
330
+ if (!kindAdmitted(a.principals, actor))
331
+ return "deny";
332
+ if (!a.keys.every((k) => actor.keys.includes(k)))
333
+ return "deny";
334
+ return (a.conditional_keys ?? []).every((k) => actor.keys.includes(k)) ? "pass" : "undecided";
335
+ }
336
+ // Shape (no snapshot reads) and permission. Returns a result to stop with, or undefined to go on.
337
+ function admit(ont, actionId, bindings, actor) {
338
+ const action = ont.actionTypes[actionId];
339
+ const params = action.parameters ?? {};
340
+ const unknown = Object.keys(bindings)
341
+ .filter((k) => bindings[k] !== undefined && !Object.hasOwn(params, k))
342
+ .sort()[0];
343
+ if (unknown !== undefined)
344
+ return { layer: "invocation", result: "INVALID_BINDING", param: unknown, reason: "unknown_param" };
345
+ for (const [name, p] of Object.entries(params)) {
346
+ const v = own(bindings, name);
347
+ if (v === null || v === undefined) {
348
+ if (p.optional === true)
349
+ continue;
350
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "missing" };
351
+ }
352
+ if (!fitsType(v, p.type))
353
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "type" };
354
+ if (Array.isArray(p.values) && !p.values.includes(v))
355
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "type" };
356
+ }
357
+ const perm = action.permission;
358
+ if (perm === "unknown")
359
+ return { layer: "indeterminate", reason: "permission_unknown" };
360
+ if ("none" in perm)
361
+ return kindAdmitted(perm.principals, actor) ? undefined : { layer: "invocation", result: "PERMISSION_DENIED" };
362
+ const states = ("any_of" in perm ? perm.any_of : [perm]).map((a) => altState(a, actor));
363
+ if (states.includes("pass"))
364
+ return undefined;
365
+ if (states.every((x) => x === "deny"))
366
+ return { layer: "invocation", result: "PERMISSION_DENIED" };
367
+ return { layer: "indeterminate", reason: "permission_condition" };
368
+ }
369
+ const condId = (row) => ("passes" in row.when ? row.when.passes : row.when.fails);
370
+ // Binding and scope, then the decision table.
371
+ function resolve(s, actionId) {
372
+ const action = s.ont.actionTypes[actionId];
373
+ // A ref parameter binds one object; each element of a set-of-ref parameter is a referenced object too (array order).
374
+ for (const [name, p] of Object.entries(action.parameters ?? {})) {
375
+ const t = p.type;
376
+ const isSet = typeof t !== "string" && "set" in t;
377
+ const elem = isSet ? t.set : t;
378
+ if (typeof elem === "string" || !("ref" in elem))
379
+ continue;
380
+ const v = own(s.env.bindings, name);
381
+ if (v === null || v === undefined)
382
+ continue;
383
+ for (const id of isSet ? v : [v]) {
384
+ if (s.byId(elem.ref).has(id))
385
+ continue;
386
+ if (s.allRows(elem.ref).some((r) => r.id === id))
387
+ return { layer: "invocation", result: "SCOPE_DENIED", param: name };
388
+ return { layer: "invocation", result: "INVALID_BINDING", param: name, reason: "not_found" };
389
+ }
390
+ }
391
+ const decision = action.decision;
392
+ if (decision === undefined)
393
+ return { layer: "indeterminate", reason: "no_decision_table" };
394
+ const frame = topFrame(s.env);
395
+ const cache = new Map();
396
+ const disposition = (result) => own(s.ont.dispositions, result).class; // checked by decide before evaluation
397
+ for (const [i, row] of decision.rows.entries()) {
398
+ const id = condId(row);
399
+ const cond = action.conditions.find((c) => c.id === id); // checked by decide before evaluation
400
+ let v = cache.get(id);
401
+ if (v === undefined) {
402
+ if (cond.expr === undefined)
403
+ v = {};
404
+ else
405
+ try {
406
+ v = predicate(s, frame, cond.expr);
407
+ }
408
+ catch (err) {
409
+ if (!(err instanceof UndefinedDerived))
410
+ throw err;
411
+ v = { undefinedDerived: err.derived };
412
+ }
413
+ cache.set(id, v);
414
+ }
415
+ if (typeof v !== "string")
416
+ return v.undefinedDerived === undefined
417
+ ? { layer: "indeterminate", reason: "unspecified", row: i, condition: id }
418
+ : { layer: "indeterminate", reason: "unspecified", row: i, condition: id, derived: v.undefinedDerived };
419
+ if ("passes" in row.when ? v === "T" : v !== "T")
420
+ return { layer: "domain", result: row.result, class: disposition(row.result), row: i, ...(row.next !== undefined ? { next: row.next } : {}) };
421
+ }
422
+ const o = decision.otherwise;
423
+ if (o === undefined)
424
+ return { layer: "indeterminate", reason: "no_row_matched" };
425
+ return { layer: "domain", result: o.result, class: disposition(o.result), row: "otherwise", ...(o.next !== undefined ? { next: o.next } : {}) };
426
+ }
427
+ // decide(action, snapshot, scope, bindings, actor, now) of the format, plus the ontology and the action id.
428
+ // Order: shape -> permission -> (envelope, not here) -> binding and scope -> decision.
429
+ export function decide(ont, actionId, snapshot, scope, bindings, actor, now) {
430
+ const action = own(ont.actionTypes, actionId);
431
+ if (action === undefined)
432
+ throw new Error(`unknown action ${actionId}`);
433
+ if (!Number.isSafeInteger(now))
434
+ throw new Error(`now must be a safe integer, found ${now}`);
435
+ inRange(now, "now");
436
+ if (ont.scope !== undefined && scope === null)
437
+ throw new Error("the ontology declares scope, so the scope argument must not be null");
438
+ if (ont.scope === undefined && scope !== null)
439
+ throw new Error("the ontology declares no scope, so the scope argument must be null");
440
+ const root = ont.scope?.root?.type;
441
+ if (root !== undefined && ont.scope?.global?.some((g) => g.type === root))
442
+ throw new Error(`${root} is both global and root`);
443
+ for (const [name, p] of Object.entries(action.parameters ?? {})) {
444
+ if (p.type === undefined)
445
+ throw new Error(`parameter ${name} has no type`);
446
+ if (RESERVED_NAMES.includes(name))
447
+ throw new Error(`parameter ${name} uses a reserved name`);
448
+ }
449
+ const noDisposition = (result) => own(ont.dispositions, result)?.class === undefined;
450
+ for (const [i, row] of (action.decision?.rows ?? []).entries()) {
451
+ const id = condId(row);
452
+ if (!action.conditions.some((c) => c.id === id))
453
+ throw new Error(`decision row ${i} names unknown condition ${id}`);
454
+ if (noDisposition(row.result))
455
+ throw new Error(`result ${row.result} has no disposition`);
456
+ }
457
+ const o = action.decision?.otherwise;
458
+ if (o !== undefined && noDisposition(o.result))
459
+ throw new Error(`result ${o.result} has no disposition`);
460
+ for (const c of action.conditions)
461
+ if (c.expr !== undefined)
462
+ checkPredicate(ont, c.expr, { action: actionId, locals: {} });
463
+ const early = admit(ont, actionId, bindings, actor);
464
+ if (early !== undefined)
465
+ return early;
466
+ return resolve(new Session(ont, { action: actionId, snapshot, scope, bindings, actor, now }), actionId);
467
+ }
468
+ // The outward projection. SCOPE_DENIED and not_found map to one value, so a caller cannot learn that an id exists in
469
+ // another tenant. Other reasons (such as type) are reported as they are.
470
+ export function externalResult(r) {
471
+ if (r.layer === "domain")
472
+ return { result: r.result };
473
+ if (r.result === "SCOPE_DENIED")
474
+ return { result: "INVALID_BINDING", param: r.param, reason: "not_found" };
475
+ if (r.result === "INVALID_BINDING")
476
+ return { result: r.result, param: r.param, reason: r.reason };
477
+ return { result: r.result };
478
+ }