@rhizomes/rhizomatic 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.
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +54 -0
- package/dist/alias.d.ts +4 -0
- package/dist/alias.d.ts.map +1 -0
- package/dist/alias.js +34 -0
- package/dist/alias.js.map +1 -0
- package/dist/cbor.d.ts +24 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +267 -0
- package/dist/cbor.js.map +1 -0
- package/dist/delta.d.ts +8 -0
- package/dist/delta.d.ts.map +1 -0
- package/dist/delta.js +92 -0
- package/dist/delta.js.map +1 -0
- package/dist/derivation.d.ts +29 -0
- package/dist/derivation.d.ts.map +1 -0
- package/dist/derivation.js +183 -0
- package/dist/derivation.js.map +1 -0
- package/dist/eval.d.ts +91 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/eval.js +318 -0
- package/dist/eval.js.map +1 -0
- package/dist/hash.d.ts +4 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +17 -0
- package/dist/hash.js.map +1 -0
- package/dist/http.d.ts +21 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +110 -0
- package/dist/http.js.map +1 -0
- package/dist/hview.d.ts +15 -0
- package/dist/hview.d.ts.map +1 -0
- package/dist/hview.js +72 -0
- package/dist/hview.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/json-profile.d.ts +4 -0
- package/dist/json-profile.d.ts.map +1 -0
- package/dist/json-profile.js +97 -0
- package/dist/json-profile.js.map +1 -0
- package/dist/pack.d.ts +5 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +227 -0
- package/dist/pack.js.map +1 -0
- package/dist/peer.d.ts +26 -0
- package/dist/peer.d.ts.map +1 -0
- package/dist/peer.js +111 -0
- package/dist/peer.js.map +1 -0
- package/dist/policy.d.ts +46 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +186 -0
- package/dist/policy.js.map +1 -0
- package/dist/pred.d.ts +78 -0
- package/dist/pred.d.ts.map +1 -0
- package/dist/pred.js +228 -0
- package/dist/pred.js.map +1 -0
- package/dist/reactor.d.ts +67 -0
- package/dist/reactor.d.ts.map +1 -0
- package/dist/reactor.js +433 -0
- package/dist/reactor.js.map +1 -0
- package/dist/schema-deltas.d.ts +14 -0
- package/dist/schema-deltas.d.ts.map +1 -0
- package/dist/schema-deltas.js +87 -0
- package/dist/schema-deltas.js.map +1 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +102 -0
- package/dist/schema.js.map +1 -0
- package/dist/set.d.ts +18 -0
- package/dist/set.d.ts.map +1 -0
- package/dist/set.js +83 -0
- package/dist/set.js.map +1 -0
- package/dist/sign.d.ts +8 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +44 -0
- package/dist/sign.js.map +1 -0
- package/dist/term-io.d.ts +13 -0
- package/dist/term-io.d.ts.map +1 -0
- package/dist/term-io.js +216 -0
- package/dist/term-io.js.map +1 -0
- package/dist/term-json.d.ts +7 -0
- package/dist/term-json.d.ts.map +1 -0
- package/dist/term-json.js +362 -0
- package/dist/term-json.js.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/vocab.d.ts +2 -0
- package/dist/vocab.d.ts.map +1 -0
- package/dist/vocab.js +4 -0
- package/dist/vocab.js.map +1 -0
- package/package.json +83 -0
- package/src/alias.ts +36 -0
- package/src/cbor.ts +280 -0
- package/src/delta.ts +89 -0
- package/src/derivation.ts +229 -0
- package/src/eval.ts +401 -0
- package/src/hash.ts +19 -0
- package/src/http.ts +124 -0
- package/src/hview.ts +91 -0
- package/src/index.ts +83 -0
- package/src/json-profile.ts +96 -0
- package/src/pack.ts +239 -0
- package/src/peer.ts +126 -0
- package/src/policy.ts +216 -0
- package/src/pred.ts +307 -0
- package/src/reactor.ts +490 -0
- package/src/schema-deltas.ts +100 -0
- package/src/schema.ts +111 -0
- package/src/set.ts +98 -0
- package/src/sign.ts +48 -0
- package/src/term-io.ts +228 -0
- package/src/term-json.ts +364 -0
- package/src/types.ts +38 -0
- package/src/vocab.ts +3 -0
package/src/policy.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Resolution policies and Views (SPEC-5, ERRATA-5). resolve : Policy -> HView -> View is the
|
|
2
|
+
// only exit from the algebra into application space; all pluralism is policy choice (P5).
|
|
3
|
+
|
|
4
|
+
import { type CborValue, array, bool, encode, float, map, tstr } from "./cbor.js";
|
|
5
|
+
import { bytesToHex } from "./hash.js";
|
|
6
|
+
import type { HVEntry, HView } from "./hview.js";
|
|
7
|
+
import { comparePrimitives, evalPred, type Pred } from "./pred.js";
|
|
8
|
+
import type { Primitive, Target } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export type View = Primitive | readonly View[] | { readonly [key: string]: View };
|
|
11
|
+
|
|
12
|
+
export type MergeFn = "max" | "min" | "sum" | "count" | "and" | "or" | "concatSorted";
|
|
13
|
+
|
|
14
|
+
export type Order =
|
|
15
|
+
| { readonly kind: "byTimestamp"; readonly dir: "desc" | "asc" }
|
|
16
|
+
| { readonly kind: "byAuthorRank"; readonly authors: readonly string[] }
|
|
17
|
+
| { readonly kind: "byPred"; readonly pred: Pred; readonly then: Order }
|
|
18
|
+
| { readonly kind: "lexById" };
|
|
19
|
+
|
|
20
|
+
export type PropPolicy =
|
|
21
|
+
| { readonly kind: "pick"; readonly order: Order }
|
|
22
|
+
| { readonly kind: "all"; readonly order: Order }
|
|
23
|
+
| { readonly kind: "merge"; readonly fn: MergeFn }
|
|
24
|
+
| { readonly kind: "conflicts"; readonly order: Order }
|
|
25
|
+
| { readonly kind: "absentAs"; readonly constant: Primitive; readonly then: PropPolicy };
|
|
26
|
+
|
|
27
|
+
export interface Policy {
|
|
28
|
+
readonly props: ReadonlyMap<string, PropPolicy>;
|
|
29
|
+
readonly default: PropPolicy;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- ordering (R3: every chain ends in an implicit lexById tiebreak) ------------------------------
|
|
33
|
+
|
|
34
|
+
function cmpByOrder(order: Order, a: HVEntry, b: HVEntry): number {
|
|
35
|
+
switch (order.kind) {
|
|
36
|
+
case "byTimestamp": {
|
|
37
|
+
const d = a.delta.claims.timestamp - b.delta.claims.timestamp;
|
|
38
|
+
if (d !== 0) return order.dir === "desc" ? -d : d;
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
case "byAuthorRank": {
|
|
42
|
+
const rank = (author: string) => {
|
|
43
|
+
const i = order.authors.indexOf(author);
|
|
44
|
+
return i === -1 ? order.authors.length : i;
|
|
45
|
+
};
|
|
46
|
+
return rank(a.delta.claims.author) - rank(b.delta.claims.author);
|
|
47
|
+
}
|
|
48
|
+
case "byPred": {
|
|
49
|
+
const am = evalPred(order.pred, a.delta) ? 0 : 1;
|
|
50
|
+
const bm = evalPred(order.pred, b.delta) ? 0 : 1;
|
|
51
|
+
if (am !== bm) return am - bm; // matches first
|
|
52
|
+
return cmpByOrder(order.then, a, b);
|
|
53
|
+
}
|
|
54
|
+
case "lexById":
|
|
55
|
+
return a.delta.id < b.delta.id ? -1 : a.delta.id > b.delta.id ? 1 : 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sortEntries(order: Order, entries: readonly HVEntry[]): HVEntry[] {
|
|
60
|
+
return [...entries].sort((a, b) => {
|
|
61
|
+
const primary = cmpByOrder(order, a, b);
|
|
62
|
+
if (primary !== 0) return primary;
|
|
63
|
+
return a.delta.id < b.delta.id ? -1 : a.delta.id > b.delta.id ? 1 : 0;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- candidate value extraction (R1) ---------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function renderTarget(t: Target, expansion: HView | undefined, policy: Policy): View {
|
|
70
|
+
if (expansion !== undefined) return resolveView(policy, expansion);
|
|
71
|
+
switch (t.kind) {
|
|
72
|
+
case "primitive":
|
|
73
|
+
return t.value;
|
|
74
|
+
case "entity":
|
|
75
|
+
return t.entity.id;
|
|
76
|
+
case "delta":
|
|
77
|
+
return t.deltaRef.delta;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function candidateValue(e: HVEntry, root: string, policy: Policy): View {
|
|
82
|
+
const nonFiling: Array<[string, View]> = [];
|
|
83
|
+
e.delta.claims.pointers.forEach((p, i) => {
|
|
84
|
+
const filing = p.target.kind === "entity" && p.target.entity.id === root;
|
|
85
|
+
if (filing) return;
|
|
86
|
+
nonFiling.push([p.role, renderTarget(p.target, e.expanded?.get(i), policy)]);
|
|
87
|
+
});
|
|
88
|
+
if (nonFiling.length === 0) return true; // the bare fact of the edge
|
|
89
|
+
if (nonFiling.length === 1) return nonFiling[0]![1];
|
|
90
|
+
const obj: Record<string, View> = {};
|
|
91
|
+
for (const [role, v] of nonFiling) {
|
|
92
|
+
const existing = obj[role];
|
|
93
|
+
if (existing === undefined) obj[role] = v;
|
|
94
|
+
else if (Array.isArray(existing)) obj[role] = [...existing, v];
|
|
95
|
+
else obj[role] = [existing, v];
|
|
96
|
+
}
|
|
97
|
+
return obj;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- View canonical form (R4) ----------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export function viewToCbor(v: View): CborValue {
|
|
103
|
+
if (typeof v === "string") return tstr(v);
|
|
104
|
+
if (typeof v === "number") return float(v);
|
|
105
|
+
if (typeof v === "boolean") return bool(v);
|
|
106
|
+
if (Array.isArray(v)) return array(v.map(viewToCbor));
|
|
107
|
+
const entries = Object.entries(v as { [key: string]: View }).map(
|
|
108
|
+
([k, x]): readonly [string, CborValue] => [k, viewToCbor(x)],
|
|
109
|
+
);
|
|
110
|
+
return map(entries);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function viewCanonicalHex(v: View): string {
|
|
114
|
+
return bytesToHex(encode(viewToCbor(v)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- resolution ------------------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
const ABSENT = Symbol("absent");
|
|
120
|
+
type Resolved = View | typeof ABSENT;
|
|
121
|
+
|
|
122
|
+
function isPrimitive(v: View): v is Primitive {
|
|
123
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function applyMerge(
|
|
127
|
+
fn: MergeFn,
|
|
128
|
+
entries: readonly HVEntry[],
|
|
129
|
+
root: string,
|
|
130
|
+
policy: Policy,
|
|
131
|
+
): Resolved {
|
|
132
|
+
// Fold in ascending delta-id order — float addition is order-dependent (R2).
|
|
133
|
+
const sorted = sortEntries({ kind: "lexById" }, entries);
|
|
134
|
+
if (fn === "count") return sorted.length === 0 ? ABSENT : sorted.length;
|
|
135
|
+
const prims = sorted
|
|
136
|
+
.map((e) => candidateValue(e, root, policy))
|
|
137
|
+
.filter((v): v is Primitive => isPrimitive(v));
|
|
138
|
+
switch (fn) {
|
|
139
|
+
case "max":
|
|
140
|
+
case "min": {
|
|
141
|
+
if (prims.length === 0) return ABSENT;
|
|
142
|
+
return prims.reduce((acc, v) => {
|
|
143
|
+
const c = comparePrimitives(v, acc);
|
|
144
|
+
return (fn === "max" ? c > 0 : c < 0) ? v : acc;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
case "sum": {
|
|
148
|
+
const nums = prims.filter((v): v is number => typeof v === "number");
|
|
149
|
+
if (nums.length === 0) return ABSENT;
|
|
150
|
+
return nums.reduce((a, b) => a + b, 0);
|
|
151
|
+
}
|
|
152
|
+
case "and":
|
|
153
|
+
case "or": {
|
|
154
|
+
const bools = prims.filter((v): v is boolean => typeof v === "boolean");
|
|
155
|
+
if (bools.length === 0) return ABSENT;
|
|
156
|
+
return fn === "and" ? bools.every(Boolean) : bools.some(Boolean);
|
|
157
|
+
}
|
|
158
|
+
case "concatSorted": {
|
|
159
|
+
if (prims.length === 0) return ABSENT;
|
|
160
|
+
return [...prims].sort(comparePrimitives);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applyPropPolicy(
|
|
166
|
+
pp: PropPolicy,
|
|
167
|
+
entries: readonly HVEntry[],
|
|
168
|
+
root: string,
|
|
169
|
+
policy: Policy,
|
|
170
|
+
): Resolved {
|
|
171
|
+
switch (pp.kind) {
|
|
172
|
+
case "pick": {
|
|
173
|
+
if (entries.length === 0) return ABSENT;
|
|
174
|
+
const sorted = sortEntries(pp.order, entries);
|
|
175
|
+
return candidateValue(sorted[0]!, root, policy);
|
|
176
|
+
}
|
|
177
|
+
case "all": {
|
|
178
|
+
if (entries.length === 0) return ABSENT;
|
|
179
|
+
return sortEntries(pp.order, entries).map((e) => candidateValue(e, root, policy));
|
|
180
|
+
}
|
|
181
|
+
case "merge":
|
|
182
|
+
return applyMerge(pp.fn, entries, root, policy);
|
|
183
|
+
case "conflicts": {
|
|
184
|
+
const sorted = sortEntries(pp.order, entries);
|
|
185
|
+
const seen = new Set<string>();
|
|
186
|
+
const distinct: View[] = [];
|
|
187
|
+
for (const e of sorted) {
|
|
188
|
+
const v = candidateValue(e, root, policy);
|
|
189
|
+
const key = viewCanonicalHex(v);
|
|
190
|
+
if (!seen.has(key)) {
|
|
191
|
+
seen.add(key);
|
|
192
|
+
distinct.push(v);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return distinct.length >= 2 ? distinct : ABSENT;
|
|
196
|
+
}
|
|
197
|
+
case "absentAs": {
|
|
198
|
+
const inner = applyPropPolicy(pp.then, entries, root, policy);
|
|
199
|
+
return inner === ABSENT ? pp.constant : inner;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// resolve(policy, HView) -> View. Deterministic; total; provenance-optional (SPEC-5 §2).
|
|
205
|
+
// The View covers every property named in the policy plus every HView property (R3).
|
|
206
|
+
export function resolveView(policy: Policy, hview: HView): View {
|
|
207
|
+
const keys = new Set<string>([...policy.props.keys(), ...hview.props.keys()]);
|
|
208
|
+
const obj: Record<string, View> = {};
|
|
209
|
+
for (const key of keys) {
|
|
210
|
+
const entries = hview.props.get(key) ?? [];
|
|
211
|
+
const pp = policy.props.get(key) ?? policy.default;
|
|
212
|
+
const v = applyPropPolicy(pp, entries, hview.id, policy);
|
|
213
|
+
if (v !== ABSENT) obj[key] = v;
|
|
214
|
+
}
|
|
215
|
+
return obj;
|
|
216
|
+
}
|
package/src/pred.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// The predicate grammar and its evaluator (SPEC-2 §3). Predicates are total, terminating,
|
|
2
|
+
// single-delta: they see one delta at a time, never the rest of the set.
|
|
3
|
+
|
|
4
|
+
import type { Delta, Pointer, Primitive } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export type Cmp = "eq" | "neq" | "lt" | "lte" | "gt" | "gte" | "prefix" | "inSet";
|
|
7
|
+
|
|
8
|
+
// A parameter slot in Const position, bound through fix's bindings (SPEC-2 §6, ERRATA-2 E15).
|
|
9
|
+
export interface Hole {
|
|
10
|
+
readonly kind: "hole";
|
|
11
|
+
readonly name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Bindings = ReadonlyMap<string, Primitive>;
|
|
15
|
+
|
|
16
|
+
// Resolve a possibly-parameterized primitive. Unbound holes fail loudly (E15).
|
|
17
|
+
export function resolveParam(p: Primitive | Hole, bindings: Bindings | undefined): Primitive {
|
|
18
|
+
if (typeof p !== "object") return p;
|
|
19
|
+
const bound = bindings?.get(p.name);
|
|
20
|
+
if (bound === undefined) throw new Error(`unbound hole "${p.name}" (E15)`);
|
|
21
|
+
return bound;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type StrMatch =
|
|
25
|
+
| { readonly kind: "exact"; readonly value: string }
|
|
26
|
+
| { readonly kind: "prefix"; readonly value: string }
|
|
27
|
+
| { readonly kind: "inSet"; readonly values: readonly string[] }
|
|
28
|
+
// The aliased closure (SPEC-9 §4): expanded to inSet against the ambient input before matching.
|
|
29
|
+
| {
|
|
30
|
+
readonly kind: "aliased";
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly via?: string;
|
|
33
|
+
readonly trust?: Pred;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ValMatch =
|
|
37
|
+
| { readonly kind: "vcmp"; readonly cmp: Cmp; readonly value: Primitive | Hole }
|
|
38
|
+
| { readonly kind: "between"; readonly lo: Primitive; readonly hi: Primitive }
|
|
39
|
+
| { readonly kind: "inSet"; readonly values: readonly Primitive[] };
|
|
40
|
+
|
|
41
|
+
// An entity to match: a literal id, the ambient root variable (ERRATA-2 E10), or a hole (E15).
|
|
42
|
+
export type EntityMatch =
|
|
43
|
+
| { readonly kind: "const"; readonly id: string }
|
|
44
|
+
| { readonly kind: "root" }
|
|
45
|
+
| Hole;
|
|
46
|
+
|
|
47
|
+
export interface PPred {
|
|
48
|
+
readonly role?: StrMatch;
|
|
49
|
+
readonly targetEntity?: EntityMatch;
|
|
50
|
+
readonly targetDelta?: string;
|
|
51
|
+
readonly context?: StrMatch;
|
|
52
|
+
readonly targetIsPrimitive?: boolean;
|
|
53
|
+
readonly targetValue?: ValMatch;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type Pred =
|
|
57
|
+
| { readonly kind: "true" }
|
|
58
|
+
| { readonly kind: "false" }
|
|
59
|
+
| {
|
|
60
|
+
readonly kind: "match";
|
|
61
|
+
readonly field: "author" | "timestamp" | "id";
|
|
62
|
+
readonly cmp: Cmp;
|
|
63
|
+
readonly constant: Primitive | Hole | readonly Primitive[];
|
|
64
|
+
}
|
|
65
|
+
| { readonly kind: "hasPointer"; readonly ppred: PPred }
|
|
66
|
+
| { readonly kind: "and"; readonly left: Pred; readonly right: Pred }
|
|
67
|
+
| { readonly kind: "or"; readonly left: Pred; readonly right: Pred }
|
|
68
|
+
| { readonly kind: "not"; readonly pred: Pred };
|
|
69
|
+
|
|
70
|
+
// --- the canonical total order over primitives (ERRATA-2 E3) ------------------------------------
|
|
71
|
+
|
|
72
|
+
const utf8 = new TextEncoder();
|
|
73
|
+
|
|
74
|
+
function utf8Compare(a: string, b: string): number {
|
|
75
|
+
const ab = utf8.encode(a);
|
|
76
|
+
const bb = utf8.encode(b);
|
|
77
|
+
const n = Math.min(ab.length, bb.length);
|
|
78
|
+
for (let i = 0; i < n; i++) {
|
|
79
|
+
const d = ab[i]! - bb[i]!;
|
|
80
|
+
if (d !== 0) return d;
|
|
81
|
+
}
|
|
82
|
+
return ab.length - bb.length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function typeRank(v: Primitive): number {
|
|
86
|
+
if (typeof v === "boolean") return 0;
|
|
87
|
+
if (typeof v === "number") return 1;
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Type rank first (bool < number < string), then value; strings by NFC UTF-8 bytes.
|
|
92
|
+
export function comparePrimitives(a: Primitive, b: Primitive): number {
|
|
93
|
+
const ra = typeRank(a);
|
|
94
|
+
const rb = typeRank(b);
|
|
95
|
+
if (ra !== rb) return ra - rb;
|
|
96
|
+
if (typeof a === "boolean") return (a ? 1 : 0) - ((b as boolean) ? 1 : 0);
|
|
97
|
+
if (typeof a === "number") {
|
|
98
|
+
const bn = b as number;
|
|
99
|
+
return a < bn ? -1 : a > bn ? 1 : 0;
|
|
100
|
+
}
|
|
101
|
+
return utf8Compare(a, b as string);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compareWith(
|
|
105
|
+
cmp: Cmp,
|
|
106
|
+
subject: Primitive,
|
|
107
|
+
constant: Primitive | readonly Primitive[],
|
|
108
|
+
): boolean {
|
|
109
|
+
if (cmp === "inSet") {
|
|
110
|
+
const values = constant as readonly Primitive[];
|
|
111
|
+
return values.some((v) => comparePrimitives(subject, v) === 0);
|
|
112
|
+
}
|
|
113
|
+
if (cmp === "prefix") {
|
|
114
|
+
return (
|
|
115
|
+
typeof subject === "string" && typeof constant === "string" && subject.startsWith(constant)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const c = comparePrimitives(subject, constant as Primitive);
|
|
119
|
+
switch (cmp) {
|
|
120
|
+
case "eq":
|
|
121
|
+
return c === 0;
|
|
122
|
+
case "neq":
|
|
123
|
+
return c !== 0;
|
|
124
|
+
case "lt":
|
|
125
|
+
return c < 0;
|
|
126
|
+
case "lte":
|
|
127
|
+
return c <= 0;
|
|
128
|
+
case "gt":
|
|
129
|
+
return c > 0;
|
|
130
|
+
case "gte":
|
|
131
|
+
return c >= 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- evaluation ----------------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export function strMatch(m: StrMatch, s: string): boolean {
|
|
138
|
+
switch (m.kind) {
|
|
139
|
+
case "exact":
|
|
140
|
+
return s === m.value;
|
|
141
|
+
case "prefix":
|
|
142
|
+
return s.startsWith(m.value);
|
|
143
|
+
case "inSet":
|
|
144
|
+
return m.values.includes(s);
|
|
145
|
+
case "aliased":
|
|
146
|
+
// Every consumer expands aliased against the ambient input first (SPEC-9 §4.1).
|
|
147
|
+
throw new Error(`aliased("${m.name}") must be expanded before matching (SPEC-9)`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function valMatch(m: ValMatch, v: Primitive, bindings: Bindings | undefined): boolean {
|
|
152
|
+
switch (m.kind) {
|
|
153
|
+
case "vcmp":
|
|
154
|
+
// cmp "inSet" is rejected at parse time (E1) — ValMatch has its own inSet arm.
|
|
155
|
+
return compareWith(m.cmp, v, resolveParam(m.value, bindings));
|
|
156
|
+
case "between":
|
|
157
|
+
return comparePrimitives(v, m.lo) >= 0 && comparePrimitives(v, m.hi) <= 0;
|
|
158
|
+
case "inSet":
|
|
159
|
+
return m.values.some((x) => comparePrimitives(v, x) === 0);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function entityWant(
|
|
164
|
+
m: EntityMatch,
|
|
165
|
+
root: string | undefined,
|
|
166
|
+
bindings: Bindings | undefined,
|
|
167
|
+
): string | undefined {
|
|
168
|
+
if (m.kind === "const") return m.id;
|
|
169
|
+
// The root variable matches nothing without an ambient root (E10).
|
|
170
|
+
if (m.kind === "root") return root;
|
|
171
|
+
const bound = resolveParam(m, bindings);
|
|
172
|
+
if (typeof bound !== "string") {
|
|
173
|
+
throw new Error(`hole "${m.name}" bound to a non-string where an entity id is required (E15)`);
|
|
174
|
+
}
|
|
175
|
+
return bound;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function pointerMatches(
|
|
179
|
+
p: PPred,
|
|
180
|
+
ptr: Pointer,
|
|
181
|
+
root: string | undefined,
|
|
182
|
+
bindings: Bindings | undefined,
|
|
183
|
+
): boolean {
|
|
184
|
+
if (p.role !== undefined && !strMatch(p.role, ptr.role)) return false;
|
|
185
|
+
if (p.targetEntity !== undefined) {
|
|
186
|
+
if (ptr.target.kind !== "entity") return false;
|
|
187
|
+
const want = entityWant(p.targetEntity, root, bindings);
|
|
188
|
+
if (want === undefined || ptr.target.entity.id !== want) return false;
|
|
189
|
+
}
|
|
190
|
+
if (p.targetDelta !== undefined) {
|
|
191
|
+
if (ptr.target.kind !== "delta" || ptr.target.deltaRef.delta !== p.targetDelta) return false;
|
|
192
|
+
}
|
|
193
|
+
if (p.context !== undefined) {
|
|
194
|
+
const ctx =
|
|
195
|
+
ptr.target.kind === "entity"
|
|
196
|
+
? ptr.target.entity.context
|
|
197
|
+
: ptr.target.kind === "delta"
|
|
198
|
+
? ptr.target.deltaRef.context
|
|
199
|
+
: undefined;
|
|
200
|
+
if (ctx === undefined || !strMatch(p.context, ctx)) return false;
|
|
201
|
+
}
|
|
202
|
+
if (p.targetIsPrimitive !== undefined) {
|
|
203
|
+
if ((ptr.target.kind === "primitive") !== p.targetIsPrimitive) return false;
|
|
204
|
+
}
|
|
205
|
+
if (p.targetValue !== undefined) {
|
|
206
|
+
if (ptr.target.kind !== "primitive" || !valMatch(p.targetValue, ptr.target.value, bindings)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Eagerly resolve every hole in a predicate against the ambient bindings (E15). Applied where
|
|
214
|
+
// a predicate meets data (select / mask-trust), so an unbound hole errors deterministically —
|
|
215
|
+
// regardless of how many deltas the operand happens to hold.
|
|
216
|
+
export function substituteHoles(pred: Pred, bindings: Bindings | undefined): Pred {
|
|
217
|
+
switch (pred.kind) {
|
|
218
|
+
case "true":
|
|
219
|
+
case "false":
|
|
220
|
+
return pred;
|
|
221
|
+
case "match": {
|
|
222
|
+
const c = pred.constant;
|
|
223
|
+
if (typeof c === "object" && !Array.isArray(c)) {
|
|
224
|
+
return { ...pred, constant: resolveParam(c as Hole, bindings) };
|
|
225
|
+
}
|
|
226
|
+
return pred;
|
|
227
|
+
}
|
|
228
|
+
case "hasPointer": {
|
|
229
|
+
const p = pred.ppred;
|
|
230
|
+
let te = p.targetEntity;
|
|
231
|
+
if (te !== undefined && te.kind === "hole") {
|
|
232
|
+
const bound = resolveParam(te, bindings);
|
|
233
|
+
if (typeof bound !== "string") {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`hole "${te.name}" bound to a non-string where an entity id is required (E15)`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
te = { kind: "const", id: bound };
|
|
239
|
+
}
|
|
240
|
+
let tv = p.targetValue;
|
|
241
|
+
if (tv !== undefined && tv.kind === "vcmp" && typeof tv.value === "object") {
|
|
242
|
+
tv = { ...tv, value: resolveParam(tv.value, bindings) };
|
|
243
|
+
}
|
|
244
|
+
if (te === p.targetEntity && tv === p.targetValue) return pred;
|
|
245
|
+
return {
|
|
246
|
+
kind: "hasPointer",
|
|
247
|
+
ppred: {
|
|
248
|
+
...p,
|
|
249
|
+
...(te === undefined ? {} : { targetEntity: te }),
|
|
250
|
+
...(tv === undefined ? {} : { targetValue: tv }),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
case "and":
|
|
255
|
+
return {
|
|
256
|
+
kind: "and",
|
|
257
|
+
left: substituteHoles(pred.left, bindings),
|
|
258
|
+
right: substituteHoles(pred.right, bindings),
|
|
259
|
+
};
|
|
260
|
+
case "or":
|
|
261
|
+
return {
|
|
262
|
+
kind: "or",
|
|
263
|
+
left: substituteHoles(pred.left, bindings),
|
|
264
|
+
right: substituteHoles(pred.right, bindings),
|
|
265
|
+
};
|
|
266
|
+
case "not":
|
|
267
|
+
return { kind: "not", pred: substituteHoles(pred.pred, bindings) };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Total and terminating: O(|delta|) per evaluation, no data dereference (SPEC-2 §3).
|
|
272
|
+
// `root` is the ambient root entity, consulted only by the root variable (E10). Holes are
|
|
273
|
+
// substituted away before predicates meet data; a stray hole here means "unbound" (E15).
|
|
274
|
+
export function evalPred(pred: Pred, delta: Delta, root?: string, bindings?: Bindings): boolean {
|
|
275
|
+
switch (pred.kind) {
|
|
276
|
+
case "true":
|
|
277
|
+
return true;
|
|
278
|
+
case "false":
|
|
279
|
+
return false;
|
|
280
|
+
case "match": {
|
|
281
|
+
const subject: Primitive =
|
|
282
|
+
pred.field === "author"
|
|
283
|
+
? delta.claims.author
|
|
284
|
+
: pred.field === "timestamp"
|
|
285
|
+
? delta.claims.timestamp
|
|
286
|
+
: delta.id;
|
|
287
|
+
const c = pred.constant;
|
|
288
|
+
const constant: Primitive | readonly Primitive[] =
|
|
289
|
+
typeof c === "object" && !Array.isArray(c)
|
|
290
|
+
? resolveParam(c as Hole, bindings)
|
|
291
|
+
: (c as Primitive | readonly Primitive[]);
|
|
292
|
+
return compareWith(pred.cmp, subject, constant);
|
|
293
|
+
}
|
|
294
|
+
case "hasPointer":
|
|
295
|
+
return delta.claims.pointers.some((ptr) => pointerMatches(pred.ppred, ptr, root, bindings));
|
|
296
|
+
case "and":
|
|
297
|
+
return (
|
|
298
|
+
evalPred(pred.left, delta, root, bindings) && evalPred(pred.right, delta, root, bindings)
|
|
299
|
+
);
|
|
300
|
+
case "or":
|
|
301
|
+
return (
|
|
302
|
+
evalPred(pred.left, delta, root, bindings) || evalPred(pred.right, delta, root, bindings)
|
|
303
|
+
);
|
|
304
|
+
case "not":
|
|
305
|
+
return !evalPred(pred.pred, delta, root, bindings);
|
|
306
|
+
}
|
|
307
|
+
}
|