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.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +356 -0
- package/SPEC.md +379 -0
- package/dist/cli/avouch.d.ts +2 -0
- package/dist/cli/avouch.js +68 -0
- package/dist/cli/check.d.ts +86 -0
- package/dist/cli/check.js +333 -0
- package/dist/cli/facts.d.ts +3 -0
- package/dist/cli/facts.js +157 -0
- package/dist/cli/gate.d.ts +3 -0
- package/dist/cli/gate.js +149 -0
- package/dist/cli/init.d.ts +21 -0
- package/dist/cli/init.js +282 -0
- package/dist/cli/jsonl.d.ts +5 -0
- package/dist/cli/jsonl.js +20 -0
- package/dist/cli/query.d.ts +2 -0
- package/dist/cli/query.js +138 -0
- package/dist/cli/reconcile.d.ts +9 -0
- package/dist/cli/reconcile.js +131 -0
- package/dist/src/checker.d.ts +3 -0
- package/dist/src/checker.js +1136 -0
- package/dist/src/contract.d.ts +135 -0
- package/dist/src/contract.js +3 -0
- package/dist/src/coverage.d.ts +65 -0
- package/dist/src/coverage.js +237 -0
- package/dist/src/evaluate.d.ts +66 -0
- package/dist/src/evaluate.js +535 -0
- package/dist/src/expr.d.ts +209 -0
- package/dist/src/expr.js +497 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +11 -0
- package/dist/src/ledger.d.ts +96 -0
- package/dist/src/ledger.js +241 -0
- package/dist/src/markdown.d.ts +66 -0
- package/dist/src/markdown.js +701 -0
- package/dist/src/patch.d.ts +6 -0
- package/dist/src/patch.js +37 -0
- package/dist/src/query.d.ts +333 -0
- package/dist/src/query.js +739 -0
- package/dist/src/reconcile.d.ts +61 -0
- package/dist/src/reconcile.js +151 -0
- package/dist/src/stage1-check.d.ts +31 -0
- package/dist/src/stage1-check.js +329 -0
- package/dist/src/touched.d.ts +1 -0
- package/dist/src/touched.js +9 -0
- package/dist/src/unknown.d.ts +10 -0
- package/dist/src/unknown.js +173 -0
- package/docs/adapter-guide.md +187 -0
- package/docs/markdown-convention.md +635 -0
- package/ontology.schema.json +1757 -0
- package/package.json +57 -0
- package/templates/agents-block.md +12 -0
- package/vocab.ttl +524 -0
package/dist/src/expr.js
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// Decision expressions: the AST, the semantic types, the part of an ontology the evaluator reads, and the static type check.
|
|
2
|
+
// No imports. The evaluator (./evaluate) assumes every expression it runs has passed checkPredicate / valueType.
|
|
3
|
+
export const SCALAR_TYPE_NAMES = ["enum", "timestamp", "duration", "date", "timezone", "id", "integer", "number", "boolean", "string"];
|
|
4
|
+
export const EXPR_NODES = [
|
|
5
|
+
"ref", "lit", "plus", "dateIn", "derived",
|
|
6
|
+
"eq", "neq", "lt", "lte", "gt", "gte", "and", "or", "not", "in", "subsetOf", "isNull", "isNotNull",
|
|
7
|
+
"exists", "none", "all", "nav",
|
|
8
|
+
];
|
|
9
|
+
export const DISPOSITION_CLASSES = ["rejected", "recorded", "pending", "applied"];
|
|
10
|
+
// The fixed invocation result set of the format. The first two belong to the idempotency envelope. A disposition id may
|
|
11
|
+
// not be one of these names.
|
|
12
|
+
// Every invocation-layer result, SCOPE_DENIED included. SCOPE_DENIED is internal and never reported (SPEC section 9).
|
|
13
|
+
export const INVOCATION_RESULTS = ["REPLAYED", "IDEMPOTENCY_CONFLICT", "INVALID_BINDING", "SCOPE_DENIED", "PERMISSION_DENIED"];
|
|
14
|
+
// The distinct request fields of a projected parameter, or null: only a scalar {ref: T} parameter with at least one field is
|
|
15
|
+
// projected; on any other parameter projectedFrom has no effect.
|
|
16
|
+
export function projectedFields(p) {
|
|
17
|
+
const fs = Array.isArray(p.projectedFrom) ? [...new Set(p.projectedFrom.filter((f) => typeof f === "string"))] : [];
|
|
18
|
+
return isObj(p.type) && typeof p.type.ref === "string" && fs.length > 0 ? fs : null;
|
|
19
|
+
}
|
|
20
|
+
// Whether an id of p that does not resolve binds to null (`unresolved: domain`): only on a required scalar {ref: T}
|
|
21
|
+
// parameter (no optional key) without projectedFrom. On any other parameter the key has no effect, and R1 reports it.
|
|
22
|
+
export function resolvesInDomain(p) {
|
|
23
|
+
return p.unresolved === "domain" && isObj(p.type) && typeof p.type.ref === "string" && !("set" in p.type) && p.optional === undefined && p.projectedFrom === undefined;
|
|
24
|
+
}
|
|
25
|
+
export class ExprError extends Error {
|
|
26
|
+
path;
|
|
27
|
+
constructor(path, msg) {
|
|
28
|
+
super(`${path}: ${msg}`);
|
|
29
|
+
this.path = path;
|
|
30
|
+
this.name = "ExprError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const newReads = () => ({ reads: [], literals: [], undefinedDerived: new Set() });
|
|
34
|
+
const VALUE_NODES = new Set(["ref", "lit", "plus", "dateIn", "derived"]);
|
|
35
|
+
const COMPARE = new Set(["eq", "neq", "lt", "lte", "gt", "gte"]);
|
|
36
|
+
const ORDERED = new Set(["timestamp", "date", "duration", "integer", "number"]);
|
|
37
|
+
// Names an expression reads without a declaration; no parameter or quantifier variable may take them.
|
|
38
|
+
export const RESERVED_NAMES = ["now", "actor", "self"];
|
|
39
|
+
const RESERVED = new Set(RESERVED_NAMES);
|
|
40
|
+
export const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
41
|
+
// Timestamp bounds, inclusive: 0001-01-02T00:00:00.000Z .. 9999-12-30T23:59:59.999Z. The one-day margin covers every UTC
|
|
42
|
+
// offset, so dateIn always gives a year from 1 to 9999.
|
|
43
|
+
export const TIMESTAMP_MIN = -62135510400000;
|
|
44
|
+
export const TIMESTAMP_MAX = 253402214399999;
|
|
45
|
+
export const isObj = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
46
|
+
// Own keys only: a name such as `toString` must not resolve to an Object.prototype member.
|
|
47
|
+
export const own = (r, k) => (r !== undefined && Object.hasOwn(r, k) ? r[k] : undefined);
|
|
48
|
+
function nodeKey(e, path) {
|
|
49
|
+
if (!isObj(e))
|
|
50
|
+
throw new ExprError(path, "an expression must be a single-key map");
|
|
51
|
+
const ks = Object.keys(e);
|
|
52
|
+
if (ks.length !== 1)
|
|
53
|
+
throw new ExprError(path, `an expression must have exactly one key, found ${ks.length}`);
|
|
54
|
+
return ks[0];
|
|
55
|
+
}
|
|
56
|
+
function args(e, k, n, path) {
|
|
57
|
+
const a = e[k];
|
|
58
|
+
if (!Array.isArray(a))
|
|
59
|
+
throw new ExprError(path, `${k} takes an array of operands`);
|
|
60
|
+
if (n === "2+" ? a.length < 2 : a.length !== n)
|
|
61
|
+
throw new ExprError(path, `${k} takes ${n === "2+" ? "at least 2" : n} operands, found ${a.length}`);
|
|
62
|
+
return a;
|
|
63
|
+
}
|
|
64
|
+
const isLit = (e) => isObj(e) && Object.keys(e).length === 1 && "lit" in e;
|
|
65
|
+
export function typeEquals(a, b) {
|
|
66
|
+
if (typeof a === "string" || typeof b === "string")
|
|
67
|
+
return a === b;
|
|
68
|
+
if ("ref" in a)
|
|
69
|
+
return "ref" in b && a.ref === b.ref;
|
|
70
|
+
if ("set" in a)
|
|
71
|
+
return "set" in b && typeEquals(a.set, b.set);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const showType = (t) => typeof t === "string" ? t : "ref" in t ? `{ref: ${t.ref}}` : `{set: ${showType(t.set)}}`;
|
|
75
|
+
const isSet = (t) => typeof t !== "string" && "set" in t;
|
|
76
|
+
export function validTimezone(v) {
|
|
77
|
+
try {
|
|
78
|
+
new Intl.DateTimeFormat("en-US", { timeZone: v });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err instanceof RangeError)
|
|
83
|
+
return false;
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// The one format table, for literals and for parameter values alike.
|
|
88
|
+
export function fitsType(v, t) {
|
|
89
|
+
if (typeof t !== "string") {
|
|
90
|
+
if ("set" in t) {
|
|
91
|
+
const el = t.set;
|
|
92
|
+
return Array.isArray(v) && v.every((x) => fitsType(x, el));
|
|
93
|
+
}
|
|
94
|
+
return typeof v === "string" && v !== "";
|
|
95
|
+
}
|
|
96
|
+
switch (t) {
|
|
97
|
+
case "id":
|
|
98
|
+
return typeof v === "string" && v !== "";
|
|
99
|
+
case "enum":
|
|
100
|
+
case "string":
|
|
101
|
+
return typeof v === "string";
|
|
102
|
+
case "timezone":
|
|
103
|
+
return typeof v === "string" && validTimezone(v);
|
|
104
|
+
case "date":
|
|
105
|
+
return typeof v === "string" && DATE_RE.test(v);
|
|
106
|
+
case "timestamp":
|
|
107
|
+
return Number.isSafeInteger(v) && v >= TIMESTAMP_MIN && v <= TIMESTAMP_MAX;
|
|
108
|
+
case "duration":
|
|
109
|
+
case "integer":
|
|
110
|
+
return Number.isSafeInteger(v);
|
|
111
|
+
case "number":
|
|
112
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
113
|
+
case "boolean":
|
|
114
|
+
return typeof v === "boolean";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// A literal is a string, a number, or an array of them; there are no Boolean literals.
|
|
118
|
+
const isLitValue = (v) => typeof v === "string" || typeof v === "number" || (Array.isArray(v) && v.every((x) => typeof x === "string" || typeof x === "number"));
|
|
119
|
+
const litFits = (v, t) => isLitValue(v) && fitsType(v, t);
|
|
120
|
+
// A literal in a position of type t; a set literal is non-empty with elements of one JSON type. Records the literal.
|
|
121
|
+
function literal(v, t, ctx, at) {
|
|
122
|
+
if (Array.isArray(v) && v.length === 0)
|
|
123
|
+
throw new ExprError(at, "a set literal must not be empty");
|
|
124
|
+
if (Array.isArray(v) && new Set(v.map((x) => typeof x)).size > 1)
|
|
125
|
+
throw new ExprError(at, `set literal ${JSON.stringify(v)} mixes element types`);
|
|
126
|
+
if (!litFits(v, t))
|
|
127
|
+
throw new ExprError(at, `literal ${JSON.stringify(v)} does not fit ${showType(t)}`);
|
|
128
|
+
if (ctx.collect && !ctx.nested)
|
|
129
|
+
ctx.collect.literals.push(...(Array.isArray(v) ? v : [v]));
|
|
130
|
+
}
|
|
131
|
+
// An object binding: a ref-typed parameter of the action, a quantifier variable, or `self`. Returns its object type.
|
|
132
|
+
export function objectBinding(ont, name, ctx) {
|
|
133
|
+
if (name === "self")
|
|
134
|
+
return ctx.self;
|
|
135
|
+
if (Object.hasOwn(ctx.locals, name))
|
|
136
|
+
return ctx.locals[name];
|
|
137
|
+
if (ctx.action === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
const t = own(ont.actionTypes[ctx.action]?.parameters, name)?.type;
|
|
140
|
+
return t !== undefined && typeof t !== "string" && "ref" in t ? t.ref : undefined;
|
|
141
|
+
}
|
|
142
|
+
function refType(ont, path, ctx, at) {
|
|
143
|
+
const parts = path.split(".");
|
|
144
|
+
if (parts.length > 2 || parts.some((p) => p === ""))
|
|
145
|
+
throw new ExprError(at, `ref ${path}: at most one dot`);
|
|
146
|
+
const [head, prop] = parts;
|
|
147
|
+
if (head === "now") {
|
|
148
|
+
if (prop !== undefined)
|
|
149
|
+
throw new ExprError(at, "now has no properties");
|
|
150
|
+
return "timestamp";
|
|
151
|
+
}
|
|
152
|
+
if (head === "actor") {
|
|
153
|
+
if (prop === "id")
|
|
154
|
+
return "id";
|
|
155
|
+
if (prop === "keys")
|
|
156
|
+
return { set: "string" };
|
|
157
|
+
if (prop === "kind")
|
|
158
|
+
return "enum";
|
|
159
|
+
throw new ExprError(at, `actor.${prop ?? ""} is not readable; use actor.id, actor.keys or actor.kind`);
|
|
160
|
+
}
|
|
161
|
+
if (head === "self" && ctx.self === undefined)
|
|
162
|
+
throw new ExprError(at, "self is valid only in a derived property expression");
|
|
163
|
+
const obj = objectBinding(ont, head, ctx);
|
|
164
|
+
if (obj !== undefined && own(ont.objectTypes, obj) === undefined)
|
|
165
|
+
throw new ExprError(at, `object type ${obj} is not declared`);
|
|
166
|
+
if (prop === undefined) {
|
|
167
|
+
if (obj !== undefined)
|
|
168
|
+
return { ref: obj };
|
|
169
|
+
if (ctx.action === undefined)
|
|
170
|
+
throw new ExprError(at, `${head} is not visible here`);
|
|
171
|
+
const p = own(ont.actionTypes[ctx.action]?.parameters, head);
|
|
172
|
+
if (p === undefined)
|
|
173
|
+
throw new ExprError(at, `${head} is not a parameter, a binding or a reserved name`);
|
|
174
|
+
if (p.type === undefined)
|
|
175
|
+
throw new ExprError(at, `parameter ${head} has no type`);
|
|
176
|
+
return p.type;
|
|
177
|
+
}
|
|
178
|
+
if (obj === undefined)
|
|
179
|
+
throw new ExprError(at, `${head} is not an object binding`);
|
|
180
|
+
if (own(ont.derivedProperties, prop)?.of === obj)
|
|
181
|
+
throw new ExprError(at, `${prop} is a derived property; read it with {derived: {id, of}}`);
|
|
182
|
+
const t = own(ont.objectTypes[obj].properties, prop)?.type;
|
|
183
|
+
if (t === undefined)
|
|
184
|
+
throw new ExprError(at, `${obj}.${prop} is not a declared property with a type`);
|
|
185
|
+
ctx.collect?.reads.push({ prop: `${obj}.${prop}` });
|
|
186
|
+
return t;
|
|
187
|
+
}
|
|
188
|
+
function bindingName(ont, from, ctx, at) {
|
|
189
|
+
if (!isObj(from) || typeof from.ref !== "string" || Object.keys(from).length !== 1)
|
|
190
|
+
throw new ExprError(at, "expected {ref: <binding name>}");
|
|
191
|
+
if (from.ref.includes("."))
|
|
192
|
+
throw new ExprError(at, `${from.ref}: a binding name has no dot`);
|
|
193
|
+
const t = objectBinding(ont, from.ref, ctx);
|
|
194
|
+
if (t === undefined)
|
|
195
|
+
throw new ExprError(at, `${from.ref} is not an object binding`);
|
|
196
|
+
return t;
|
|
197
|
+
}
|
|
198
|
+
// top: the derived property is the checked expression itself (its literals are recorded), not one reached from another.
|
|
199
|
+
function derivedType(ont, id, at, stack, collect, top = false) {
|
|
200
|
+
const dp = own(ont.derivedProperties, id);
|
|
201
|
+
if (dp === undefined)
|
|
202
|
+
throw new ExprError(at, `derived property ${id} is not declared`);
|
|
203
|
+
if (dp.type === undefined)
|
|
204
|
+
throw new ExprError(at, `derived property ${id} has no type`);
|
|
205
|
+
if (dp.expr === undefined)
|
|
206
|
+
collect?.undefinedDerived.add(id);
|
|
207
|
+
else {
|
|
208
|
+
const ctx = { self: dp.of, locals: {}, collect, nested: !top };
|
|
209
|
+
if (stack.includes(id))
|
|
210
|
+
throw new ExprError(at, `derived properties form a cycle: ${[...stack, id].join(" -> ")}`);
|
|
211
|
+
const where = `${at}<${id}>`;
|
|
212
|
+
const k = nodeKey(dp.expr, where);
|
|
213
|
+
if (!VALUE_NODES.has(k)) {
|
|
214
|
+
// A predicate root is a Boolean expression: the declared type must be boolean.
|
|
215
|
+
if (dp.type !== "boolean")
|
|
216
|
+
throw new ExprError(at, `derived property ${id}: a predicate expression needs the declared type boolean, not ${showType(dp.type)}`);
|
|
217
|
+
checkPredicateIn(ont, dp.expr, ctx, where, [...stack, id]);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// A boolean derived property has a predicate root (its definition is a Boolean expression).
|
|
221
|
+
if (dp.type === "boolean")
|
|
222
|
+
throw new ExprError(at, `derived property ${id}: declared boolean, so its expression root must be a predicate, not ${k}`);
|
|
223
|
+
const t = valueTypeIn(ont, dp.expr, ctx, dp.type, where, [...stack, id]);
|
|
224
|
+
if (!typeEquals(t, dp.type))
|
|
225
|
+
throw new ExprError(at, `derived property ${id}: expression type ${showType(t)} is not the declared ${showType(dp.type)}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return dp.type;
|
|
229
|
+
}
|
|
230
|
+
function operand(ont, e, ctx, want, at, stack) {
|
|
231
|
+
const t = valueTypeIn(ont, e, ctx, want, at, stack);
|
|
232
|
+
if (!typeEquals(t, want))
|
|
233
|
+
throw new ExprError(at, `expected ${showType(want)}, found ${showType(t)}`);
|
|
234
|
+
}
|
|
235
|
+
function valueTypeIn(ont, e, ctx, expected, at, stack) {
|
|
236
|
+
const k = nodeKey(e, at);
|
|
237
|
+
const node = e;
|
|
238
|
+
const p = `${at}.${k}`;
|
|
239
|
+
switch (k) {
|
|
240
|
+
case "ref":
|
|
241
|
+
if (typeof node.ref !== "string")
|
|
242
|
+
throw new ExprError(p, "ref takes a path string");
|
|
243
|
+
return refType(ont, node.ref, ctx, p);
|
|
244
|
+
case "lit":
|
|
245
|
+
if (expected === undefined)
|
|
246
|
+
throw new ExprError(p, "a literal takes its type from its position, and this position gives none");
|
|
247
|
+
if (expected === "boolean")
|
|
248
|
+
throw new ExprError(p, "there are no boolean literals; write x or not(x)");
|
|
249
|
+
if (expected === "string")
|
|
250
|
+
throw new ExprError(p, "there are no string literals; a string is read only by isNull and isNotNull");
|
|
251
|
+
literal(node.lit, expected, ctx, p);
|
|
252
|
+
return expected;
|
|
253
|
+
case "plus":
|
|
254
|
+
case "dateIn": {
|
|
255
|
+
const [a, b] = args(node, k, 2, p);
|
|
256
|
+
if (isLit(a) && isLit(b))
|
|
257
|
+
throw new ExprError(p, `${k} with two literal operands`);
|
|
258
|
+
operand(ont, a, ctx, "timestamp", `${p}[0]`, stack);
|
|
259
|
+
operand(ont, b, ctx, k === "plus" ? "duration" : "timezone", `${p}[1]`, stack);
|
|
260
|
+
return k === "plus" ? "timestamp" : "date";
|
|
261
|
+
}
|
|
262
|
+
case "derived": {
|
|
263
|
+
const d = node.derived;
|
|
264
|
+
if (!isObj(d) || typeof d.id !== "string")
|
|
265
|
+
throw new ExprError(p, "derived takes {id, of: {ref}}");
|
|
266
|
+
const of = bindingName(ont, d.of, ctx, `${p}.of`);
|
|
267
|
+
const dp = own(ont.derivedProperties, d.id);
|
|
268
|
+
if (dp !== undefined && dp.of !== of)
|
|
269
|
+
throw new ExprError(p, `derived property ${d.id} is of ${dp.of}, not ${of}`);
|
|
270
|
+
return derivedType(ont, d.id, p, stack, ctx.collect);
|
|
271
|
+
}
|
|
272
|
+
case "nav":
|
|
273
|
+
throw new ExprError(p, "nav is valid only as the `in` of a quantifier");
|
|
274
|
+
default:
|
|
275
|
+
if (EXPR_NODES.includes(k))
|
|
276
|
+
throw new ExprError(p, `predicate ${k} in a value position`);
|
|
277
|
+
throw new ExprError(p, `unknown expression node ${k}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// A literal compared with `{ref: P}`, where P (no dot) is a parameter of the action that declares `values`, must be one of
|
|
281
|
+
// them. Quantifier variables cannot reuse parameter names, so such a ref is always the parameter.
|
|
282
|
+
function checkValues(ont, e, xs, ctx, p) {
|
|
283
|
+
if (ctx.action === undefined || !isObj(e) || typeof e.ref !== "string" || e.ref.includes("."))
|
|
284
|
+
return;
|
|
285
|
+
const param = own(ont.actionTypes[ctx.action]?.parameters, e.ref);
|
|
286
|
+
const vs = param?.values;
|
|
287
|
+
if (param?.type !== "enum" || !Array.isArray(vs))
|
|
288
|
+
return;
|
|
289
|
+
for (const x of xs)
|
|
290
|
+
if (!vs.includes(x))
|
|
291
|
+
throw new ExprError(p, `literal ${JSON.stringify(x)} is not one of the values of parameter ${e.ref}`);
|
|
292
|
+
}
|
|
293
|
+
// Types the two operands of a binary operation; a literal takes the type of the other side.
|
|
294
|
+
function pair(ont, a, b, ctx, p, stack) {
|
|
295
|
+
if (isLit(a) && isLit(b))
|
|
296
|
+
throw new ExprError(p, "both operands are literals");
|
|
297
|
+
if (isLit(a)) {
|
|
298
|
+
const tb = valueTypeIn(ont, b, ctx, undefined, `${p}[1]`, stack);
|
|
299
|
+
return [valueTypeIn(ont, a, ctx, tb, `${p}[0]`, stack), tb];
|
|
300
|
+
}
|
|
301
|
+
const ta = valueTypeIn(ont, a, ctx, undefined, `${p}[0]`, stack);
|
|
302
|
+
return [ta, valueTypeIn(ont, b, ctx, ta, `${p}[1]`, stack)];
|
|
303
|
+
}
|
|
304
|
+
function checkPredicateIn(ont, e, ctx, at, stack) {
|
|
305
|
+
const k = nodeKey(e, at);
|
|
306
|
+
const node = e;
|
|
307
|
+
const p = `${at}.${k}`;
|
|
308
|
+
if (COMPARE.has(k)) {
|
|
309
|
+
const [a, b] = args(node, k, 2, p);
|
|
310
|
+
const n0 = ctx.collect?.reads.length ?? 0;
|
|
311
|
+
const [ta, tb] = pair(ont, a, b, ctx, p, stack);
|
|
312
|
+
if (isSet(ta) || isSet(tb))
|
|
313
|
+
throw new ExprError(p, `${k} on a set; sets have only in and subsetOf`);
|
|
314
|
+
if (ta === "string" || tb === "string")
|
|
315
|
+
throw new ExprError(p, `${k} on a string; a string is read only by isNull and isNotNull`);
|
|
316
|
+
// a property read compared by eq / neq with a string literal: record the literal with the read
|
|
317
|
+
const [l, r] = isLit(a) ? [a, b] : [b, a];
|
|
318
|
+
if ((k === "eq" || k === "neq") && ctx.collect?.reads.length === n0 + 1 && isLit(l) && typeof l.lit === "string" && isObj(r) && "ref" in r)
|
|
319
|
+
ctx.collect.reads[n0].lit = l.lit;
|
|
320
|
+
if (k === "eq" || k === "neq") {
|
|
321
|
+
const idVsRef = (x, y) => x === "id" && typeof y !== "string" && "ref" in y;
|
|
322
|
+
if (!typeEquals(ta, tb) && !idVsRef(ta, tb) && !idVsRef(tb, ta))
|
|
323
|
+
throw new ExprError(p, `${k} compares ${showType(ta)} with ${showType(tb)}`);
|
|
324
|
+
if (isLit(l))
|
|
325
|
+
checkValues(ont, r, [l.lit], ctx, p);
|
|
326
|
+
}
|
|
327
|
+
else if (typeof ta !== "string" || !ORDERED.has(ta) || !typeEquals(ta, tb))
|
|
328
|
+
throw new ExprError(p, `${k} needs two operands of one type among timestamp, date, duration, integer, number; found ${showType(ta)} and ${showType(tb)}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
switch (k) {
|
|
332
|
+
case "and":
|
|
333
|
+
case "or":
|
|
334
|
+
args(node, k, "2+", p).forEach((x, i) => checkPredicateIn(ont, x, ctx, `${p}[${i}]`, stack));
|
|
335
|
+
return;
|
|
336
|
+
case "not":
|
|
337
|
+
checkPredicateIn(ont, args(node, k, 1, p)[0], ctx, `${p}[0]`, stack);
|
|
338
|
+
return;
|
|
339
|
+
case "in": {
|
|
340
|
+
const [a, s] = args(node, k, 2, p);
|
|
341
|
+
if (isLit(a) && isLit(s))
|
|
342
|
+
throw new ExprError(p, "both operands are literals");
|
|
343
|
+
if (isLit(a)) {
|
|
344
|
+
const ts = valueTypeIn(ont, s, ctx, undefined, `${p}[1]`, stack);
|
|
345
|
+
if (!isSet(ts))
|
|
346
|
+
throw new ExprError(`${p}[1]`, `in needs a set, found ${showType(ts)}`);
|
|
347
|
+
literal(a.lit, ts.set, ctx, `${p}[0]`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const ta = valueTypeIn(ont, a, ctx, undefined, `${p}[0]`, stack);
|
|
351
|
+
if (isSet(ta))
|
|
352
|
+
throw new ExprError(`${p}[0]`, "in takes an element, found a set");
|
|
353
|
+
if (ta === "string")
|
|
354
|
+
throw new ExprError(`${p}[0]`, "in on a string; a string is read only by isNull and isNotNull");
|
|
355
|
+
const ts = valueTypeIn(ont, s, ctx, { set: ta }, `${p}[1]`, stack);
|
|
356
|
+
if (!isSet(ts) || !typeEquals(ts.set, ta))
|
|
357
|
+
throw new ExprError(p, `in needs {set: ${showType(ta)}}, found ${showType(ts)}`);
|
|
358
|
+
if (isLit(s) && Array.isArray(s.lit))
|
|
359
|
+
checkValues(ont, a, s.lit, ctx, p);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
case "subsetOf": {
|
|
363
|
+
const [a, b] = args(node, k, 2, p);
|
|
364
|
+
const [ta, tb] = pair(ont, a, b, ctx, p, stack);
|
|
365
|
+
if (!isSet(ta) || !typeEquals(ta, tb))
|
|
366
|
+
throw new ExprError(p, `subsetOf needs two sets of one element type, found ${showType(ta)} and ${showType(tb)}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
case "isNull":
|
|
370
|
+
case "isNotNull": {
|
|
371
|
+
const [a] = args(node, k, 1, p);
|
|
372
|
+
if (isLit(a))
|
|
373
|
+
throw new ExprError(`${p}[0]`, `${k} of a literal`);
|
|
374
|
+
valueTypeIn(ont, a, ctx, undefined, `${p}[0]`, stack);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
case "exists":
|
|
378
|
+
case "none":
|
|
379
|
+
case "all": {
|
|
380
|
+
const q = node[k];
|
|
381
|
+
if (!isObj(q) || typeof q.as !== "string")
|
|
382
|
+
throw new ExprError(p, `${k} takes {as, in, where?}`);
|
|
383
|
+
const as = q.as;
|
|
384
|
+
if (RESERVED.has(as))
|
|
385
|
+
throw new ExprError(`${p}.as`, `${as} is a reserved name`);
|
|
386
|
+
if (Object.hasOwn(ctx.locals, as))
|
|
387
|
+
throw new ExprError(`${p}.as`, `${as} is already bound by an outer quantifier`);
|
|
388
|
+
if (ctx.action !== undefined && own(ont.actionTypes[ctx.action]?.parameters, as) !== undefined)
|
|
389
|
+
throw new ExprError(`${p}.as`, `${as} is a parameter name`);
|
|
390
|
+
const elem = navType(ont, q.in, ctx, `${p}.in`);
|
|
391
|
+
if (q.where !== undefined)
|
|
392
|
+
checkPredicateIn(ont, q.where, { ...ctx, locals: { ...ctx.locals, [as]: elem } }, `${p}.where`, stack);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
default: {
|
|
396
|
+
if (!VALUE_NODES.has(k))
|
|
397
|
+
throw new ExprError(p, `unknown predicate node ${k}`);
|
|
398
|
+
// A value node of type boolean is a predicate: true / false / null give T / F / U.
|
|
399
|
+
if (k === "lit")
|
|
400
|
+
throw new ExprError(p, "a literal in a predicate position; there are no boolean literals");
|
|
401
|
+
const t = valueTypeIn(ont, e, ctx, undefined, at, stack);
|
|
402
|
+
if (t !== "boolean")
|
|
403
|
+
throw new ExprError(p, `value ${k} of type ${showType(t)} in a predicate position; only a boolean value is a predicate`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// A pure link: its table is no object's datasource (the evaluator reads snapshot.links[link.id]).
|
|
409
|
+
export function isPureLink(ont, link) {
|
|
410
|
+
return link.table !== undefined && !(isObj(ont.objectTypes) && Object.values(ont.objectTypes).some((o) => isObj(o) && o.datasource === link.table));
|
|
411
|
+
}
|
|
412
|
+
// A navigation over a foreign-key link reads link.via on rows of link.from (forward: the source row; reverse: each target
|
|
413
|
+
// row). Recorded only when via is a declared property of link.from and neither name has a dot (a read is `<Object>.<prop>`).
|
|
414
|
+
function collectVia(ont, link, ctx) {
|
|
415
|
+
if (ctx.collect === undefined || isPureLink(ont, link) || typeof link.via !== "string" || link.via.includes(".") || String(link.from).includes(".") || !isObj(ont.objectTypes))
|
|
416
|
+
return;
|
|
417
|
+
const props = own(ont.objectTypes, link.from)?.properties;
|
|
418
|
+
if (isObj(props) && own(props, link.via) !== undefined)
|
|
419
|
+
ctx.collect.reads.push({ prop: `${link.from}.${link.via}` });
|
|
420
|
+
}
|
|
421
|
+
// Returns the element object type of a navigation.
|
|
422
|
+
export function navType(ont, n, ctx, at) {
|
|
423
|
+
if (nodeKey(n, at) !== "nav")
|
|
424
|
+
throw new ExprError(at, "a quantifier ranges over a nav");
|
|
425
|
+
const v = n.nav;
|
|
426
|
+
const p = `${at}.nav`;
|
|
427
|
+
if (!isObj(v))
|
|
428
|
+
throw new ExprError(p, "nav takes {from, link, dir}");
|
|
429
|
+
const from = bindingName(ont, v.from, ctx, `${p}.from`);
|
|
430
|
+
const link = ont.linkTypes.find((l) => l.id === v.link);
|
|
431
|
+
if (link === undefined)
|
|
432
|
+
throw new ExprError(`${p}.link`, `link ${String(v.link)} is not declared`);
|
|
433
|
+
if (v.dir === "forward") {
|
|
434
|
+
if (from !== link.from)
|
|
435
|
+
throw new ExprError(p, `forward ${link.id} starts at ${link.from}, not ${from}`);
|
|
436
|
+
collectVia(ont, link, ctx);
|
|
437
|
+
return link.to;
|
|
438
|
+
}
|
|
439
|
+
if (v.dir === "reverse") {
|
|
440
|
+
if (from !== link.to)
|
|
441
|
+
throw new ExprError(p, `reverse ${link.id} starts at ${link.to}, not ${from}`);
|
|
442
|
+
collectVia(ont, link, ctx);
|
|
443
|
+
return link.from;
|
|
444
|
+
}
|
|
445
|
+
throw new ExprError(`${p}.dir`, "dir is forward or reverse");
|
|
446
|
+
}
|
|
447
|
+
// Throws ExprError when expr is not a well-typed predicate in ctx.
|
|
448
|
+
export function checkPredicate(ont, expr, ctx) {
|
|
449
|
+
if (ctx.action !== undefined)
|
|
450
|
+
for (const name of Object.keys(ont.actionTypes[ctx.action]?.parameters ?? {}))
|
|
451
|
+
if (RESERVED.has(name))
|
|
452
|
+
throw new ExprError("$", `parameter ${name} uses a reserved name`);
|
|
453
|
+
checkPredicateIn(ont, expr, ctx, "$", []);
|
|
454
|
+
}
|
|
455
|
+
// A permission conditional_when: a predicate over request inputs only. The permission step runs after shape and before
|
|
456
|
+
// binding and reads no snapshot, so only these nodes are allowed, a ref is actor.<x> or a parameter name without a dot,
|
|
457
|
+
// and a projected parameter (not a request field) is not readable. Then the ordinary type check.
|
|
458
|
+
const WHEN_NODES = new Set(["eq", "neq", "lt", "lte", "gt", "gte", "and", "or", "not", "in", "subsetOf", "isNull", "isNotNull", "ref", "lit"]);
|
|
459
|
+
export function checkPermissionWhen(ont, expr, action, collect) {
|
|
460
|
+
const walk = (e, at) => {
|
|
461
|
+
const k = nodeKey(e, at);
|
|
462
|
+
const p = `${at}.${k}`;
|
|
463
|
+
if (!WHEN_NODES.has(k))
|
|
464
|
+
throw new ExprError(p, `${k} is not allowed in conditional_when; it reads only parameters, actor and literals`);
|
|
465
|
+
if (k === "lit")
|
|
466
|
+
return;
|
|
467
|
+
const v = e[k];
|
|
468
|
+
if (k === "ref") {
|
|
469
|
+
if (typeof v !== "string")
|
|
470
|
+
return; // the type check reports it
|
|
471
|
+
const [head, prop] = v.split(".");
|
|
472
|
+
if (head === "actor")
|
|
473
|
+
return; // the type check checks the property
|
|
474
|
+
if (head === "now")
|
|
475
|
+
throw new ExprError(p, "now is not allowed in conditional_when");
|
|
476
|
+
if (prop !== undefined)
|
|
477
|
+
throw new ExprError(p, `${v}: conditional_when reads no property; it reads a parameter as given`);
|
|
478
|
+
const param = own(ont.actionTypes[action]?.parameters, head);
|
|
479
|
+
if (param !== undefined && projectedFields(param) !== null)
|
|
480
|
+
throw new ExprError(p, `${head} is a projected parameter; conditional_when reads only request inputs`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (Array.isArray(v))
|
|
484
|
+
v.forEach((x, i) => walk(x, `${p}[${i}]`));
|
|
485
|
+
};
|
|
486
|
+
walk(expr, "$");
|
|
487
|
+
checkPredicate(ont, expr, { action, locals: {}, collect });
|
|
488
|
+
}
|
|
489
|
+
// Type-checks a derived property on its own and returns its declared type; throws ExprError when it is not well typed.
|
|
490
|
+
// With collect, its expression is the checked one (its literals are recorded).
|
|
491
|
+
export function derivedPropertyType(ont, id, collect) {
|
|
492
|
+
return derivedType(ont, id, "$", [], collect, true);
|
|
493
|
+
}
|
|
494
|
+
// The type of a value expression; throws ExprError when it is not well typed. `expected` types a bare literal.
|
|
495
|
+
export function valueType(ont, expr, ctx, expected) {
|
|
496
|
+
return valueTypeIn(ont, expr, ctx, expected, "$", []);
|
|
497
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./contract.js";
|
|
2
|
+
export * from "./checker.js";
|
|
3
|
+
export * from "./expr.js";
|
|
4
|
+
export * from "./evaluate.js";
|
|
5
|
+
export * from "./reconcile.js";
|
|
6
|
+
export * from "./patch.js";
|
|
7
|
+
export * from "./query.js";
|
|
8
|
+
export * from "./markdown.js";
|
|
9
|
+
export * from "./unknown.js";
|
|
10
|
+
export * from "./coverage.js";
|
|
11
|
+
export * from "./ledger.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./contract.js";
|
|
2
|
+
export * from "./checker.js";
|
|
3
|
+
export * from "./expr.js";
|
|
4
|
+
export * from "./evaluate.js";
|
|
5
|
+
export * from "./reconcile.js";
|
|
6
|
+
export * from "./patch.js";
|
|
7
|
+
export * from "./query.js";
|
|
8
|
+
export * from "./markdown.js";
|
|
9
|
+
export * from "./unknown.js";
|
|
10
|
+
export * from "./coverage.js";
|
|
11
|
+
export * from "./ledger.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type Sha256 = (text: string) => string;
|
|
2
|
+
/** The key of a unit: the document, the titles from the top-level ancestor to the heading, and the occurrence (from 1) of that path. */
|
|
3
|
+
export interface UnitKey {
|
|
4
|
+
doc: string;
|
|
5
|
+
path: string[];
|
|
6
|
+
n: number;
|
|
7
|
+
}
|
|
8
|
+
/** A unit: the text from one heading line to the next heading line of any level (or from the start, heading null). */
|
|
9
|
+
export interface DocUnit extends UnitKey {
|
|
10
|
+
heading: string | null;
|
|
11
|
+
text: string;
|
|
12
|
+
sha256: string;
|
|
13
|
+
empty: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface LedgerDoc {
|
|
16
|
+
doc: string;
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
export type LedgerUnitEntry = UnitKey & {
|
|
20
|
+
sha256: string;
|
|
21
|
+
} & ({
|
|
22
|
+
claims: string[];
|
|
23
|
+
} | {
|
|
24
|
+
noClaim: string;
|
|
25
|
+
});
|
|
26
|
+
export interface LedgerOutlineEntry {
|
|
27
|
+
doc: string;
|
|
28
|
+
outline: string;
|
|
29
|
+
}
|
|
30
|
+
export type LedgerEntry = (LedgerUnitEntry | LedgerOutlineEntry) & {
|
|
31
|
+
line: number;
|
|
32
|
+
};
|
|
33
|
+
/** `entry`: the ledger line of the unit as it is now, without `claims` and `noClaim`; the writer adds the decision. */
|
|
34
|
+
export interface LedgerUnitFailure extends UnitKey {
|
|
35
|
+
sha256: string;
|
|
36
|
+
firstLine: string;
|
|
37
|
+
entry: string;
|
|
38
|
+
}
|
|
39
|
+
export interface LedgerResult {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
docs: number;
|
|
42
|
+
units: number;
|
|
43
|
+
emptyUnits: number;
|
|
44
|
+
changed: LedgerUnitFailure[];
|
|
45
|
+
unreviewed: LedgerUnitFailure[];
|
|
46
|
+
stale: (UnitKey | {
|
|
47
|
+
doc: string;
|
|
48
|
+
outline: true;
|
|
49
|
+
})[];
|
|
50
|
+
outline: {
|
|
51
|
+
doc: string;
|
|
52
|
+
sha256: string;
|
|
53
|
+
}[];
|
|
54
|
+
dangling: (UnitKey & {
|
|
55
|
+
pointer: string;
|
|
56
|
+
})[];
|
|
57
|
+
}
|
|
58
|
+
/** The text of a key: `<doc> <path as JSON> <n>`. */
|
|
59
|
+
export declare const unitKeyText: (k: UnitKey) => string;
|
|
60
|
+
/**
|
|
61
|
+
* The units of one document. The text is normalized first: the BOM is removed and CRLF becomes LF. Generated blocks
|
|
62
|
+
* are not removed: their text is hashed as other text, so a change in it re-opens its unit (fail closed). Then the text
|
|
63
|
+
* is cut at each ATX heading outside fences. The units keep their line breaks, so together they are exactly the
|
|
64
|
+
* normalized text. The text before the first heading is the unit with heading null and path []; it is always the first unit.
|
|
65
|
+
*/
|
|
66
|
+
export declare function docUnits(d: LedgerDoc, sha256: Sha256): DocUnit[];
|
|
67
|
+
/**
|
|
68
|
+
* The outline of a document: the sha256 of the JSON text of the ordered list of `[path, n, heading line]` of all its
|
|
69
|
+
* units, empty units included. The heading line is the exact first line (null for the text before the first heading),
|
|
70
|
+
* so a change of level or of closing #s changes the outline, also for an empty unit.
|
|
71
|
+
*/
|
|
72
|
+
export declare const outlineHash: (units: DocUnit[], sha256: Sha256) => string;
|
|
73
|
+
/** Ledger order: doc, then the outline entry before the unit entries, then path element by element (a prefix first), then n. */
|
|
74
|
+
export declare function compareEntries(a: UnitKey | {
|
|
75
|
+
doc: string;
|
|
76
|
+
outline: unknown;
|
|
77
|
+
}, b: UnitKey | {
|
|
78
|
+
doc: string;
|
|
79
|
+
outline: unknown;
|
|
80
|
+
}): number;
|
|
81
|
+
/**
|
|
82
|
+
* Checks the shape of the ledger lines (`{line, value}` from a JSON Lines file, blank lines skipped) and returns the
|
|
83
|
+
* entries. The first line is `{"avouchLedger":1}`; the entries follow in ledger order, each key once.
|
|
84
|
+
* Throws `<line>: <reason>` on the first error.
|
|
85
|
+
*/
|
|
86
|
+
export declare function parseLedger(lines: readonly {
|
|
87
|
+
line: number;
|
|
88
|
+
value: unknown;
|
|
89
|
+
}[]): LedgerEntry[];
|
|
90
|
+
/**
|
|
91
|
+
* Resolves a JSON Pointer (RFC 6901: `~1` is `/`, `~0` is `~`; a `~` before another character, and the root "", do not resolve) in a value. One extension: on an array, a segment
|
|
92
|
+
* selects the element whose `id` is the segment, or else the string element equal to it. A numeric index does not select.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolvePointer(value: unknown, pointer: string): boolean;
|
|
95
|
+
/** The gate: the current units of the documents against the ledger entries, and the claims against the ontology. */
|
|
96
|
+
export declare function verifyLedger(docs: readonly LedgerDoc[], entries: readonly LedgerEntry[], ontology: unknown, sha256: Sha256): LedgerResult;
|