@githonllc/avouch 0.0.0-stage → 0.1.1
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 +161 -2
- package/SPEC.md +337 -0
- package/dist/cli/avouch.d.ts +2 -0
- package/dist/cli/avouch.js +51 -0
- package/dist/cli/check.d.ts +8 -0
- package/dist/cli/check.js +63 -0
- package/dist/cli/query.d.ts +2 -0
- package/dist/cli/query.js +150 -0
- package/dist/src/checker.d.ts +3 -0
- package/dist/src/checker.js +1001 -0
- package/dist/src/contract.d.ts +135 -0
- package/dist/src/contract.js +3 -0
- package/dist/src/evaluate.d.ts +66 -0
- package/dist/src/evaluate.js +478 -0
- package/dist/src/expr.d.ts +190 -0
- package/dist/src/expr.js +426 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +8 -0
- package/dist/src/markdown.d.ts +15 -0
- package/dist/src/markdown.js +476 -0
- package/dist/src/patch.d.ts +6 -0
- package/dist/src/patch.js +37 -0
- package/dist/src/query.d.ts +304 -0
- package/dist/src/query.js +681 -0
- package/dist/src/reconcile.d.ts +53 -0
- package/dist/src/reconcile.js +78 -0
- package/dist/src/stage1-check.d.ts +29 -0
- package/dist/src/stage1-check.js +305 -0
- package/dist/src/unknown.d.ts +10 -0
- package/dist/src/unknown.js +172 -0
- package/docs/adapter-guide.md +155 -0
- package/ontology.schema.json +1690 -0
- package/package.json +53 -4
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export type Scalar = "enum" | "timestamp" | "duration" | "date" | "timezone" | "id" | "integer" | "boolean" | {
|
|
2
|
+
ref: string;
|
|
3
|
+
};
|
|
4
|
+
export type Type = Scalar | {
|
|
5
|
+
set: Scalar | "string";
|
|
6
|
+
};
|
|
7
|
+
export declare const SCALAR_TYPE_NAMES: readonly ["enum", "timestamp", "duration", "date", "timezone", "id", "integer", "boolean"];
|
|
8
|
+
export declare const EXPR_NODES: readonly ["ref", "lit", "plus", "dateIn", "derived", "eq", "neq", "lt", "lte", "gt", "gte", "and", "or", "not", "in", "subsetOf", "isNull", "isNotNull", "exists", "none", "all", "nav"];
|
|
9
|
+
export type Lit = string | number | (string | number)[];
|
|
10
|
+
export type Nav = {
|
|
11
|
+
nav: {
|
|
12
|
+
from: {
|
|
13
|
+
ref: string;
|
|
14
|
+
};
|
|
15
|
+
link: string;
|
|
16
|
+
dir: "forward" | "reverse";
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type Quantifier = {
|
|
20
|
+
as: string;
|
|
21
|
+
in: Nav;
|
|
22
|
+
where?: Expr;
|
|
23
|
+
};
|
|
24
|
+
export type ValueExpr = {
|
|
25
|
+
ref: string;
|
|
26
|
+
} | {
|
|
27
|
+
lit: Lit;
|
|
28
|
+
} | {
|
|
29
|
+
plus: [Expr, Expr];
|
|
30
|
+
} | {
|
|
31
|
+
dateIn: [Expr, Expr];
|
|
32
|
+
} | {
|
|
33
|
+
derived: {
|
|
34
|
+
id: string;
|
|
35
|
+
of: {
|
|
36
|
+
ref: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export type PredicateExpr = {
|
|
41
|
+
eq: [Expr, Expr];
|
|
42
|
+
} | {
|
|
43
|
+
neq: [Expr, Expr];
|
|
44
|
+
} | {
|
|
45
|
+
lt: [Expr, Expr];
|
|
46
|
+
} | {
|
|
47
|
+
lte: [Expr, Expr];
|
|
48
|
+
} | {
|
|
49
|
+
gt: [Expr, Expr];
|
|
50
|
+
} | {
|
|
51
|
+
gte: [Expr, Expr];
|
|
52
|
+
} | {
|
|
53
|
+
and: Expr[];
|
|
54
|
+
} | {
|
|
55
|
+
or: Expr[];
|
|
56
|
+
} | {
|
|
57
|
+
not: [Expr];
|
|
58
|
+
} | {
|
|
59
|
+
in: [Expr, Expr];
|
|
60
|
+
} | {
|
|
61
|
+
subsetOf: [Expr, Expr];
|
|
62
|
+
} | {
|
|
63
|
+
isNull: [Expr];
|
|
64
|
+
} | {
|
|
65
|
+
isNotNull: [Expr];
|
|
66
|
+
} | {
|
|
67
|
+
exists: Quantifier;
|
|
68
|
+
} | {
|
|
69
|
+
none: Quantifier;
|
|
70
|
+
} | {
|
|
71
|
+
all: Quantifier;
|
|
72
|
+
};
|
|
73
|
+
export type Expr = ValueExpr | PredicateExpr;
|
|
74
|
+
export declare const DISPOSITION_CLASSES: readonly ["rejected", "recorded", "pending", "applied"];
|
|
75
|
+
export type DispositionClass = (typeof DISPOSITION_CLASSES)[number];
|
|
76
|
+
export declare const INVOCATION_RESULTS: readonly ["REPLAYED", "IDEMPOTENCY_CONFLICT", "INVALID_BINDING", "SCOPE_DENIED", "PERMISSION_DENIED"];
|
|
77
|
+
export type Principals = {
|
|
78
|
+
kinds: string[];
|
|
79
|
+
cite?: unknown;
|
|
80
|
+
};
|
|
81
|
+
export type Alt = {
|
|
82
|
+
keys: string[];
|
|
83
|
+
conditional_keys?: string[];
|
|
84
|
+
principals?: Principals;
|
|
85
|
+
};
|
|
86
|
+
export type Permission = "unknown" | {
|
|
87
|
+
none: string;
|
|
88
|
+
principals?: Principals;
|
|
89
|
+
} | Alt | {
|
|
90
|
+
any_of: Alt[];
|
|
91
|
+
};
|
|
92
|
+
export type DecisionRow = {
|
|
93
|
+
when: {
|
|
94
|
+
passes: string;
|
|
95
|
+
} | {
|
|
96
|
+
fails: string;
|
|
97
|
+
};
|
|
98
|
+
result: string;
|
|
99
|
+
next?: unknown;
|
|
100
|
+
};
|
|
101
|
+
export type Decision = {
|
|
102
|
+
hitPolicy: "first";
|
|
103
|
+
rows: DecisionRow[];
|
|
104
|
+
otherwise?: {
|
|
105
|
+
result: string;
|
|
106
|
+
next?: unknown;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
export type Condition = {
|
|
110
|
+
id: string;
|
|
111
|
+
expr?: Expr;
|
|
112
|
+
unspecified?: string;
|
|
113
|
+
};
|
|
114
|
+
export type ActionType = {
|
|
115
|
+
parameters?: Record<string, {
|
|
116
|
+
type?: Type;
|
|
117
|
+
optional?: true;
|
|
118
|
+
values?: string[];
|
|
119
|
+
}>;
|
|
120
|
+
permission: Permission;
|
|
121
|
+
conditions: Condition[];
|
|
122
|
+
decision?: Decision;
|
|
123
|
+
};
|
|
124
|
+
export interface Stage1Ontology {
|
|
125
|
+
scope?: {
|
|
126
|
+
by: string;
|
|
127
|
+
global?: {
|
|
128
|
+
type: string;
|
|
129
|
+
}[];
|
|
130
|
+
root?: {
|
|
131
|
+
type: string;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
dispositions?: Record<string, {
|
|
135
|
+
class: DispositionClass;
|
|
136
|
+
}>;
|
|
137
|
+
objectTypes: Record<string, {
|
|
138
|
+
datasource: string;
|
|
139
|
+
properties: Record<string, {
|
|
140
|
+
type?: Type;
|
|
141
|
+
}>;
|
|
142
|
+
}>;
|
|
143
|
+
linkTypes: {
|
|
144
|
+
id: string;
|
|
145
|
+
from: string;
|
|
146
|
+
to: string;
|
|
147
|
+
via: string;
|
|
148
|
+
table?: string;
|
|
149
|
+
}[];
|
|
150
|
+
derivedProperties?: Record<string, {
|
|
151
|
+
of: string;
|
|
152
|
+
type?: Type;
|
|
153
|
+
expr?: Expr;
|
|
154
|
+
}>;
|
|
155
|
+
actionTypes: Record<string, ActionType>;
|
|
156
|
+
}
|
|
157
|
+
export declare class ExprError extends Error {
|
|
158
|
+
readonly path: string;
|
|
159
|
+
constructor(path: string, msg: string);
|
|
160
|
+
}
|
|
161
|
+
export interface ExprReads {
|
|
162
|
+
reads: {
|
|
163
|
+
prop: string;
|
|
164
|
+
lit?: string;
|
|
165
|
+
}[];
|
|
166
|
+
literals: (string | number)[];
|
|
167
|
+
undefinedDerived: Set<string>;
|
|
168
|
+
}
|
|
169
|
+
export declare const newReads: () => ExprReads;
|
|
170
|
+
export type ExprContext = {
|
|
171
|
+
action?: string;
|
|
172
|
+
self?: string;
|
|
173
|
+
locals: Record<string, string>;
|
|
174
|
+
collect?: ExprReads;
|
|
175
|
+
nested?: boolean;
|
|
176
|
+
};
|
|
177
|
+
export declare const RESERVED_NAMES: readonly ["now", "actor", "self"];
|
|
178
|
+
export declare const DATE_RE: RegExp;
|
|
179
|
+
export declare const TIMESTAMP_MIN = -62135510400000;
|
|
180
|
+
export declare const TIMESTAMP_MAX = 253402214399999;
|
|
181
|
+
export declare const isObj: (x: unknown) => x is Record<string, any>;
|
|
182
|
+
export declare const own: <V>(r: Readonly<Record<string, V>> | undefined, k: string) => V | undefined;
|
|
183
|
+
export declare function typeEquals(a: Type | "string", b: Type | "string"): boolean;
|
|
184
|
+
export declare function validTimezone(v: string): boolean;
|
|
185
|
+
export declare function fitsType(v: unknown, t: Type | "string"): boolean;
|
|
186
|
+
export declare function objectBinding(ont: Stage1Ontology, name: string, ctx: ExprContext): string | undefined;
|
|
187
|
+
export declare function navType(ont: Stage1Ontology, n: unknown, ctx: ExprContext, at: string): string;
|
|
188
|
+
export declare function checkPredicate(ont: Stage1Ontology, expr: unknown, ctx: ExprContext): void;
|
|
189
|
+
export declare function derivedPropertyType(ont: Stage1Ontology, id: string, collect?: ExprReads): Type;
|
|
190
|
+
export declare function valueType(ont: Stage1Ontology, expr: unknown, ctx: ExprContext, expected?: Type): Type;
|
package/dist/src/expr.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
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", "boolean"];
|
|
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
|
+
export const INVOCATION_RESULTS = ["REPLAYED", "IDEMPOTENCY_CONFLICT", "INVALID_BINDING", "SCOPE_DENIED", "PERMISSION_DENIED"];
|
|
13
|
+
export class ExprError extends Error {
|
|
14
|
+
path;
|
|
15
|
+
constructor(path, msg) {
|
|
16
|
+
super(`${path}: ${msg}`);
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.name = "ExprError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const newReads = () => ({ reads: [], literals: [], undefinedDerived: new Set() });
|
|
22
|
+
const VALUE_NODES = new Set(["ref", "lit", "plus", "dateIn", "derived"]);
|
|
23
|
+
const COMPARE = new Set(["eq", "neq", "lt", "lte", "gt", "gte"]);
|
|
24
|
+
const ORDERED = new Set(["timestamp", "date", "duration", "integer"]);
|
|
25
|
+
// Names an expression reads without a declaration; no parameter or quantifier variable may take them.
|
|
26
|
+
export const RESERVED_NAMES = ["now", "actor", "self"];
|
|
27
|
+
const RESERVED = new Set(RESERVED_NAMES);
|
|
28
|
+
export const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
29
|
+
// Timestamp bounds, inclusive: 0001-01-02T00:00:00.000Z .. 9999-12-30T23:59:59.999Z. The one-day margin covers every UTC
|
|
30
|
+
// offset, so dateIn always gives a year from 1 to 9999.
|
|
31
|
+
export const TIMESTAMP_MIN = -62135510400000;
|
|
32
|
+
export const TIMESTAMP_MAX = 253402214399999;
|
|
33
|
+
export const isObj = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
34
|
+
// Own keys only: a name such as `toString` must not resolve to an Object.prototype member.
|
|
35
|
+
export const own = (r, k) => (r !== undefined && Object.hasOwn(r, k) ? r[k] : undefined);
|
|
36
|
+
function nodeKey(e, path) {
|
|
37
|
+
if (!isObj(e))
|
|
38
|
+
throw new ExprError(path, "an expression must be a single-key map");
|
|
39
|
+
const ks = Object.keys(e);
|
|
40
|
+
if (ks.length !== 1)
|
|
41
|
+
throw new ExprError(path, `an expression must have exactly one key, found ${ks.length}`);
|
|
42
|
+
return ks[0];
|
|
43
|
+
}
|
|
44
|
+
function args(e, k, n, path) {
|
|
45
|
+
const a = e[k];
|
|
46
|
+
if (!Array.isArray(a))
|
|
47
|
+
throw new ExprError(path, `${k} takes an array of operands`);
|
|
48
|
+
if (n === "2+" ? a.length < 2 : a.length !== n)
|
|
49
|
+
throw new ExprError(path, `${k} takes ${n === "2+" ? "at least 2" : n} operands, found ${a.length}`);
|
|
50
|
+
return a;
|
|
51
|
+
}
|
|
52
|
+
const isLit = (e) => isObj(e) && Object.keys(e).length === 1 && "lit" in e;
|
|
53
|
+
export function typeEquals(a, b) {
|
|
54
|
+
if (typeof a === "string" || typeof b === "string")
|
|
55
|
+
return a === b;
|
|
56
|
+
if ("ref" in a)
|
|
57
|
+
return "ref" in b && a.ref === b.ref;
|
|
58
|
+
if ("set" in a)
|
|
59
|
+
return "set" in b && typeEquals(a.set, b.set);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const showType = (t) => typeof t === "string" ? t : "ref" in t ? `{ref: ${t.ref}}` : `{set: ${showType(t.set)}}`;
|
|
63
|
+
const isSet = (t) => typeof t !== "string" && "set" in t;
|
|
64
|
+
export function validTimezone(v) {
|
|
65
|
+
try {
|
|
66
|
+
new Intl.DateTimeFormat("en-US", { timeZone: v });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof RangeError)
|
|
71
|
+
return false;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// The one format table, for literals and for parameter values alike.
|
|
76
|
+
export function fitsType(v, t) {
|
|
77
|
+
if (typeof t !== "string") {
|
|
78
|
+
if ("set" in t) {
|
|
79
|
+
const el = t.set;
|
|
80
|
+
return Array.isArray(v) && v.every((x) => fitsType(x, el));
|
|
81
|
+
}
|
|
82
|
+
return typeof v === "string" && v !== "";
|
|
83
|
+
}
|
|
84
|
+
switch (t) {
|
|
85
|
+
case "id":
|
|
86
|
+
return typeof v === "string" && v !== "";
|
|
87
|
+
case "enum":
|
|
88
|
+
case "string":
|
|
89
|
+
return typeof v === "string";
|
|
90
|
+
case "timezone":
|
|
91
|
+
return typeof v === "string" && validTimezone(v);
|
|
92
|
+
case "date":
|
|
93
|
+
return typeof v === "string" && DATE_RE.test(v);
|
|
94
|
+
case "timestamp":
|
|
95
|
+
return Number.isSafeInteger(v) && v >= TIMESTAMP_MIN && v <= TIMESTAMP_MAX;
|
|
96
|
+
case "duration":
|
|
97
|
+
case "integer":
|
|
98
|
+
return Number.isSafeInteger(v);
|
|
99
|
+
case "boolean":
|
|
100
|
+
return typeof v === "boolean";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// A literal is a string, a number, or an array of them; there are no Boolean literals.
|
|
104
|
+
const isLitValue = (v) => typeof v === "string" || typeof v === "number" || (Array.isArray(v) && v.every((x) => typeof x === "string" || typeof x === "number"));
|
|
105
|
+
const litFits = (v, t) => isLitValue(v) && fitsType(v, t);
|
|
106
|
+
// A literal in a position of type t; a set literal is non-empty with elements of one JSON type. Records the literal.
|
|
107
|
+
function literal(v, t, ctx, at) {
|
|
108
|
+
if (Array.isArray(v) && v.length === 0)
|
|
109
|
+
throw new ExprError(at, "a set literal must not be empty");
|
|
110
|
+
if (Array.isArray(v) && new Set(v.map((x) => typeof x)).size > 1)
|
|
111
|
+
throw new ExprError(at, `set literal ${JSON.stringify(v)} mixes element types`);
|
|
112
|
+
if (!litFits(v, t))
|
|
113
|
+
throw new ExprError(at, `literal ${JSON.stringify(v)} does not fit ${showType(t)}`);
|
|
114
|
+
if (ctx.collect && !ctx.nested)
|
|
115
|
+
ctx.collect.literals.push(...(Array.isArray(v) ? v : [v]));
|
|
116
|
+
}
|
|
117
|
+
// An object binding: a ref-typed parameter of the action, a quantifier variable, or `self`. Returns its object type.
|
|
118
|
+
export function objectBinding(ont, name, ctx) {
|
|
119
|
+
if (name === "self")
|
|
120
|
+
return ctx.self;
|
|
121
|
+
if (Object.hasOwn(ctx.locals, name))
|
|
122
|
+
return ctx.locals[name];
|
|
123
|
+
if (ctx.action === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const t = own(ont.actionTypes[ctx.action]?.parameters, name)?.type;
|
|
126
|
+
return t !== undefined && typeof t !== "string" && "ref" in t ? t.ref : undefined;
|
|
127
|
+
}
|
|
128
|
+
function refType(ont, path, ctx, at) {
|
|
129
|
+
const parts = path.split(".");
|
|
130
|
+
if (parts.length > 2 || parts.some((p) => p === ""))
|
|
131
|
+
throw new ExprError(at, `ref ${path}: at most one dot`);
|
|
132
|
+
const [head, prop] = parts;
|
|
133
|
+
if (head === "now") {
|
|
134
|
+
if (prop !== undefined)
|
|
135
|
+
throw new ExprError(at, "now has no properties");
|
|
136
|
+
return "timestamp";
|
|
137
|
+
}
|
|
138
|
+
if (head === "actor") {
|
|
139
|
+
if (prop === "id")
|
|
140
|
+
return "id";
|
|
141
|
+
if (prop === "keys")
|
|
142
|
+
return { set: "string" };
|
|
143
|
+
throw new ExprError(at, `actor.${prop ?? ""} is not readable; use actor.id or actor.keys`);
|
|
144
|
+
}
|
|
145
|
+
if (head === "self" && ctx.self === undefined)
|
|
146
|
+
throw new ExprError(at, "self is valid only in a derived property expression");
|
|
147
|
+
const obj = objectBinding(ont, head, ctx);
|
|
148
|
+
if (obj !== undefined && own(ont.objectTypes, obj) === undefined)
|
|
149
|
+
throw new ExprError(at, `object type ${obj} is not declared`);
|
|
150
|
+
if (prop === undefined) {
|
|
151
|
+
if (obj !== undefined)
|
|
152
|
+
return { ref: obj };
|
|
153
|
+
if (ctx.action === undefined)
|
|
154
|
+
throw new ExprError(at, `${head} is not visible here`);
|
|
155
|
+
const p = own(ont.actionTypes[ctx.action]?.parameters, head);
|
|
156
|
+
if (p === undefined)
|
|
157
|
+
throw new ExprError(at, `${head} is not a parameter, a binding or a reserved name`);
|
|
158
|
+
if (p.type === undefined)
|
|
159
|
+
throw new ExprError(at, `parameter ${head} has no type`);
|
|
160
|
+
return p.type;
|
|
161
|
+
}
|
|
162
|
+
if (obj === undefined)
|
|
163
|
+
throw new ExprError(at, `${head} is not an object binding`);
|
|
164
|
+
if (own(ont.derivedProperties, prop)?.of === obj)
|
|
165
|
+
throw new ExprError(at, `${prop} is a derived property; read it with {derived: {id, of}}`);
|
|
166
|
+
const t = own(ont.objectTypes[obj].properties, prop)?.type;
|
|
167
|
+
if (t === undefined)
|
|
168
|
+
throw new ExprError(at, `${obj}.${prop} is not a declared property with a type`);
|
|
169
|
+
ctx.collect?.reads.push({ prop: `${obj}.${prop}` });
|
|
170
|
+
return t;
|
|
171
|
+
}
|
|
172
|
+
function bindingName(ont, from, ctx, at) {
|
|
173
|
+
if (!isObj(from) || typeof from.ref !== "string" || Object.keys(from).length !== 1)
|
|
174
|
+
throw new ExprError(at, "expected {ref: <binding name>}");
|
|
175
|
+
if (from.ref.includes("."))
|
|
176
|
+
throw new ExprError(at, `${from.ref}: a binding name has no dot`);
|
|
177
|
+
const t = objectBinding(ont, from.ref, ctx);
|
|
178
|
+
if (t === undefined)
|
|
179
|
+
throw new ExprError(at, `${from.ref} is not an object binding`);
|
|
180
|
+
return t;
|
|
181
|
+
}
|
|
182
|
+
// top: the derived property is the checked expression itself (its literals are recorded), not one reached from another.
|
|
183
|
+
function derivedType(ont, id, at, stack, collect, top = false) {
|
|
184
|
+
const dp = own(ont.derivedProperties, id);
|
|
185
|
+
if (dp === undefined)
|
|
186
|
+
throw new ExprError(at, `derived property ${id} is not declared`);
|
|
187
|
+
if (dp.type === undefined)
|
|
188
|
+
throw new ExprError(at, `derived property ${id} has no type`);
|
|
189
|
+
if (dp.expr === undefined)
|
|
190
|
+
collect?.undefinedDerived.add(id);
|
|
191
|
+
else {
|
|
192
|
+
const ctx = { self: dp.of, locals: {}, collect, nested: !top };
|
|
193
|
+
if (stack.includes(id))
|
|
194
|
+
throw new ExprError(at, `derived properties form a cycle: ${[...stack, id].join(" -> ")}`);
|
|
195
|
+
const where = `${at}<${id}>`;
|
|
196
|
+
const k = nodeKey(dp.expr, where);
|
|
197
|
+
if (!VALUE_NODES.has(k)) {
|
|
198
|
+
// A predicate root is a Boolean expression: the declared type must be boolean.
|
|
199
|
+
if (dp.type !== "boolean")
|
|
200
|
+
throw new ExprError(at, `derived property ${id}: a predicate expression needs the declared type boolean, not ${showType(dp.type)}`);
|
|
201
|
+
checkPredicateIn(ont, dp.expr, ctx, where, [...stack, id]);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// A boolean derived property has a predicate root (its definition is a Boolean expression).
|
|
205
|
+
if (dp.type === "boolean")
|
|
206
|
+
throw new ExprError(at, `derived property ${id}: declared boolean, so its expression root must be a predicate, not ${k}`);
|
|
207
|
+
const t = valueTypeIn(ont, dp.expr, ctx, dp.type, where, [...stack, id]);
|
|
208
|
+
if (!typeEquals(t, dp.type))
|
|
209
|
+
throw new ExprError(at, `derived property ${id}: expression type ${showType(t)} is not the declared ${showType(dp.type)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return dp.type;
|
|
213
|
+
}
|
|
214
|
+
function operand(ont, e, ctx, want, at, stack) {
|
|
215
|
+
const t = valueTypeIn(ont, e, ctx, want, at, stack);
|
|
216
|
+
if (!typeEquals(t, want))
|
|
217
|
+
throw new ExprError(at, `expected ${showType(want)}, found ${showType(t)}`);
|
|
218
|
+
}
|
|
219
|
+
function valueTypeIn(ont, e, ctx, expected, at, stack) {
|
|
220
|
+
const k = nodeKey(e, at);
|
|
221
|
+
const node = e;
|
|
222
|
+
const p = `${at}.${k}`;
|
|
223
|
+
switch (k) {
|
|
224
|
+
case "ref":
|
|
225
|
+
if (typeof node.ref !== "string")
|
|
226
|
+
throw new ExprError(p, "ref takes a path string");
|
|
227
|
+
return refType(ont, node.ref, ctx, p);
|
|
228
|
+
case "lit":
|
|
229
|
+
if (expected === undefined)
|
|
230
|
+
throw new ExprError(p, "a literal takes its type from its position, and this position gives none");
|
|
231
|
+
if (expected === "boolean")
|
|
232
|
+
throw new ExprError(p, "there are no boolean literals; write x or not(x)");
|
|
233
|
+
literal(node.lit, expected, ctx, p);
|
|
234
|
+
return expected;
|
|
235
|
+
case "plus":
|
|
236
|
+
case "dateIn": {
|
|
237
|
+
const [a, b] = args(node, k, 2, p);
|
|
238
|
+
if (isLit(a) && isLit(b))
|
|
239
|
+
throw new ExprError(p, `${k} with two literal operands`);
|
|
240
|
+
operand(ont, a, ctx, "timestamp", `${p}[0]`, stack);
|
|
241
|
+
operand(ont, b, ctx, k === "plus" ? "duration" : "timezone", `${p}[1]`, stack);
|
|
242
|
+
return k === "plus" ? "timestamp" : "date";
|
|
243
|
+
}
|
|
244
|
+
case "derived": {
|
|
245
|
+
const d = node.derived;
|
|
246
|
+
if (!isObj(d) || typeof d.id !== "string")
|
|
247
|
+
throw new ExprError(p, "derived takes {id, of: {ref}}");
|
|
248
|
+
const of = bindingName(ont, d.of, ctx, `${p}.of`);
|
|
249
|
+
const dp = own(ont.derivedProperties, d.id);
|
|
250
|
+
if (dp !== undefined && dp.of !== of)
|
|
251
|
+
throw new ExprError(p, `derived property ${d.id} is of ${dp.of}, not ${of}`);
|
|
252
|
+
return derivedType(ont, d.id, p, stack, ctx.collect);
|
|
253
|
+
}
|
|
254
|
+
case "nav":
|
|
255
|
+
throw new ExprError(p, "nav is valid only as the `in` of a quantifier");
|
|
256
|
+
default:
|
|
257
|
+
if (EXPR_NODES.includes(k))
|
|
258
|
+
throw new ExprError(p, `predicate ${k} in a value position`);
|
|
259
|
+
throw new ExprError(p, `unknown expression node ${k}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// A literal compared with `{ref: P}`, where P (no dot) is a parameter of the action that declares `values`, must be one of
|
|
263
|
+
// them. Quantifier variables cannot reuse parameter names, so such a ref is always the parameter.
|
|
264
|
+
function checkValues(ont, e, xs, ctx, p) {
|
|
265
|
+
if (ctx.action === undefined || !isObj(e) || typeof e.ref !== "string" || e.ref.includes("."))
|
|
266
|
+
return;
|
|
267
|
+
const param = own(ont.actionTypes[ctx.action]?.parameters, e.ref);
|
|
268
|
+
const vs = param?.values;
|
|
269
|
+
if (param?.type !== "enum" || !Array.isArray(vs))
|
|
270
|
+
return;
|
|
271
|
+
for (const x of xs)
|
|
272
|
+
if (!vs.includes(x))
|
|
273
|
+
throw new ExprError(p, `literal ${JSON.stringify(x)} is not one of the values of parameter ${e.ref}`);
|
|
274
|
+
}
|
|
275
|
+
// Types the two operands of a binary operation; a literal takes the type of the other side.
|
|
276
|
+
function pair(ont, a, b, ctx, p, stack) {
|
|
277
|
+
if (isLit(a) && isLit(b))
|
|
278
|
+
throw new ExprError(p, "both operands are literals");
|
|
279
|
+
if (isLit(a)) {
|
|
280
|
+
const tb = valueTypeIn(ont, b, ctx, undefined, `${p}[1]`, stack);
|
|
281
|
+
return [valueTypeIn(ont, a, ctx, tb, `${p}[0]`, stack), tb];
|
|
282
|
+
}
|
|
283
|
+
const ta = valueTypeIn(ont, a, ctx, undefined, `${p}[0]`, stack);
|
|
284
|
+
return [ta, valueTypeIn(ont, b, ctx, ta, `${p}[1]`, stack)];
|
|
285
|
+
}
|
|
286
|
+
function checkPredicateIn(ont, e, ctx, at, stack) {
|
|
287
|
+
const k = nodeKey(e, at);
|
|
288
|
+
const node = e;
|
|
289
|
+
const p = `${at}.${k}`;
|
|
290
|
+
if (COMPARE.has(k)) {
|
|
291
|
+
const [a, b] = args(node, k, 2, p);
|
|
292
|
+
const n0 = ctx.collect?.reads.length ?? 0;
|
|
293
|
+
const [ta, tb] = pair(ont, a, b, ctx, p, stack);
|
|
294
|
+
if (isSet(ta) || isSet(tb))
|
|
295
|
+
throw new ExprError(p, `${k} on a set; sets have only in and subsetOf`);
|
|
296
|
+
// a property read compared by eq / neq with a string literal: record the literal with the read
|
|
297
|
+
const [l, r] = isLit(a) ? [a, b] : [b, a];
|
|
298
|
+
if ((k === "eq" || k === "neq") && ctx.collect?.reads.length === n0 + 1 && isLit(l) && typeof l.lit === "string" && isObj(r) && "ref" in r)
|
|
299
|
+
ctx.collect.reads[n0].lit = l.lit;
|
|
300
|
+
if (k === "eq" || k === "neq") {
|
|
301
|
+
const idVsRef = (x, y) => x === "id" && typeof y !== "string" && "ref" in y;
|
|
302
|
+
if (!typeEquals(ta, tb) && !idVsRef(ta, tb) && !idVsRef(tb, ta))
|
|
303
|
+
throw new ExprError(p, `${k} compares ${showType(ta)} with ${showType(tb)}`);
|
|
304
|
+
if (isLit(l))
|
|
305
|
+
checkValues(ont, r, [l.lit], ctx, p);
|
|
306
|
+
}
|
|
307
|
+
else if (typeof ta !== "string" || !ORDERED.has(ta) || !typeEquals(ta, tb))
|
|
308
|
+
throw new ExprError(p, `${k} needs two operands of one type among timestamp, date, duration, integer; found ${showType(ta)} and ${showType(tb)}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
switch (k) {
|
|
312
|
+
case "and":
|
|
313
|
+
case "or":
|
|
314
|
+
args(node, k, "2+", p).forEach((x, i) => checkPredicateIn(ont, x, ctx, `${p}[${i}]`, stack));
|
|
315
|
+
return;
|
|
316
|
+
case "not":
|
|
317
|
+
checkPredicateIn(ont, args(node, k, 1, p)[0], ctx, `${p}[0]`, stack);
|
|
318
|
+
return;
|
|
319
|
+
case "in": {
|
|
320
|
+
const [a, s] = args(node, k, 2, p);
|
|
321
|
+
if (isLit(a) && isLit(s))
|
|
322
|
+
throw new ExprError(p, "both operands are literals");
|
|
323
|
+
if (isLit(a)) {
|
|
324
|
+
const ts = valueTypeIn(ont, s, ctx, undefined, `${p}[1]`, stack);
|
|
325
|
+
if (!isSet(ts))
|
|
326
|
+
throw new ExprError(`${p}[1]`, `in needs a set, found ${showType(ts)}`);
|
|
327
|
+
literal(a.lit, ts.set, ctx, `${p}[0]`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const ta = valueTypeIn(ont, a, ctx, undefined, `${p}[0]`, stack);
|
|
331
|
+
if (isSet(ta))
|
|
332
|
+
throw new ExprError(`${p}[0]`, "in takes an element, found a set");
|
|
333
|
+
const ts = valueTypeIn(ont, s, ctx, { set: ta }, `${p}[1]`, stack);
|
|
334
|
+
if (!isSet(ts) || !typeEquals(ts.set, ta))
|
|
335
|
+
throw new ExprError(p, `in needs {set: ${showType(ta)}}, found ${showType(ts)}`);
|
|
336
|
+
if (isLit(s) && Array.isArray(s.lit))
|
|
337
|
+
checkValues(ont, a, s.lit, ctx, p);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
case "subsetOf": {
|
|
341
|
+
const [a, b] = args(node, k, 2, p);
|
|
342
|
+
const [ta, tb] = pair(ont, a, b, ctx, p, stack);
|
|
343
|
+
if (!isSet(ta) || !typeEquals(ta, tb))
|
|
344
|
+
throw new ExprError(p, `subsetOf needs two sets of one element type, found ${showType(ta)} and ${showType(tb)}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
case "isNull":
|
|
348
|
+
case "isNotNull": {
|
|
349
|
+
const [a] = args(node, k, 1, p);
|
|
350
|
+
if (isLit(a))
|
|
351
|
+
throw new ExprError(`${p}[0]`, `${k} of a literal`);
|
|
352
|
+
valueTypeIn(ont, a, ctx, undefined, `${p}[0]`, stack);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
case "exists":
|
|
356
|
+
case "none":
|
|
357
|
+
case "all": {
|
|
358
|
+
const q = node[k];
|
|
359
|
+
if (!isObj(q) || typeof q.as !== "string")
|
|
360
|
+
throw new ExprError(p, `${k} takes {as, in, where?}`);
|
|
361
|
+
const as = q.as;
|
|
362
|
+
if (RESERVED.has(as))
|
|
363
|
+
throw new ExprError(`${p}.as`, `${as} is a reserved name`);
|
|
364
|
+
if (Object.hasOwn(ctx.locals, as))
|
|
365
|
+
throw new ExprError(`${p}.as`, `${as} is already bound by an outer quantifier`);
|
|
366
|
+
if (ctx.action !== undefined && own(ont.actionTypes[ctx.action]?.parameters, as) !== undefined)
|
|
367
|
+
throw new ExprError(`${p}.as`, `${as} is a parameter name`);
|
|
368
|
+
const elem = navType(ont, q.in, ctx, `${p}.in`);
|
|
369
|
+
if (q.where !== undefined)
|
|
370
|
+
checkPredicateIn(ont, q.where, { ...ctx, locals: { ...ctx.locals, [as]: elem } }, `${p}.where`, stack);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
default: {
|
|
374
|
+
if (!VALUE_NODES.has(k))
|
|
375
|
+
throw new ExprError(p, `unknown predicate node ${k}`);
|
|
376
|
+
// A value node of type boolean is a predicate: true / false / null give T / F / U.
|
|
377
|
+
if (k === "lit")
|
|
378
|
+
throw new ExprError(p, "a literal in a predicate position; there are no boolean literals");
|
|
379
|
+
const t = valueTypeIn(ont, e, ctx, undefined, at, stack);
|
|
380
|
+
if (t !== "boolean")
|
|
381
|
+
throw new ExprError(p, `value ${k} of type ${showType(t)} in a predicate position; only a boolean value is a predicate`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Returns the element object type of a navigation.
|
|
387
|
+
export function navType(ont, n, ctx, at) {
|
|
388
|
+
if (nodeKey(n, at) !== "nav")
|
|
389
|
+
throw new ExprError(at, "a quantifier ranges over a nav");
|
|
390
|
+
const v = n.nav;
|
|
391
|
+
const p = `${at}.nav`;
|
|
392
|
+
if (!isObj(v))
|
|
393
|
+
throw new ExprError(p, "nav takes {from, link, dir}");
|
|
394
|
+
const from = bindingName(ont, v.from, ctx, `${p}.from`);
|
|
395
|
+
const link = ont.linkTypes.find((l) => l.id === v.link);
|
|
396
|
+
if (link === undefined)
|
|
397
|
+
throw new ExprError(`${p}.link`, `link ${String(v.link)} is not declared`);
|
|
398
|
+
if (v.dir === "forward") {
|
|
399
|
+
if (from !== link.from)
|
|
400
|
+
throw new ExprError(p, `forward ${link.id} starts at ${link.from}, not ${from}`);
|
|
401
|
+
return link.to;
|
|
402
|
+
}
|
|
403
|
+
if (v.dir === "reverse") {
|
|
404
|
+
if (from !== link.to)
|
|
405
|
+
throw new ExprError(p, `reverse ${link.id} starts at ${link.to}, not ${from}`);
|
|
406
|
+
return link.from;
|
|
407
|
+
}
|
|
408
|
+
throw new ExprError(`${p}.dir`, "dir is forward or reverse");
|
|
409
|
+
}
|
|
410
|
+
// Throws ExprError when expr is not a well-typed predicate in ctx.
|
|
411
|
+
export function checkPredicate(ont, expr, ctx) {
|
|
412
|
+
if (ctx.action !== undefined)
|
|
413
|
+
for (const name of Object.keys(ont.actionTypes[ctx.action]?.parameters ?? {}))
|
|
414
|
+
if (RESERVED.has(name))
|
|
415
|
+
throw new ExprError("$", `parameter ${name} uses a reserved name`);
|
|
416
|
+
checkPredicateIn(ont, expr, ctx, "$", []);
|
|
417
|
+
}
|
|
418
|
+
// Type-checks a derived property on its own and returns its declared type; throws ExprError when it is not well typed.
|
|
419
|
+
// With collect, its expression is the checked one (its literals are recorded).
|
|
420
|
+
export function derivedPropertyType(ont, id, collect) {
|
|
421
|
+
return derivedType(ont, id, "$", [], collect, true);
|
|
422
|
+
}
|
|
423
|
+
// The type of a value expression; throws ExprError when it is not well typed. `expected` types a bare literal.
|
|
424
|
+
export function valueType(ont, expr, ctx, expected) {
|
|
425
|
+
return valueTypeIn(ont, expr, ctx, expected, "$", []);
|
|
426
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FormatConfig, type SourceFacts } from "./contract.js";
|
|
2
|
+
export interface MarkdownDoc {
|
|
3
|
+
path: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export type MarkdownFactName = "storeCatalog" | "eventCatalog" | "contextMembers" | "permissions";
|
|
7
|
+
export interface MarkdownConfig {
|
|
8
|
+
docs: string[];
|
|
9
|
+
facts: Partial<Record<MarkdownFactName, string>>;
|
|
10
|
+
profile: FormatConfig;
|
|
11
|
+
}
|
|
12
|
+
/** Parses the JSON configuration (`avouch.json`). Throws `<file>: <json path>: <problem>` on any error. */
|
|
13
|
+
export declare function parseMarkdownConfig(text: string, file: string): MarkdownConfig;
|
|
14
|
+
/** Facts from plain Markdown documents. Optional facts are present exactly when their anchor is configured. */
|
|
15
|
+
export declare function markdownFacts(docs: MarkdownDoc[], config: MarkdownConfig): SourceFacts;
|