@hraness/oh 0.2.3
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 +21 -0
- package/README.md +598 -0
- package/dist/canonical.d.ts +32 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3419 -0
- package/dist/contract.d.ts +33 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/graph.d.ts +67 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1988 -0
- package/dist/libsql.d.ts +57 -0
- package/dist/libsql.d.ts.map +1 -0
- package/dist/libsql.js +2662 -0
- package/dist/memory.d.ts +366 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +3650 -0
- package/dist/ontology.d.ts +242 -0
- package/dist/ontology.d.ts.map +1 -0
- package/dist/operation.d.ts +24 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/projection-public.d.ts +59 -0
- package/dist/projection-public.d.ts.map +1 -0
- package/dist/projection-public.js +1682 -0
- package/dist/projection-suss.d.ts +17 -0
- package/dist/projection-suss.d.ts.map +1 -0
- package/dist/projection-suss.js +1721 -0
- package/dist/projection.d.ts +315 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/schema.d.ts +45 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/sdk.d.ts +51 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +3072 -0
- package/dist/search.d.ts +34 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/semantic.d.ts +83 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +706 -0
- package/dist/sqlite/driver.d.ts +6 -0
- package/dist/sqlite/driver.d.ts.map +1 -0
- package/dist/sqlite/index.d.ts +5 -0
- package/dist/sqlite/index.d.ts.map +1 -0
- package/dist/sqlite/index.js +2840 -0
- package/dist/sqlite/migrations.d.ts +10 -0
- package/dist/sqlite/migrations.d.ts.map +1 -0
- package/dist/sqlite/port.d.ts +40 -0
- package/dist/sqlite/port.d.ts.map +1 -0
- package/dist/sqlite/runtime.d.ts +23 -0
- package/dist/sqlite/runtime.d.ts.map +1 -0
- package/dist/sqlite/store.d.ts +85 -0
- package/dist/sqlite/store.d.ts.map +1 -0
- package/dist/store.d.ts +236 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +996 -0
- package/dist/sync.d.ts +57 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +1328 -0
- package/package.json +121 -0
- package/skills/oh/SKILL.md +206 -0
- package/skills/oh/agents/openai.yaml +4 -0
- package/spec/README.md +74 -0
- package/spec/manifest.json +41 -0
- package/spec/v1/canonical-json.md +59 -0
- package/spec/v1/contract.json +28 -0
- package/spec/v1/contract.schema.json +58 -0
- package/spec/v1/embedding-profile.json +11 -0
- package/spec/v1/embedding.md +56 -0
- package/spec/v1/graph.md +87 -0
- package/spec/v1/memory.md +193 -0
- package/spec/v1/migration.md +92 -0
- package/spec/v1/ontology.json +55 -0
- package/spec/v1/ontology.md +80 -0
- package/spec/v1/operation.schema.json +138 -0
- package/spec/v1/projection-identity.schema.json +58 -0
- package/spec/v1/projection-query.schema.json +60 -0
- package/spec/v1/projection-result.schema.json +452 -0
- package/spec/v1/projection-rule-pack.schema.json +182 -0
- package/spec/v1/projection.md +165 -0
- package/spec/v1/record.schema.json +95 -0
- package/spec/v1/schema-evolution.md +51 -0
- package/spec/v1/schema-revision.schema.json +178 -0
- package/spec/v1/storage.md +88 -0
- package/spec/v1/store.md +131 -0
- package/spec/v1/sync-bundle.schema.json +51 -0
- package/spec/v1/sync.md +67 -0
- package/src/canonical.test.ts +46 -0
- package/src/canonical.ts +203 -0
- package/src/cli.test.ts +103 -0
- package/src/cli.ts +308 -0
- package/src/contract.ts +87 -0
- package/src/contracts.test.ts +147 -0
- package/src/graph.ts +248 -0
- package/src/index.ts +8 -0
- package/src/libsql.test.ts +657 -0
- package/src/libsql.ts +1687 -0
- package/src/memory.test.ts +783 -0
- package/src/memory.ts +1684 -0
- package/src/ontology.ts +573 -0
- package/src/operation.ts +80 -0
- package/src/projection-public.ts +53 -0
- package/src/projection-suss.ts +129 -0
- package/src/projection.test.ts +418 -0
- package/src/projection.ts +1457 -0
- package/src/schema.ts +156 -0
- package/src/sdk.ts +96 -0
- package/src/search.ts +66 -0
- package/src/semantic.test.ts +480 -0
- package/src/semantic.ts +333 -0
- package/src/sqlite/driver.ts +47 -0
- package/src/sqlite/index.ts +4 -0
- package/src/sqlite/migrations.test.ts +44 -0
- package/src/sqlite/migrations.ts +178 -0
- package/src/sqlite/port.test.ts +127 -0
- package/src/sqlite/port.ts +120 -0
- package/src/sqlite/runtime.test.ts +68 -0
- package/src/sqlite/runtime.ts +53 -0
- package/src/sqlite/store.test.ts +295 -0
- package/src/sqlite/store.ts +988 -0
- package/src/store.test.ts +121 -0
- package/src/store.ts +701 -0
- package/src/sync.test.ts +117 -0
- package/src/sync.ts +227 -0
|
@@ -0,0 +1,1721 @@
|
|
|
1
|
+
// src/projection-suss.ts
|
|
2
|
+
import {
|
|
3
|
+
Database,
|
|
4
|
+
constant,
|
|
5
|
+
evaluate,
|
|
6
|
+
lit,
|
|
7
|
+
rule,
|
|
8
|
+
variable
|
|
9
|
+
} from "@suss/datalog";
|
|
10
|
+
|
|
11
|
+
// src/canonical.ts
|
|
12
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
class OhValidationError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
path;
|
|
17
|
+
constructor(code, path, message) {
|
|
18
|
+
super(`${path}: ${message}`);
|
|
19
|
+
this.name = "OhValidationError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.path = path;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isPlainRecord(value) {
|
|
25
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
26
|
+
return false;
|
|
27
|
+
const prototype = Object.getPrototypeOf(value);
|
|
28
|
+
return prototype === Object.prototype || prototype === null;
|
|
29
|
+
}
|
|
30
|
+
function hasExactKeys(value, keys) {
|
|
31
|
+
const actual = Object.keys(value);
|
|
32
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
33
|
+
}
|
|
34
|
+
function assertUnicodeScalarString(value, path) {
|
|
35
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
36
|
+
const code = value.charCodeAt(index);
|
|
37
|
+
if (code >= 55296 && code <= 56319) {
|
|
38
|
+
const next = value.charCodeAt(index + 1);
|
|
39
|
+
if (!(next >= 56320 && next <= 57343)) {
|
|
40
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
41
|
+
}
|
|
42
|
+
index += 1;
|
|
43
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
44
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function encodeCanonical(value, path, ancestors) {
|
|
49
|
+
if (value === null || typeof value === "boolean")
|
|
50
|
+
return JSON.stringify(value);
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
assertUnicodeScalarString(value, path);
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "number") {
|
|
56
|
+
if (!Number.isFinite(value)) {
|
|
57
|
+
throw new OhValidationError("non-json-number", path, "must be finite");
|
|
58
|
+
}
|
|
59
|
+
if (Object.is(value, -0)) {
|
|
60
|
+
throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical");
|
|
61
|
+
}
|
|
62
|
+
return JSON.stringify(value);
|
|
63
|
+
}
|
|
64
|
+
if (typeof value !== "object" || value === null) {
|
|
65
|
+
throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`);
|
|
66
|
+
}
|
|
67
|
+
if (ancestors.has(value)) {
|
|
68
|
+
throw new OhValidationError("cycle", path, "contains a cycle");
|
|
69
|
+
}
|
|
70
|
+
ancestors.add(value);
|
|
71
|
+
try {
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
const encoded = [];
|
|
74
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
75
|
+
if (!Object.hasOwn(value, index)) {
|
|
76
|
+
throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
|
|
77
|
+
}
|
|
78
|
+
encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors));
|
|
79
|
+
}
|
|
80
|
+
const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length));
|
|
81
|
+
if (extraKeys.length > 0) {
|
|
82
|
+
throw new OhValidationError("non-json-property", path, "array has non-index properties");
|
|
83
|
+
}
|
|
84
|
+
return `[${encoded.join(",")}]`;
|
|
85
|
+
}
|
|
86
|
+
if (!isPlainRecord(value)) {
|
|
87
|
+
throw new OhValidationError("non-plain-object", path, "must be a plain object");
|
|
88
|
+
}
|
|
89
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
90
|
+
if (ownKeys.some((key) => typeof key !== "string")) {
|
|
91
|
+
throw new OhValidationError("non-json-property", path, "object has a symbol property");
|
|
92
|
+
}
|
|
93
|
+
const keys = ownKeys;
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
96
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
97
|
+
throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
keys.sort();
|
|
101
|
+
const entries = keys.map((key) => {
|
|
102
|
+
assertUnicodeScalarString(key, `${path}.<key>`);
|
|
103
|
+
return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`;
|
|
104
|
+
});
|
|
105
|
+
return `{${entries.join(",")}}`;
|
|
106
|
+
} finally {
|
|
107
|
+
ancestors.delete(value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function canonicalJson(value) {
|
|
111
|
+
return encodeCanonical(value, "$", new Set);
|
|
112
|
+
}
|
|
113
|
+
function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) {
|
|
114
|
+
if (utf8ByteLength(text) > maximumBytes) {
|
|
115
|
+
throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit");
|
|
116
|
+
}
|
|
117
|
+
let value;
|
|
118
|
+
try {
|
|
119
|
+
value = JSON.parse(text);
|
|
120
|
+
} catch {
|
|
121
|
+
throw new OhValidationError("invalid-json", "$", "is not valid JSON");
|
|
122
|
+
}
|
|
123
|
+
if (canonicalJson(value) !== text) {
|
|
124
|
+
throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical");
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
function utf8ByteLength(value) {
|
|
129
|
+
return Buffer.byteLength(value, "utf8");
|
|
130
|
+
}
|
|
131
|
+
function sha256Hex(value) {
|
|
132
|
+
return createHash("sha256").update(value).digest("hex");
|
|
133
|
+
}
|
|
134
|
+
function canonicalSha256(value) {
|
|
135
|
+
return sha256Hex(canonicalJson(value));
|
|
136
|
+
}
|
|
137
|
+
function parseSha256Hex(value) {
|
|
138
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
|
|
139
|
+
}
|
|
140
|
+
function parseCanonicalInstantV1(value) {
|
|
141
|
+
if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const timestamp = Date.parse(value);
|
|
145
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null;
|
|
146
|
+
}
|
|
147
|
+
function canonicalNow() {
|
|
148
|
+
return new Date().toISOString();
|
|
149
|
+
}
|
|
150
|
+
function safeCode(value, maximumLength = 128) {
|
|
151
|
+
return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
152
|
+
}
|
|
153
|
+
function orderedUnique(values, key) {
|
|
154
|
+
return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value));
|
|
155
|
+
}
|
|
156
|
+
function sortUnique(values, key) {
|
|
157
|
+
const sorted = [...values].sort((left, right) => {
|
|
158
|
+
const leftKey = key(left);
|
|
159
|
+
const rightKey = key(right);
|
|
160
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
161
|
+
});
|
|
162
|
+
if (!orderedUnique(sorted, key)) {
|
|
163
|
+
throw new OhValidationError("duplicate", "$", "contains duplicate canonical values");
|
|
164
|
+
}
|
|
165
|
+
return sorted;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/graph.ts
|
|
169
|
+
var OH_GRAPH_FORMAT_VERSION_V1 = 1;
|
|
170
|
+
var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
171
|
+
changesPerOperation: 8192,
|
|
172
|
+
dependenciesPerRecord: 4096,
|
|
173
|
+
recordBytes: 1024 * 1024,
|
|
174
|
+
recordsPerSnapshot: 65536
|
|
175
|
+
});
|
|
176
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
177
|
+
"activity",
|
|
178
|
+
"assertion",
|
|
179
|
+
"context",
|
|
180
|
+
"dependency-manifest",
|
|
181
|
+
"edition",
|
|
182
|
+
"entity",
|
|
183
|
+
"evidence",
|
|
184
|
+
"identity-operation",
|
|
185
|
+
"inquiry",
|
|
186
|
+
"inquiry-event",
|
|
187
|
+
"review-decision",
|
|
188
|
+
"rights-decision",
|
|
189
|
+
"schema",
|
|
190
|
+
"shape",
|
|
191
|
+
"statement",
|
|
192
|
+
"type-membership",
|
|
193
|
+
"view",
|
|
194
|
+
"vocabulary"
|
|
195
|
+
];
|
|
196
|
+
function recordKey(value) {
|
|
197
|
+
return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
198
|
+
}
|
|
199
|
+
function createKnowledgeGraphRecordV1(input) {
|
|
200
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies))
|
|
201
|
+
throw new TypeError("Invalid graph record input.");
|
|
202
|
+
const key = recordKey(input.key);
|
|
203
|
+
const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
|
|
204
|
+
if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
205
|
+
throw new TypeError("Invalid graph record identity.");
|
|
206
|
+
const dependencies = input.dependencies.map(recordKey);
|
|
207
|
+
if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
|
|
208
|
+
throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
|
|
209
|
+
}
|
|
210
|
+
const valueJson = canonicalJson(input.value);
|
|
211
|
+
if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) {
|
|
212
|
+
throw new RangeError("Graph record value exceeds its canonical byte limit.");
|
|
213
|
+
}
|
|
214
|
+
const payload = { dependencies, key, kind, v: 1, value: input.value };
|
|
215
|
+
return { ...payload, recordSha256: canonicalSha256(payload) };
|
|
216
|
+
}
|
|
217
|
+
function parseKnowledgeGraphRecordV1(value) {
|
|
218
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256"))
|
|
219
|
+
return null;
|
|
220
|
+
const recordSha256 = parseSha256Hex(value.recordSha256);
|
|
221
|
+
const { recordSha256: _digest, ...input } = value;
|
|
222
|
+
try {
|
|
223
|
+
const created = createKnowledgeGraphRecordV1(input);
|
|
224
|
+
return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function knowledgeGraphRecordRefV1(record) {
|
|
230
|
+
return {
|
|
231
|
+
dependencies: record.dependencies,
|
|
232
|
+
key: record.key,
|
|
233
|
+
kind: record.kind,
|
|
234
|
+
sha256: record.recordSha256,
|
|
235
|
+
v: 1
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function changeKey(change) {
|
|
239
|
+
return change.kind === "put" ? change.record.key : change.key;
|
|
240
|
+
}
|
|
241
|
+
function canonicalKnowledgeGraphChangesV1(changes) {
|
|
242
|
+
const normalized = [];
|
|
243
|
+
for (const change of changes) {
|
|
244
|
+
if (!isPlainRecord(change) || change.v !== 1)
|
|
245
|
+
throw new TypeError("Invalid graph change.");
|
|
246
|
+
if (change.kind === "put") {
|
|
247
|
+
const record = parseKnowledgeGraphRecordV1(change.record);
|
|
248
|
+
if (record === null)
|
|
249
|
+
throw new TypeError("Invalid graph record in change.");
|
|
250
|
+
normalized.push({ kind: "put", record, v: 1 });
|
|
251
|
+
} else if (change.kind === "tombstone") {
|
|
252
|
+
const key = recordKey(change.key);
|
|
253
|
+
const priorSha256 = parseSha256Hex(change.priorSha256);
|
|
254
|
+
if (key === null || priorSha256 === null)
|
|
255
|
+
throw new TypeError("Invalid graph tombstone.");
|
|
256
|
+
normalized.push({ key, kind: "tombstone", priorSha256, v: 1 });
|
|
257
|
+
} else
|
|
258
|
+
throw new TypeError("Unknown graph change kind.");
|
|
259
|
+
}
|
|
260
|
+
return sortUnique(normalized, changeKey);
|
|
261
|
+
}
|
|
262
|
+
function graphRevisionSha256V1(input) {
|
|
263
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
264
|
+
const operationId = safeCode(input.operationId);
|
|
265
|
+
const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256);
|
|
266
|
+
const recordsSha256 = parseSha256Hex(input.recordsSha256);
|
|
267
|
+
const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null;
|
|
268
|
+
if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) {
|
|
269
|
+
throw new TypeError("Invalid graph revision digest input.");
|
|
270
|
+
}
|
|
271
|
+
return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/ontology.ts
|
|
275
|
+
var OH_ONTOLOGY_VERSION_V1 = "1.0.0";
|
|
276
|
+
var OH_CONTRACT_ID_V1 = "oh.ontology.v1";
|
|
277
|
+
var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({
|
|
278
|
+
dimensions: 64,
|
|
279
|
+
listValues: 256,
|
|
280
|
+
qualifiers: 128,
|
|
281
|
+
statementBytes: 256 * 1024,
|
|
282
|
+
textBytes: 64 * 1024
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// src/schema.ts
|
|
286
|
+
var OH_SCHEMA_FORMAT_VERSION_V1 = 1;
|
|
287
|
+
|
|
288
|
+
// src/contract.ts
|
|
289
|
+
var manifestPayload = Object.freeze({
|
|
290
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
291
|
+
graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1,
|
|
292
|
+
ontologyVersion: OH_ONTOLOGY_VERSION_V1,
|
|
293
|
+
recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1,
|
|
294
|
+
schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1,
|
|
295
|
+
v: 1
|
|
296
|
+
});
|
|
297
|
+
var OH_CONTRACT_MANIFEST_V1 = Object.freeze({
|
|
298
|
+
...manifestPayload,
|
|
299
|
+
contractSha256: canonicalSha256(manifestPayload)
|
|
300
|
+
});
|
|
301
|
+
class OhRecordCodecRegistry {
|
|
302
|
+
#codecs = new Map;
|
|
303
|
+
#sealed = false;
|
|
304
|
+
register(codec) {
|
|
305
|
+
if (this.#sealed)
|
|
306
|
+
throw new TypeError("The codec registry is sealed.");
|
|
307
|
+
if (this.#codecs.has(codec.kind))
|
|
308
|
+
throw new TypeError(`A codec is already registered for ${codec.kind}.`);
|
|
309
|
+
this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse }));
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
parse(kind, value) {
|
|
313
|
+
const codec = this.#codecs.get(kind);
|
|
314
|
+
if (codec !== undefined)
|
|
315
|
+
return codec.parse(value);
|
|
316
|
+
try {
|
|
317
|
+
canonicalJson(value);
|
|
318
|
+
return value;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
has(kind) {
|
|
324
|
+
return this.#codecs.has(kind);
|
|
325
|
+
}
|
|
326
|
+
parseRequired(kind, value) {
|
|
327
|
+
const codec = this.#codecs.get(kind);
|
|
328
|
+
if (codec === undefined)
|
|
329
|
+
return null;
|
|
330
|
+
try {
|
|
331
|
+
const parsed = codec.parse(value);
|
|
332
|
+
if (parsed === null)
|
|
333
|
+
return null;
|
|
334
|
+
canonicalJson(parsed);
|
|
335
|
+
return parsed;
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
seal() {
|
|
341
|
+
this.#sealed = true;
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
get sealed() {
|
|
345
|
+
return this.#sealed;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/projection.ts
|
|
350
|
+
var OH_PROJECTION_FORMAT_VERSION_V1 = 1;
|
|
351
|
+
var OH_PROJECTION_SEMANTICS_V1 = "oh.projection.positive-datalog.v1";
|
|
352
|
+
var OH_PROJECTION_INTERNAL_ENGINE_V1 = "oh.naive.positive.v1";
|
|
353
|
+
var OH_PROJECTION_LIMITS_V1 = Object.freeze({
|
|
354
|
+
arity: 32,
|
|
355
|
+
atomBytes: 16 * 1024,
|
|
356
|
+
derivedTuples: 262144,
|
|
357
|
+
facts: 262144,
|
|
358
|
+
literalsPerRule: 64,
|
|
359
|
+
proofDepth: 128,
|
|
360
|
+
proofNodes: 4096,
|
|
361
|
+
queryLiterals: 64,
|
|
362
|
+
queryMatches: 262144,
|
|
363
|
+
queryResults: 65536,
|
|
364
|
+
relations: 4096,
|
|
365
|
+
resultBytes: 16 * 1024 * 1024,
|
|
366
|
+
rounds: 1024,
|
|
367
|
+
rules: 1024,
|
|
368
|
+
sourcesPerFact: 64,
|
|
369
|
+
totalProofNodes: 65536,
|
|
370
|
+
variables: 256,
|
|
371
|
+
workUnits: 16777216
|
|
372
|
+
});
|
|
373
|
+
var recordFactExtractorPayloadV1 = {
|
|
374
|
+
factPackId: "oh.record-facts",
|
|
375
|
+
factPackRevision: 1,
|
|
376
|
+
relations: ["oh.dependency", "oh.record"],
|
|
377
|
+
semantics: OH_PROJECTION_SEMANTICS_V1,
|
|
378
|
+
v: 1
|
|
379
|
+
};
|
|
380
|
+
var OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Object.freeze({
|
|
381
|
+
...recordFactExtractorPayloadV1,
|
|
382
|
+
extractorSha256: canonicalSha256(recordFactExtractorPayloadV1)
|
|
383
|
+
});
|
|
384
|
+
function nonnegativeInteger(value) {
|
|
385
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
386
|
+
}
|
|
387
|
+
function positiveInteger(value, maximum = Number.MAX_SAFE_INTEGER) {
|
|
388
|
+
return Number.isSafeInteger(value) && value >= 1 && value <= maximum ? value : null;
|
|
389
|
+
}
|
|
390
|
+
function projectionName(value, maximumLength = 128) {
|
|
391
|
+
return safeCode(value, maximumLength);
|
|
392
|
+
}
|
|
393
|
+
function compareCanonical(left, right) {
|
|
394
|
+
const leftKey = canonicalJson(left);
|
|
395
|
+
const rightKey = canonicalJson(right);
|
|
396
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
397
|
+
}
|
|
398
|
+
function compareProjectionFacts(left, right) {
|
|
399
|
+
return compareCanonical([left.relation, left.tuple], [right.relation, right.tuple]);
|
|
400
|
+
}
|
|
401
|
+
var INVALID_PROJECTION_ATOM = Symbol("invalid-projection-atom");
|
|
402
|
+
function atom(value) {
|
|
403
|
+
if (value !== null && typeof value !== "boolean" && typeof value !== "number" && typeof value !== "string") {
|
|
404
|
+
return INVALID_PROJECTION_ATOM;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const encoded = canonicalJson(value);
|
|
408
|
+
return utf8ByteLength(encoded) <= OH_PROJECTION_LIMITS_V1.atomBytes ? value : INVALID_PROJECTION_ATOM;
|
|
409
|
+
} catch {
|
|
410
|
+
return INVALID_PROJECTION_ATOM;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function tuple(value) {
|
|
414
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > OH_PROJECTION_LIMITS_V1.arity)
|
|
415
|
+
return null;
|
|
416
|
+
const parsed = value.map(atom);
|
|
417
|
+
return parsed.some((item) => item === INVALID_PROJECTION_ATOM) ? null : parsed;
|
|
418
|
+
}
|
|
419
|
+
function parseRecordRef(value) {
|
|
420
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["dependencies", "key", "kind", "sha256", "v"]) || value.v !== 1 || !Array.isArray(value.dependencies))
|
|
421
|
+
return null;
|
|
422
|
+
const key = safeCode(value.key, 512);
|
|
423
|
+
const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === value.kind);
|
|
424
|
+
const sha256 = parseSha256Hex(value.sha256);
|
|
425
|
+
const dependencies = value.dependencies.map((dependency) => safeCode(dependency, 512));
|
|
426
|
+
if (key === null || kind === undefined || sha256 === null || dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord || dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key))
|
|
427
|
+
return null;
|
|
428
|
+
return { dependencies, key, kind, sha256, v: 1 };
|
|
429
|
+
}
|
|
430
|
+
function createOhProjectionSnapshotV1(input) {
|
|
431
|
+
const spaceId = projectionName(input.spaceId);
|
|
432
|
+
const generation = nonnegativeInteger(input.head.generation);
|
|
433
|
+
const sequence = nonnegativeInteger(input.head.sequence);
|
|
434
|
+
const operationSha256 = input.head.operationSha256 === null ? null : parseSha256Hex(input.head.operationSha256);
|
|
435
|
+
const graphRevisionSha256 = input.head.graphRevisionSha256 === null ? null : parseSha256Hex(input.head.graphRevisionSha256);
|
|
436
|
+
const declaredRecordsSha256 = parseSha256Hex(input.head.recordsSha256);
|
|
437
|
+
if (spaceId === null || generation === null || sequence === null || generation !== sequence || input.head.operationSha256 !== null && operationSha256 === null || input.head.graphRevisionSha256 !== null && graphRevisionSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || declaredRecordsSha256 === null || input.records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
438
|
+
throw new TypeError("Invalid projection snapshot head.");
|
|
439
|
+
}
|
|
440
|
+
const records = input.records.map(parseKnowledgeGraphRecordV1);
|
|
441
|
+
if (records.some((record) => record === null))
|
|
442
|
+
throw new TypeError("Invalid record in projection snapshot.");
|
|
443
|
+
const recordRefs = sortUnique(records.map(knowledgeGraphRecordRefV1), (reference) => reference.key);
|
|
444
|
+
const recordsSha256 = canonicalSha256(recordRefs);
|
|
445
|
+
if (recordsSha256 !== declaredRecordsSha256) {
|
|
446
|
+
throw new TypeError("Projection snapshot records do not reproduce the declared head.");
|
|
447
|
+
}
|
|
448
|
+
const keys = new Set(recordRefs.map((reference) => reference.key));
|
|
449
|
+
if (recordRefs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) {
|
|
450
|
+
throw new TypeError("Projection snapshot has a missing record dependency.");
|
|
451
|
+
}
|
|
452
|
+
const payload = {
|
|
453
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
454
|
+
generation,
|
|
455
|
+
graphRevisionSha256,
|
|
456
|
+
operationSha256,
|
|
457
|
+
recordRefs,
|
|
458
|
+
recordsSha256,
|
|
459
|
+
sequence,
|
|
460
|
+
spaceId,
|
|
461
|
+
v: 1
|
|
462
|
+
};
|
|
463
|
+
return { ...payload, snapshotSha256: canonicalSha256(payload) };
|
|
464
|
+
}
|
|
465
|
+
function parseOhProjectionSnapshotV1(value) {
|
|
466
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
467
|
+
"contractSha256",
|
|
468
|
+
"generation",
|
|
469
|
+
"graphRevisionSha256",
|
|
470
|
+
"operationSha256",
|
|
471
|
+
"recordRefs",
|
|
472
|
+
"recordsSha256",
|
|
473
|
+
"sequence",
|
|
474
|
+
"snapshotSha256",
|
|
475
|
+
"spaceId",
|
|
476
|
+
"v"
|
|
477
|
+
]) || value.v !== 1 || !Array.isArray(value.recordRefs) || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot)
|
|
478
|
+
return null;
|
|
479
|
+
const contractSha256 = parseSha256Hex(value.contractSha256);
|
|
480
|
+
const generation = nonnegativeInteger(value.generation);
|
|
481
|
+
const sequence = nonnegativeInteger(value.sequence);
|
|
482
|
+
const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256);
|
|
483
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
484
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
485
|
+
const snapshotSha256 = parseSha256Hex(value.snapshotSha256);
|
|
486
|
+
const spaceId = projectionName(value.spaceId);
|
|
487
|
+
const recordRefs = value.recordRefs.map(parseRecordRef);
|
|
488
|
+
if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || generation === null || sequence === null || generation !== sequence || spaceId === null || recordsSha256 === null || snapshotSha256 === null || value.graphRevisionSha256 !== null && graphRevisionSha256 === null || value.operationSha256 !== null && operationSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || recordRefs.some((reference) => reference === null))
|
|
489
|
+
return null;
|
|
490
|
+
const refs = recordRefs;
|
|
491
|
+
if (!orderedUnique(refs, (reference) => reference.key) || canonicalSha256(refs) !== recordsSha256)
|
|
492
|
+
return null;
|
|
493
|
+
const keys = new Set(refs.map((reference) => reference.key));
|
|
494
|
+
if (refs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency))))
|
|
495
|
+
return null;
|
|
496
|
+
const payload = {
|
|
497
|
+
contractSha256,
|
|
498
|
+
generation,
|
|
499
|
+
graphRevisionSha256,
|
|
500
|
+
operationSha256,
|
|
501
|
+
recordRefs: refs,
|
|
502
|
+
recordsSha256,
|
|
503
|
+
sequence,
|
|
504
|
+
spaceId,
|
|
505
|
+
v: 1
|
|
506
|
+
};
|
|
507
|
+
return canonicalSha256(payload) === snapshotSha256 ? { ...payload, snapshotSha256 } : null;
|
|
508
|
+
}
|
|
509
|
+
function createOhProjectionFactV1(input) {
|
|
510
|
+
const relation = projectionName(input.relation);
|
|
511
|
+
const parsedTuple = tuple(input.tuple);
|
|
512
|
+
if (relation === null || parsedTuple === null || input.sources.length < 1 || input.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) {
|
|
513
|
+
throw new TypeError("Invalid projection fact.");
|
|
514
|
+
}
|
|
515
|
+
const sources = input.sources.map((source) => {
|
|
516
|
+
if (!isPlainRecord(source) || !hasExactKeys(source, ["key", "recordSha256", "v"]) || source.v !== 1) {
|
|
517
|
+
throw new TypeError("Invalid projection fact source.");
|
|
518
|
+
}
|
|
519
|
+
const key = safeCode(source.key, 512);
|
|
520
|
+
const recordSha256 = parseSha256Hex(source.recordSha256);
|
|
521
|
+
if (key === null || recordSha256 === null)
|
|
522
|
+
throw new TypeError("Invalid projection fact source.");
|
|
523
|
+
return { key, recordSha256, v: 1 };
|
|
524
|
+
}).sort(compareCanonical);
|
|
525
|
+
if (!orderedUnique(sources, (source) => source.key)) {
|
|
526
|
+
throw new TypeError("Projection fact sources must have unique record keys.");
|
|
527
|
+
}
|
|
528
|
+
const payload = { relation, sources, tuple: parsedTuple, v: 1 };
|
|
529
|
+
return { ...payload, factSha256: canonicalSha256(payload) };
|
|
530
|
+
}
|
|
531
|
+
function parseOhProjectionFactV1(value) {
|
|
532
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["factSha256", "relation", "sources", "tuple", "v"]) || value.v !== 1 || !Array.isArray(value.sources) || !Array.isArray(value.tuple))
|
|
533
|
+
return null;
|
|
534
|
+
const factSha256 = parseSha256Hex(value.factSha256);
|
|
535
|
+
try {
|
|
536
|
+
const fact = createOhProjectionFactV1({
|
|
537
|
+
relation: value.relation,
|
|
538
|
+
sources: value.sources,
|
|
539
|
+
tuple: value.tuple
|
|
540
|
+
});
|
|
541
|
+
return factSha256 !== null && fact.factSha256 === factSha256 ? fact : null;
|
|
542
|
+
} catch {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function mergeProjectionFacts(facts) {
|
|
547
|
+
const grouped = new Map;
|
|
548
|
+
for (const fact of facts) {
|
|
549
|
+
const identity = canonicalJson([fact.relation, fact.tuple]);
|
|
550
|
+
let group = grouped.get(identity);
|
|
551
|
+
if (group === undefined) {
|
|
552
|
+
group = { relation: fact.relation, sources: new Map, tuple: fact.tuple };
|
|
553
|
+
grouped.set(identity, group);
|
|
554
|
+
}
|
|
555
|
+
for (const source of fact.sources) {
|
|
556
|
+
const existing = group.sources.get(source.key);
|
|
557
|
+
if (existing !== undefined && existing.recordSha256 !== source.recordSha256) {
|
|
558
|
+
throw new TypeError("One fact source key is bound to multiple record digests.");
|
|
559
|
+
}
|
|
560
|
+
group.sources.set(source.key, source);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return [...grouped.values()].map((group) => createOhProjectionFactV1({
|
|
564
|
+
relation: group.relation,
|
|
565
|
+
sources: [...group.sources.values()],
|
|
566
|
+
tuple: group.tuple
|
|
567
|
+
})).sort(compareProjectionFacts);
|
|
568
|
+
}
|
|
569
|
+
function createOhProjectionDatasetV1(input) {
|
|
570
|
+
const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
|
|
571
|
+
const extractorSha256 = parseSha256Hex(input.extractorSha256);
|
|
572
|
+
const factPackId = projectionName(input.factPackId);
|
|
573
|
+
const factPackRevision = positiveInteger(input.factPackRevision);
|
|
574
|
+
if (snapshot === null || extractorSha256 === null || factPackId === null || factPackRevision === null || input.facts.length > OH_PROJECTION_LIMITS_V1.facts)
|
|
575
|
+
throw new TypeError("Invalid projection dataset.");
|
|
576
|
+
const parsedFacts = input.facts.map(parseOhProjectionFactV1);
|
|
577
|
+
if (parsedFacts.some((fact) => fact === null))
|
|
578
|
+
throw new TypeError("Invalid fact in projection dataset.");
|
|
579
|
+
const facts = mergeProjectionFacts(parsedFacts);
|
|
580
|
+
if (facts.length > OH_PROJECTION_LIMITS_V1.facts)
|
|
581
|
+
throw new RangeError("Projection dataset has too many facts.");
|
|
582
|
+
const refs = new Map(snapshot.recordRefs.map((reference) => [reference.key, reference.sha256]));
|
|
583
|
+
for (const fact of facts) {
|
|
584
|
+
for (const source of fact.sources) {
|
|
585
|
+
if (refs.get(source.key) !== source.recordSha256) {
|
|
586
|
+
throw new TypeError("Projection fact source is not present at the exact input snapshot.");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const factPackPayload = {
|
|
591
|
+
extractorSha256,
|
|
592
|
+
factPackId,
|
|
593
|
+
factPackRevision,
|
|
594
|
+
semantics: OH_PROJECTION_SEMANTICS_V1,
|
|
595
|
+
v: 1
|
|
596
|
+
};
|
|
597
|
+
const factPackSha256 = canonicalSha256(factPackPayload);
|
|
598
|
+
const factsSha256 = canonicalSha256(facts);
|
|
599
|
+
const payload = {
|
|
600
|
+
extractorSha256,
|
|
601
|
+
factPackId,
|
|
602
|
+
factPackRevision,
|
|
603
|
+
factPackSha256,
|
|
604
|
+
facts,
|
|
605
|
+
factsSha256,
|
|
606
|
+
snapshotSha256: snapshot.snapshotSha256,
|
|
607
|
+
v: 1
|
|
608
|
+
};
|
|
609
|
+
return { ...payload, datasetSha256: canonicalSha256(payload) };
|
|
610
|
+
}
|
|
611
|
+
function parseOhProjectionDatasetV1(value, snapshot) {
|
|
612
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
613
|
+
"datasetSha256",
|
|
614
|
+
"extractorSha256",
|
|
615
|
+
"factPackId",
|
|
616
|
+
"factPackRevision",
|
|
617
|
+
"factPackSha256",
|
|
618
|
+
"facts",
|
|
619
|
+
"factsSha256",
|
|
620
|
+
"snapshotSha256",
|
|
621
|
+
"v"
|
|
622
|
+
]) || value.v !== 1 || !Array.isArray(value.facts))
|
|
623
|
+
return null;
|
|
624
|
+
const datasetSha256 = parseSha256Hex(value.datasetSha256);
|
|
625
|
+
const declaredFactPackSha256 = parseSha256Hex(value.factPackSha256);
|
|
626
|
+
const declaredFactsSha256 = parseSha256Hex(value.factsSha256);
|
|
627
|
+
try {
|
|
628
|
+
const dataset = createOhProjectionDatasetV1({
|
|
629
|
+
extractorSha256: value.extractorSha256,
|
|
630
|
+
factPackId: value.factPackId,
|
|
631
|
+
factPackRevision: value.factPackRevision,
|
|
632
|
+
facts: value.facts,
|
|
633
|
+
snapshot
|
|
634
|
+
});
|
|
635
|
+
return datasetSha256 !== null && declaredFactPackSha256 === dataset.factPackSha256 && declaredFactsSha256 === dataset.factsSha256 && value.snapshotSha256 === dataset.snapshotSha256 && dataset.datasetSha256 === datasetSha256 ? dataset : null;
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function ohProjectionVariableV1(name) {
|
|
641
|
+
const parsed = projectionName(name);
|
|
642
|
+
if (parsed === null)
|
|
643
|
+
throw new TypeError("Invalid projection variable name.");
|
|
644
|
+
return { kind: "variable", name: parsed, v: 1 };
|
|
645
|
+
}
|
|
646
|
+
function ohProjectionConstantV1(value) {
|
|
647
|
+
const parsed = atom(value);
|
|
648
|
+
if (parsed === INVALID_PROJECTION_ATOM)
|
|
649
|
+
throw new TypeError("Invalid projection constant.");
|
|
650
|
+
return { kind: "constant", v: 1, value: parsed };
|
|
651
|
+
}
|
|
652
|
+
function createOhProjectionLiteralV1(input) {
|
|
653
|
+
const relation = projectionName(input.relation);
|
|
654
|
+
if (relation === null || input.terms.length < 1 || input.terms.length > OH_PROJECTION_LIMITS_V1.arity) {
|
|
655
|
+
throw new TypeError("Invalid projection literal.");
|
|
656
|
+
}
|
|
657
|
+
const terms = input.terms.map((term) => parseOhProjectionTermV1(term));
|
|
658
|
+
if (terms.some((term) => term === null))
|
|
659
|
+
throw new TypeError("Invalid term in projection literal.");
|
|
660
|
+
return { relation, terms, v: 1 };
|
|
661
|
+
}
|
|
662
|
+
function parseOhProjectionTermV1(value) {
|
|
663
|
+
if (!isPlainRecord(value) || value.v !== 1)
|
|
664
|
+
return null;
|
|
665
|
+
if (value.kind === "variable" && hasExactKeys(value, ["kind", "name", "v"])) {
|
|
666
|
+
const name = projectionName(value.name);
|
|
667
|
+
return name === null ? null : { kind: "variable", name, v: 1 };
|
|
668
|
+
}
|
|
669
|
+
if (value.kind === "constant" && hasExactKeys(value, ["kind", "v", "value"])) {
|
|
670
|
+
const parsed = atom(value.value);
|
|
671
|
+
return parsed === INVALID_PROJECTION_ATOM ? null : { kind: "constant", v: 1, value: parsed };
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
function parseOhProjectionLiteralV1(value) {
|
|
676
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["relation", "terms", "v"]) || value.v !== 1 || !Array.isArray(value.terms))
|
|
677
|
+
return null;
|
|
678
|
+
try {
|
|
679
|
+
return createOhProjectionLiteralV1({
|
|
680
|
+
relation: value.relation,
|
|
681
|
+
terms: value.terms
|
|
682
|
+
});
|
|
683
|
+
} catch {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function literalVariables(literal) {
|
|
688
|
+
return literal.terms.flatMap((term) => term.kind === "variable" ? [term.name] : []);
|
|
689
|
+
}
|
|
690
|
+
function createOhProjectionRuleV1(input) {
|
|
691
|
+
const ruleId = projectionName(input.ruleId);
|
|
692
|
+
const head = parseOhProjectionLiteralV1(input.head);
|
|
693
|
+
if (ruleId === null || head === null || input.body.length < 1 || input.body.length > OH_PROJECTION_LIMITS_V1.literalsPerRule)
|
|
694
|
+
throw new TypeError("Invalid projection rule.");
|
|
695
|
+
const body = input.body.map(parseOhProjectionLiteralV1);
|
|
696
|
+
if (body.some((literal) => literal === null))
|
|
697
|
+
throw new TypeError("Invalid body literal in projection rule.");
|
|
698
|
+
const bound = new Set(body.flatMap(literalVariables));
|
|
699
|
+
if (literalVariables(head).some((variable) => !bound.has(variable)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) {
|
|
700
|
+
throw new TypeError("Every projection rule head variable must be bound in its body.");
|
|
701
|
+
}
|
|
702
|
+
const payload = { body, head, ruleId, v: 1 };
|
|
703
|
+
return { ...payload, ruleSha256: canonicalSha256(payload) };
|
|
704
|
+
}
|
|
705
|
+
function parseOhProjectionRuleV1(value) {
|
|
706
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["body", "head", "ruleId", "ruleSha256", "v"]) || value.v !== 1 || !Array.isArray(value.body))
|
|
707
|
+
return null;
|
|
708
|
+
const ruleSha256 = parseSha256Hex(value.ruleSha256);
|
|
709
|
+
try {
|
|
710
|
+
const rule = createOhProjectionRuleV1({
|
|
711
|
+
body: value.body,
|
|
712
|
+
head: value.head,
|
|
713
|
+
ruleId: value.ruleId
|
|
714
|
+
});
|
|
715
|
+
return ruleSha256 !== null && rule.ruleSha256 === ruleSha256 ? rule : null;
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function createOhProjectionRulePackV1(input) {
|
|
721
|
+
const rulePackId = projectionName(input.rulePackId);
|
|
722
|
+
const rulePackRevision = positiveInteger(input.rulePackRevision);
|
|
723
|
+
if (rulePackId === null || rulePackRevision === null || input.rules.length < 1 || input.rules.length > OH_PROJECTION_LIMITS_V1.rules)
|
|
724
|
+
throw new TypeError("Invalid projection rule pack.");
|
|
725
|
+
const parsedRules = input.rules.map(parseOhProjectionRuleV1);
|
|
726
|
+
if (parsedRules.some((rule) => rule === null))
|
|
727
|
+
throw new TypeError("Invalid rule in projection rule pack.");
|
|
728
|
+
const rules = sortUnique(parsedRules, (rule) => rule.ruleId);
|
|
729
|
+
const rulesSha256 = canonicalSha256(rules);
|
|
730
|
+
const payload = {
|
|
731
|
+
rulePackId,
|
|
732
|
+
rulePackRevision,
|
|
733
|
+
rules,
|
|
734
|
+
rulesSha256,
|
|
735
|
+
semantics: OH_PROJECTION_SEMANTICS_V1,
|
|
736
|
+
v: 1
|
|
737
|
+
};
|
|
738
|
+
return { ...payload, rulePackSha256: canonicalSha256(payload) };
|
|
739
|
+
}
|
|
740
|
+
function parseOhProjectionRulePackV1(value) {
|
|
741
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
742
|
+
"rulePackId",
|
|
743
|
+
"rulePackRevision",
|
|
744
|
+
"rulePackSha256",
|
|
745
|
+
"rules",
|
|
746
|
+
"rulesSha256",
|
|
747
|
+
"semantics",
|
|
748
|
+
"v"
|
|
749
|
+
]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1 || !Array.isArray(value.rules))
|
|
750
|
+
return null;
|
|
751
|
+
const rulePackSha256 = parseSha256Hex(value.rulePackSha256);
|
|
752
|
+
const rulesSha256 = parseSha256Hex(value.rulesSha256);
|
|
753
|
+
try {
|
|
754
|
+
const pack = createOhProjectionRulePackV1({
|
|
755
|
+
rulePackId: value.rulePackId,
|
|
756
|
+
rulePackRevision: value.rulePackRevision,
|
|
757
|
+
rules: value.rules
|
|
758
|
+
});
|
|
759
|
+
return rulePackSha256 === pack.rulePackSha256 && rulesSha256 === pack.rulesSha256 ? pack : null;
|
|
760
|
+
} catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function createOhProjectionQueryV1(input) {
|
|
765
|
+
const queryId = projectionName(input.queryId);
|
|
766
|
+
const limit = positiveInteger(input.limit ?? 1000, OH_PROJECTION_LIMITS_V1.queryResults);
|
|
767
|
+
if (queryId === null || limit === null || input.find.length < 1 || input.find.length > OH_PROJECTION_LIMITS_V1.arity || input.where.length < 1 || input.where.length > OH_PROJECTION_LIMITS_V1.queryLiterals)
|
|
768
|
+
throw new TypeError("Invalid projection query.");
|
|
769
|
+
const find = input.find.map((name) => projectionName(name));
|
|
770
|
+
const where = input.where.map(parseOhProjectionLiteralV1);
|
|
771
|
+
if (find.some((name) => name === null) || !orderedUnique([...find].sort(), String) || where.some((literal) => literal === null))
|
|
772
|
+
throw new TypeError("Invalid projection query variables or literals.");
|
|
773
|
+
const bound = new Set(where.flatMap(literalVariables));
|
|
774
|
+
if (find.some((name) => !bound.has(name)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) {
|
|
775
|
+
throw new TypeError("Every projected query variable must be bound in the query body.");
|
|
776
|
+
}
|
|
777
|
+
const payload = {
|
|
778
|
+
find,
|
|
779
|
+
limit,
|
|
780
|
+
queryId,
|
|
781
|
+
where,
|
|
782
|
+
v: 1
|
|
783
|
+
};
|
|
784
|
+
return { ...payload, querySha256: canonicalSha256(payload) };
|
|
785
|
+
}
|
|
786
|
+
function parseOhProjectionQueryV1(value) {
|
|
787
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["find", "limit", "queryId", "querySha256", "where", "v"]) || value.v !== 1 || !Array.isArray(value.find) || !Array.isArray(value.where))
|
|
788
|
+
return null;
|
|
789
|
+
const querySha256 = parseSha256Hex(value.querySha256);
|
|
790
|
+
try {
|
|
791
|
+
const query = createOhProjectionQueryV1({
|
|
792
|
+
find: value.find,
|
|
793
|
+
limit: value.limit,
|
|
794
|
+
queryId: value.queryId,
|
|
795
|
+
where: value.where
|
|
796
|
+
});
|
|
797
|
+
return querySha256 !== null && query.querySha256 === querySha256 ? query : null;
|
|
798
|
+
} catch {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function createOhProjectionIdentityV1(input) {
|
|
803
|
+
const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
|
|
804
|
+
const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot);
|
|
805
|
+
const query = parseOhProjectionQueryV1(input.query);
|
|
806
|
+
const rulePack = parseOhProjectionRulePackV1(input.rulePack);
|
|
807
|
+
if (snapshot === null || dataset === null || query === null || rulePack === null) {
|
|
808
|
+
throw new TypeError("Invalid projection identity input.");
|
|
809
|
+
}
|
|
810
|
+
const engine = safeCode(input.engine ?? OH_PROJECTION_INTERNAL_ENGINE_V1, 256);
|
|
811
|
+
if (engine === null)
|
|
812
|
+
throw new TypeError("Invalid projection engine identity.");
|
|
813
|
+
const evaluation = { ...resolveEvaluationOptions(input.options ?? {}), v: 1 };
|
|
814
|
+
const payload = {
|
|
815
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
816
|
+
datasetSha256: dataset.datasetSha256,
|
|
817
|
+
engineSha256: canonicalSha256({ engine, v: 1 }),
|
|
818
|
+
evaluationSha256: canonicalSha256(evaluation),
|
|
819
|
+
querySha256: query.querySha256,
|
|
820
|
+
rulePackSha256: rulePack.rulePackSha256,
|
|
821
|
+
semantics: OH_PROJECTION_SEMANTICS_V1,
|
|
822
|
+
snapshotSha256: snapshot.snapshotSha256,
|
|
823
|
+
v: 1
|
|
824
|
+
};
|
|
825
|
+
return { ...payload, projectionSha256: canonicalSha256(payload) };
|
|
826
|
+
}
|
|
827
|
+
function parseOhProjectionIdentityV1(value) {
|
|
828
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
829
|
+
"contractSha256",
|
|
830
|
+
"datasetSha256",
|
|
831
|
+
"engineSha256",
|
|
832
|
+
"evaluationSha256",
|
|
833
|
+
"projectionSha256",
|
|
834
|
+
"querySha256",
|
|
835
|
+
"rulePackSha256",
|
|
836
|
+
"semantics",
|
|
837
|
+
"snapshotSha256",
|
|
838
|
+
"v"
|
|
839
|
+
]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1)
|
|
840
|
+
return null;
|
|
841
|
+
const contractSha256 = parseSha256Hex(value.contractSha256);
|
|
842
|
+
const datasetSha256 = parseSha256Hex(value.datasetSha256);
|
|
843
|
+
const engineSha256 = parseSha256Hex(value.engineSha256);
|
|
844
|
+
const evaluationSha256 = parseSha256Hex(value.evaluationSha256);
|
|
845
|
+
const projectionSha256 = parseSha256Hex(value.projectionSha256);
|
|
846
|
+
const querySha256 = parseSha256Hex(value.querySha256);
|
|
847
|
+
const rulePackSha256 = parseSha256Hex(value.rulePackSha256);
|
|
848
|
+
const snapshotSha256 = parseSha256Hex(value.snapshotSha256);
|
|
849
|
+
if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || datasetSha256 === null || engineSha256 === null || evaluationSha256 === null || projectionSha256 === null || querySha256 === null || rulePackSha256 === null || snapshotSha256 === null)
|
|
850
|
+
return null;
|
|
851
|
+
const payload = {
|
|
852
|
+
contractSha256,
|
|
853
|
+
datasetSha256,
|
|
854
|
+
engineSha256,
|
|
855
|
+
evaluationSha256,
|
|
856
|
+
querySha256,
|
|
857
|
+
rulePackSha256,
|
|
858
|
+
semantics: OH_PROJECTION_SEMANTICS_V1,
|
|
859
|
+
snapshotSha256,
|
|
860
|
+
v: 1
|
|
861
|
+
};
|
|
862
|
+
return canonicalSha256(payload) === projectionSha256 ? { ...payload, projectionSha256 } : null;
|
|
863
|
+
}
|
|
864
|
+
function invalidationForOhProjectionV1(previous, next) {
|
|
865
|
+
const parsedPrevious = parseOhProjectionIdentityV1(previous);
|
|
866
|
+
const parsedNext = parseOhProjectionIdentityV1(next);
|
|
867
|
+
if (parsedPrevious === null || parsedNext === null)
|
|
868
|
+
throw new TypeError("Invalid projection identity.");
|
|
869
|
+
if (parsedPrevious.projectionSha256 === parsedNext.projectionSha256)
|
|
870
|
+
return { kind: "reusable", v: 1 };
|
|
871
|
+
const reasons = [];
|
|
872
|
+
if (parsedPrevious.snapshotSha256 !== parsedNext.snapshotSha256)
|
|
873
|
+
reasons.push("snapshot-changed");
|
|
874
|
+
if (parsedPrevious.datasetSha256 !== parsedNext.datasetSha256)
|
|
875
|
+
reasons.push("dataset-changed");
|
|
876
|
+
if (parsedPrevious.engineSha256 !== parsedNext.engineSha256)
|
|
877
|
+
reasons.push("engine-changed");
|
|
878
|
+
if (parsedPrevious.evaluationSha256 !== parsedNext.evaluationSha256)
|
|
879
|
+
reasons.push("evaluation-changed");
|
|
880
|
+
if (parsedPrevious.rulePackSha256 !== parsedNext.rulePackSha256)
|
|
881
|
+
reasons.push("rule-pack-changed");
|
|
882
|
+
if (parsedPrevious.querySha256 !== parsedNext.querySha256)
|
|
883
|
+
reasons.push("query-changed");
|
|
884
|
+
return { kind: "full-rebuild", reasons, v: 1 };
|
|
885
|
+
}
|
|
886
|
+
function tupleKey(value) {
|
|
887
|
+
return canonicalJson(value);
|
|
888
|
+
}
|
|
889
|
+
function referenceKey(reference) {
|
|
890
|
+
return canonicalJson([reference.relation, reference.tuple]);
|
|
891
|
+
}
|
|
892
|
+
function relationTuples(relations, relation) {
|
|
893
|
+
return [...relations.get(relation)?.values() ?? []].sort((left, right) => compareCanonical(left.tuple, right.tuple));
|
|
894
|
+
}
|
|
895
|
+
function setArity(arities, relation, arity) {
|
|
896
|
+
const existing = arities.get(relation);
|
|
897
|
+
if (existing !== undefined && existing !== arity) {
|
|
898
|
+
throw new TypeError(`Projection relation ${relation} is used with conflicting arities.`);
|
|
899
|
+
}
|
|
900
|
+
arities.set(relation, arity);
|
|
901
|
+
if (arities.size > OH_PROJECTION_LIMITS_V1.relations)
|
|
902
|
+
throw new RangeError("Projection uses too many relations.");
|
|
903
|
+
}
|
|
904
|
+
function validateProgramArities(dataset, rulePack, query) {
|
|
905
|
+
const arities = new Map;
|
|
906
|
+
for (const fact of dataset.facts)
|
|
907
|
+
setArity(arities, fact.relation, fact.tuple.length);
|
|
908
|
+
for (const rule of rulePack.rules) {
|
|
909
|
+
setArity(arities, rule.head.relation, rule.head.terms.length);
|
|
910
|
+
for (const literal of rule.body)
|
|
911
|
+
setArity(arities, literal.relation, literal.terms.length);
|
|
912
|
+
}
|
|
913
|
+
for (const literal of query.where)
|
|
914
|
+
setArity(arities, literal.relation, literal.terms.length);
|
|
915
|
+
}
|
|
916
|
+
function sameAtom(left, right) {
|
|
917
|
+
return left === right;
|
|
918
|
+
}
|
|
919
|
+
function unifyLiteral(literal, state, binding) {
|
|
920
|
+
const next = new Map(binding);
|
|
921
|
+
for (let index = 0;index < literal.terms.length; index += 1) {
|
|
922
|
+
const term = literal.terms[index];
|
|
923
|
+
const value = state.tuple[index];
|
|
924
|
+
if (term.kind === "constant") {
|
|
925
|
+
if (!sameAtom(term.value, value))
|
|
926
|
+
return null;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (next.has(term.name)) {
|
|
930
|
+
if (!sameAtom(next.get(term.name), value))
|
|
931
|
+
return null;
|
|
932
|
+
} else
|
|
933
|
+
next.set(term.name, value);
|
|
934
|
+
}
|
|
935
|
+
return next;
|
|
936
|
+
}
|
|
937
|
+
function consumeWorkUnit(budget) {
|
|
938
|
+
if (budget.units >= budget.maximum)
|
|
939
|
+
throw new RangeError("Projection exceeds its work-unit bound.");
|
|
940
|
+
budget.units += 1;
|
|
941
|
+
}
|
|
942
|
+
function matchBody(relations, body, maximumMatches, work) {
|
|
943
|
+
let matches = [{ binding: new Map, premises: [] }];
|
|
944
|
+
for (const literal of body) {
|
|
945
|
+
const next = [];
|
|
946
|
+
const candidates = relationTuples(relations, literal.relation);
|
|
947
|
+
for (const match of matches) {
|
|
948
|
+
for (const candidate of candidates) {
|
|
949
|
+
consumeWorkUnit(work);
|
|
950
|
+
const binding = unifyLiteral(literal, candidate, match.binding);
|
|
951
|
+
if (binding === null)
|
|
952
|
+
continue;
|
|
953
|
+
next.push({ binding, premises: [...match.premises, {
|
|
954
|
+
relation: literal.relation,
|
|
955
|
+
tuple: candidate.tuple
|
|
956
|
+
}] });
|
|
957
|
+
if (next.length > maximumMatches)
|
|
958
|
+
throw new RangeError("Projection join exceeds its match bound.");
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
matches = next;
|
|
962
|
+
if (matches.length === 0)
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
return matches;
|
|
966
|
+
}
|
|
967
|
+
function instantiateHead(head, binding) {
|
|
968
|
+
return head.terms.map((term) => term.kind === "constant" ? term.value : binding.get(term.name));
|
|
969
|
+
}
|
|
970
|
+
function canonicalWitness(witness) {
|
|
971
|
+
if (witness.kind === "fact")
|
|
972
|
+
return canonicalJson(witness);
|
|
973
|
+
return canonicalJson({ kind: witness.kind, premises: witness.premises, ruleSha256: witness.rule.ruleSha256 });
|
|
974
|
+
}
|
|
975
|
+
function materializeNaive(input) {
|
|
976
|
+
const relations = new Map;
|
|
977
|
+
for (const fact of input.dataset.facts) {
|
|
978
|
+
let relation = relations.get(fact.relation);
|
|
979
|
+
if (relation === undefined) {
|
|
980
|
+
relation = new Map;
|
|
981
|
+
relations.set(fact.relation, relation);
|
|
982
|
+
}
|
|
983
|
+
relation.set(tupleKey(fact.tuple), { tuple: fact.tuple, witness: { kind: "fact", sources: fact.sources } });
|
|
984
|
+
}
|
|
985
|
+
let derivedFacts = 0;
|
|
986
|
+
let rounds = 0;
|
|
987
|
+
while (true) {
|
|
988
|
+
const candidates = new Map;
|
|
989
|
+
for (const rule of input.rulePack.rules) {
|
|
990
|
+
for (const match of matchBody(relations, rule.body, OH_PROJECTION_LIMITS_V1.queryMatches, input.work)) {
|
|
991
|
+
const derivedTuple = instantiateHead(rule.head, match.binding);
|
|
992
|
+
const relation = relations.get(rule.head.relation);
|
|
993
|
+
const key = tupleKey(derivedTuple);
|
|
994
|
+
if (relation?.has(key) === true)
|
|
995
|
+
continue;
|
|
996
|
+
const state = {
|
|
997
|
+
tuple: derivedTuple,
|
|
998
|
+
witness: { kind: "derived", premises: match.premises, rule }
|
|
999
|
+
};
|
|
1000
|
+
const identity = referenceKey({ relation: rule.head.relation, tuple: derivedTuple });
|
|
1001
|
+
const existing = candidates.get(identity);
|
|
1002
|
+
if (existing === undefined || canonicalWitness(state.witness) < canonicalWitness(existing.state.witness)) {
|
|
1003
|
+
candidates.set(identity, { relation: rule.head.relation, state });
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (candidates.size === 0)
|
|
1008
|
+
break;
|
|
1009
|
+
if (rounds >= input.maximumRounds)
|
|
1010
|
+
throw new RangeError("Projection exceeds its evaluation round bound.");
|
|
1011
|
+
if (derivedFacts + candidates.size > input.maximumDerivedTuples) {
|
|
1012
|
+
throw new RangeError("Projection exceeds its derived tuple bound.");
|
|
1013
|
+
}
|
|
1014
|
+
const ordered = [...candidates.values()].sort((left, right) => compareCanonical([left.relation, left.state.tuple], [right.relation, right.state.tuple]));
|
|
1015
|
+
for (const candidate of ordered) {
|
|
1016
|
+
let relation = relations.get(candidate.relation);
|
|
1017
|
+
if (relation === undefined) {
|
|
1018
|
+
relation = new Map;
|
|
1019
|
+
relations.set(candidate.relation, relation);
|
|
1020
|
+
}
|
|
1021
|
+
relation.set(tupleKey(candidate.state.tuple), candidate.state);
|
|
1022
|
+
}
|
|
1023
|
+
derivedFacts += candidates.size;
|
|
1024
|
+
rounds += 1;
|
|
1025
|
+
}
|
|
1026
|
+
return { baseFacts: input.dataset.facts.length, derivedFacts, relations, rounds };
|
|
1027
|
+
}
|
|
1028
|
+
function boundedOption(value, fallback, maximum, label) {
|
|
1029
|
+
const parsed = positiveInteger(value ?? fallback, maximum);
|
|
1030
|
+
if (parsed === null)
|
|
1031
|
+
throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`);
|
|
1032
|
+
return parsed;
|
|
1033
|
+
}
|
|
1034
|
+
function resolveEvaluationOptions(options) {
|
|
1035
|
+
const resolved = {
|
|
1036
|
+
maximumDerivedTuples: boundedOption(options.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"),
|
|
1037
|
+
maximumProofDepth: boundedOption(options.maximumProofDepth, 32, OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"),
|
|
1038
|
+
maximumProofNodes: boundedOption(options.maximumProofNodes, 1024, OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"),
|
|
1039
|
+
maximumResultBytes: boundedOption(options.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes"),
|
|
1040
|
+
maximumRounds: boundedOption(options.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds, OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"),
|
|
1041
|
+
maximumTotalProofNodes: boundedOption(options.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, "maximumTotalProofNodes"),
|
|
1042
|
+
maximumWorkUnits: boundedOption(options.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits, OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits")
|
|
1043
|
+
};
|
|
1044
|
+
if (resolved.maximumResultBytes < 64 * 1024) {
|
|
1045
|
+
throw new RangeError("maximumResultBytes must be at least 65536.");
|
|
1046
|
+
}
|
|
1047
|
+
return resolved;
|
|
1048
|
+
}
|
|
1049
|
+
function reserveResultBytes(budget, value) {
|
|
1050
|
+
const bytes = utf8ByteLength(canonicalJson(value));
|
|
1051
|
+
if (budget.bytes + bytes > budget.maximumBytes)
|
|
1052
|
+
return false;
|
|
1053
|
+
budget.bytes += bytes;
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
function reserveProofNode(budget, options, envelope) {
|
|
1057
|
+
if (budget.nodes >= options.maximumProofNodes || budget.result.nodes >= options.maximumTotalProofNodes || !reserveResultBytes(budget.result, envelope))
|
|
1058
|
+
return false;
|
|
1059
|
+
budget.nodes += 1;
|
|
1060
|
+
budget.result.nodes += 1;
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
function proofForReference(relations, reference, budget, options, depth, visiting) {
|
|
1064
|
+
if (depth >= options.maximumProofDepth) {
|
|
1065
|
+
const proof = {
|
|
1066
|
+
kind: "truncated",
|
|
1067
|
+
reason: "depth",
|
|
1068
|
+
relation: reference.relation,
|
|
1069
|
+
tuple: reference.tuple,
|
|
1070
|
+
v: 1
|
|
1071
|
+
};
|
|
1072
|
+
return reserveProofNode(budget, options, proof) ? proof : null;
|
|
1073
|
+
}
|
|
1074
|
+
const identity = referenceKey(reference);
|
|
1075
|
+
if (visiting.has(identity)) {
|
|
1076
|
+
const proof = {
|
|
1077
|
+
kind: "truncated",
|
|
1078
|
+
reason: "cycle",
|
|
1079
|
+
relation: reference.relation,
|
|
1080
|
+
tuple: reference.tuple,
|
|
1081
|
+
v: 1
|
|
1082
|
+
};
|
|
1083
|
+
return reserveProofNode(budget, options, proof) ? proof : null;
|
|
1084
|
+
}
|
|
1085
|
+
const state = relations.get(reference.relation)?.get(tupleKey(reference.tuple));
|
|
1086
|
+
if (state === undefined)
|
|
1087
|
+
throw new Error("Projection proof references a tuple outside the materialized result.");
|
|
1088
|
+
if (state.witness.kind === "fact") {
|
|
1089
|
+
const proof = {
|
|
1090
|
+
kind: "fact",
|
|
1091
|
+
relation: reference.relation,
|
|
1092
|
+
sources: state.witness.sources,
|
|
1093
|
+
tuple: reference.tuple,
|
|
1094
|
+
v: 1
|
|
1095
|
+
};
|
|
1096
|
+
return reserveProofNode(budget, options, proof) ? proof : null;
|
|
1097
|
+
}
|
|
1098
|
+
const envelope = {
|
|
1099
|
+
kind: "derived",
|
|
1100
|
+
premises: [],
|
|
1101
|
+
premisesTruncated: false,
|
|
1102
|
+
relation: reference.relation,
|
|
1103
|
+
ruleId: state.witness.rule.ruleId,
|
|
1104
|
+
ruleSha256: state.witness.rule.ruleSha256,
|
|
1105
|
+
tuple: reference.tuple,
|
|
1106
|
+
v: 1
|
|
1107
|
+
};
|
|
1108
|
+
if (!reserveProofNode(budget, options, envelope))
|
|
1109
|
+
return null;
|
|
1110
|
+
visiting.add(identity);
|
|
1111
|
+
try {
|
|
1112
|
+
const premises = [];
|
|
1113
|
+
let premisesTruncated = false;
|
|
1114
|
+
for (const premise of state.witness.premises) {
|
|
1115
|
+
const proof = proofForReference(relations, premise, budget, options, depth + 1, visiting);
|
|
1116
|
+
if (proof === null) {
|
|
1117
|
+
premisesTruncated = true;
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
premises.push(proof);
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
kind: "derived",
|
|
1124
|
+
premises,
|
|
1125
|
+
premisesTruncated,
|
|
1126
|
+
relation: reference.relation,
|
|
1127
|
+
ruleId: state.witness.rule.ruleId,
|
|
1128
|
+
ruleSha256: state.witness.rule.ruleSha256,
|
|
1129
|
+
tuple: reference.tuple,
|
|
1130
|
+
v: 1
|
|
1131
|
+
};
|
|
1132
|
+
} finally {
|
|
1133
|
+
visiting.delete(identity);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function proofIsTruncated(proof) {
|
|
1137
|
+
return proof.kind === "truncated" || proof.kind === "derived" && (proof.premisesTruncated || proof.premises.some(proofIsTruncated));
|
|
1138
|
+
}
|
|
1139
|
+
function reserveProjectionParseBytes(budget, value) {
|
|
1140
|
+
const bytes = utf8ByteLength(canonicalJson(value));
|
|
1141
|
+
if (budget.bytes + bytes > budget.maximumBytes)
|
|
1142
|
+
return false;
|
|
1143
|
+
budget.bytes += bytes;
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
function parseProjectionFactSource(value) {
|
|
1147
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["key", "recordSha256", "v"]) || value.v !== 1) {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
const key = safeCode(value.key, 512);
|
|
1151
|
+
const recordSha256 = parseSha256Hex(value.recordSha256);
|
|
1152
|
+
return key === null || recordSha256 === null ? null : { key, recordSha256, v: 1 };
|
|
1153
|
+
}
|
|
1154
|
+
function parseProjectionProofWithBudget(value, budget, depth) {
|
|
1155
|
+
if (depth > budget.maximumDepth || budget.nodes >= budget.maximumNodes || !isPlainRecord(value) || value.v !== 1)
|
|
1156
|
+
return null;
|
|
1157
|
+
const relation = projectionName(value.relation);
|
|
1158
|
+
const parsedTuple = tuple(value.tuple);
|
|
1159
|
+
if (relation === null || parsedTuple === null)
|
|
1160
|
+
return null;
|
|
1161
|
+
if (value.kind === "fact") {
|
|
1162
|
+
if (!hasExactKeys(value, ["kind", "relation", "sources", "tuple", "v"]) || !Array.isArray(value.sources) || value.sources.length < 1 || value.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact)
|
|
1163
|
+
return null;
|
|
1164
|
+
const sources = value.sources.map(parseProjectionFactSource);
|
|
1165
|
+
if (sources.some((source) => source === null))
|
|
1166
|
+
return null;
|
|
1167
|
+
const parsedSources = sources;
|
|
1168
|
+
if (!orderedUnique(parsedSources, (source) => source.key))
|
|
1169
|
+
return null;
|
|
1170
|
+
const proof = {
|
|
1171
|
+
kind: "fact",
|
|
1172
|
+
relation,
|
|
1173
|
+
sources: parsedSources,
|
|
1174
|
+
tuple: parsedTuple,
|
|
1175
|
+
v: 1
|
|
1176
|
+
};
|
|
1177
|
+
if (!reserveProjectionParseBytes(budget, proof))
|
|
1178
|
+
return null;
|
|
1179
|
+
budget.nodes += 1;
|
|
1180
|
+
return proof;
|
|
1181
|
+
}
|
|
1182
|
+
if (value.kind === "truncated") {
|
|
1183
|
+
if (!hasExactKeys(value, ["kind", "reason", "relation", "tuple", "v"]) || value.reason !== "cycle" && value.reason !== "depth" && value.reason !== "nodes")
|
|
1184
|
+
return null;
|
|
1185
|
+
const reason = value.reason;
|
|
1186
|
+
const proof = {
|
|
1187
|
+
kind: "truncated",
|
|
1188
|
+
reason,
|
|
1189
|
+
relation,
|
|
1190
|
+
tuple: parsedTuple,
|
|
1191
|
+
v: 1
|
|
1192
|
+
};
|
|
1193
|
+
if (!reserveProjectionParseBytes(budget, proof))
|
|
1194
|
+
return null;
|
|
1195
|
+
budget.nodes += 1;
|
|
1196
|
+
return proof;
|
|
1197
|
+
}
|
|
1198
|
+
if (value.kind !== "derived" || !hasExactKeys(value, [
|
|
1199
|
+
"kind",
|
|
1200
|
+
"premises",
|
|
1201
|
+
"premisesTruncated",
|
|
1202
|
+
"relation",
|
|
1203
|
+
"ruleId",
|
|
1204
|
+
"ruleSha256",
|
|
1205
|
+
"tuple",
|
|
1206
|
+
"v"
|
|
1207
|
+
]) || !Array.isArray(value.premises) || value.premises.length > OH_PROJECTION_LIMITS_V1.literalsPerRule || typeof value.premisesTruncated !== "boolean")
|
|
1208
|
+
return null;
|
|
1209
|
+
const ruleId = projectionName(value.ruleId);
|
|
1210
|
+
const ruleSha256 = parseSha256Hex(value.ruleSha256);
|
|
1211
|
+
if (ruleId === null || ruleSha256 === null || !value.premisesTruncated && value.premises.length === 0 || value.premisesTruncated && value.premises.length === OH_PROJECTION_LIMITS_V1.literalsPerRule) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
const skeleton = {
|
|
1215
|
+
kind: "derived",
|
|
1216
|
+
premises: [],
|
|
1217
|
+
premisesTruncated: value.premisesTruncated,
|
|
1218
|
+
relation,
|
|
1219
|
+
ruleId,
|
|
1220
|
+
ruleSha256,
|
|
1221
|
+
tuple: parsedTuple,
|
|
1222
|
+
v: 1
|
|
1223
|
+
};
|
|
1224
|
+
if (!reserveProjectionParseBytes(budget, skeleton))
|
|
1225
|
+
return null;
|
|
1226
|
+
budget.nodes += 1;
|
|
1227
|
+
const premises = [];
|
|
1228
|
+
for (const premise of value.premises) {
|
|
1229
|
+
const parsed = parseProjectionProofWithBudget(premise, budget, depth + 1);
|
|
1230
|
+
if (parsed === null)
|
|
1231
|
+
return null;
|
|
1232
|
+
premises.push(parsed);
|
|
1233
|
+
}
|
|
1234
|
+
return { ...skeleton, premises };
|
|
1235
|
+
}
|
|
1236
|
+
function parseOhProjectionProofV1(value) {
|
|
1237
|
+
try {
|
|
1238
|
+
const budget = {
|
|
1239
|
+
bytes: 0,
|
|
1240
|
+
maximumBytes: OH_PROJECTION_LIMITS_V1.resultBytes,
|
|
1241
|
+
maximumDepth: OH_PROJECTION_LIMITS_V1.proofDepth,
|
|
1242
|
+
maximumNodes: OH_PROJECTION_LIMITS_V1.proofNodes,
|
|
1243
|
+
nodes: 0
|
|
1244
|
+
};
|
|
1245
|
+
const proof = parseProjectionProofWithBudget(value, budget, 0);
|
|
1246
|
+
return proof !== null && utf8ByteLength(canonicalJson(proof)) <= budget.maximumBytes ? proof : null;
|
|
1247
|
+
} catch {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function parseProjectionEvaluation(value) {
|
|
1252
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1253
|
+
"maximumDerivedTuples",
|
|
1254
|
+
"maximumProofDepth",
|
|
1255
|
+
"maximumProofNodes",
|
|
1256
|
+
"maximumResultBytes",
|
|
1257
|
+
"maximumRounds",
|
|
1258
|
+
"maximumTotalProofNodes",
|
|
1259
|
+
"maximumWorkUnits",
|
|
1260
|
+
"v"
|
|
1261
|
+
]) || value.v !== 1)
|
|
1262
|
+
return null;
|
|
1263
|
+
const maximumDerivedTuples = positiveInteger(value.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples);
|
|
1264
|
+
const maximumProofDepth = positiveInteger(value.maximumProofDepth, OH_PROJECTION_LIMITS_V1.proofDepth);
|
|
1265
|
+
const maximumProofNodes = positiveInteger(value.maximumProofNodes, OH_PROJECTION_LIMITS_V1.proofNodes);
|
|
1266
|
+
const maximumResultBytes = positiveInteger(value.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes);
|
|
1267
|
+
const maximumRounds = positiveInteger(value.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds);
|
|
1268
|
+
const maximumTotalProofNodes = positiveInteger(value.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes);
|
|
1269
|
+
const maximumWorkUnits = positiveInteger(value.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits);
|
|
1270
|
+
if (maximumDerivedTuples === null || maximumProofDepth === null || maximumProofNodes === null || maximumResultBytes === null || maximumResultBytes < 64 * 1024 || maximumRounds === null || maximumTotalProofNodes === null || maximumWorkUnits === null)
|
|
1271
|
+
return null;
|
|
1272
|
+
return {
|
|
1273
|
+
maximumDerivedTuples,
|
|
1274
|
+
maximumProofDepth,
|
|
1275
|
+
maximumProofNodes,
|
|
1276
|
+
maximumResultBytes,
|
|
1277
|
+
maximumRounds,
|
|
1278
|
+
maximumTotalProofNodes,
|
|
1279
|
+
maximumWorkUnits,
|
|
1280
|
+
v: 1
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function parseProjectionResultRow(value, evaluation, resultBudget) {
|
|
1284
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["proofs", "proofsTruncated", "supportCount", "values", "v"]) || value.v !== 1 || !Array.isArray(value.proofs) || value.proofs.length > OH_PROJECTION_LIMITS_V1.queryLiterals || typeof value.proofsTruncated !== "boolean")
|
|
1285
|
+
return null;
|
|
1286
|
+
const values = tuple(value.values);
|
|
1287
|
+
const supportCount = positiveInteger(value.supportCount, OH_PROJECTION_LIMITS_V1.queryMatches);
|
|
1288
|
+
if (values === null || supportCount === null || !value.proofsTruncated && value.proofs.length === 0)
|
|
1289
|
+
return null;
|
|
1290
|
+
if (!reserveProjectionParseBytes(resultBudget, {
|
|
1291
|
+
proofs: [],
|
|
1292
|
+
proofsTruncated: value.proofsTruncated,
|
|
1293
|
+
supportCount,
|
|
1294
|
+
values,
|
|
1295
|
+
v: 1
|
|
1296
|
+
}))
|
|
1297
|
+
return null;
|
|
1298
|
+
const before = resultBudget.nodes;
|
|
1299
|
+
resultBudget.maximumNodes = Math.min(resultBudget.maximumNodes, before + evaluation.maximumProofNodes);
|
|
1300
|
+
const proofs = [];
|
|
1301
|
+
for (const proof of value.proofs) {
|
|
1302
|
+
const parsed = parseProjectionProofWithBudget(proof, resultBudget, 0);
|
|
1303
|
+
if (parsed === null)
|
|
1304
|
+
return null;
|
|
1305
|
+
proofs.push(parsed);
|
|
1306
|
+
}
|
|
1307
|
+
resultBudget.maximumNodes = evaluation.maximumTotalProofNodes;
|
|
1308
|
+
const containsTruncation = proofs.some(proofIsTruncated);
|
|
1309
|
+
if (!value.proofsTruncated && containsTruncation || value.proofsTruncated && proofs.length === OH_PROJECTION_LIMITS_V1.queryLiterals && !containsTruncation)
|
|
1310
|
+
return null;
|
|
1311
|
+
return {
|
|
1312
|
+
nodes: resultBudget.nodes - before,
|
|
1313
|
+
row: { proofs, proofsTruncated: value.proofsTruncated, supportCount, values, v: 1 }
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function parseOhProjectionResultV1(value, expectedProjectionSha256) {
|
|
1317
|
+
try {
|
|
1318
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1319
|
+
"authority",
|
|
1320
|
+
"cache",
|
|
1321
|
+
"engine",
|
|
1322
|
+
"evaluation",
|
|
1323
|
+
"identity",
|
|
1324
|
+
"resultSha256",
|
|
1325
|
+
"rows",
|
|
1326
|
+
"stats",
|
|
1327
|
+
"v"
|
|
1328
|
+
]) || value.v !== 1 || value.authority !== "derived" || !isPlainRecord(value.cache) || !hasExactKeys(value.cache, ["strategy", "v"]) || value.cache.strategy !== "full-rebuild" || value.cache.v !== 1 || !Array.isArray(value.rows) || value.rows.length > OH_PROJECTION_LIMITS_V1.queryResults || !isPlainRecord(value.stats) || !hasExactKeys(value.stats, [
|
|
1329
|
+
"baseFacts",
|
|
1330
|
+
"derivedFacts",
|
|
1331
|
+
"proofNodes",
|
|
1332
|
+
"proofsTruncated",
|
|
1333
|
+
"queryMatches",
|
|
1334
|
+
"relations",
|
|
1335
|
+
"rounds",
|
|
1336
|
+
"truncated",
|
|
1337
|
+
"truncationReasons",
|
|
1338
|
+
"v",
|
|
1339
|
+
"workUnits"
|
|
1340
|
+
]) || value.stats.v !== 1 || !Array.isArray(value.stats.truncationReasons) || typeof value.stats.proofsTruncated !== "boolean" || typeof value.stats.truncated !== "boolean")
|
|
1341
|
+
return null;
|
|
1342
|
+
const engine = safeCode(value.engine, 256);
|
|
1343
|
+
const evaluation = parseProjectionEvaluation(value.evaluation);
|
|
1344
|
+
const identity = parseOhProjectionIdentityV1(value.identity);
|
|
1345
|
+
const resultSha256 = parseSha256Hex(value.resultSha256);
|
|
1346
|
+
const expected = expectedProjectionSha256 === undefined ? undefined : parseSha256Hex(expectedProjectionSha256);
|
|
1347
|
+
if (engine === null || evaluation === null || identity === null || resultSha256 === null || expectedProjectionSha256 !== undefined && expected === null || expected !== undefined && identity.projectionSha256 !== expected || identity.engineSha256 !== canonicalSha256({ engine, v: 1 }) || identity.evaluationSha256 !== canonicalSha256(evaluation))
|
|
1348
|
+
return null;
|
|
1349
|
+
const baseFacts = nonnegativeInteger(value.stats.baseFacts);
|
|
1350
|
+
const derivedFacts = nonnegativeInteger(value.stats.derivedFacts);
|
|
1351
|
+
const proofNodes = nonnegativeInteger(value.stats.proofNodes);
|
|
1352
|
+
const queryMatches = nonnegativeInteger(value.stats.queryMatches);
|
|
1353
|
+
const relations = nonnegativeInteger(value.stats.relations);
|
|
1354
|
+
const rounds = nonnegativeInteger(value.stats.rounds);
|
|
1355
|
+
const workUnits = nonnegativeInteger(value.stats.workUnits);
|
|
1356
|
+
if (baseFacts === null || baseFacts > OH_PROJECTION_LIMITS_V1.facts || derivedFacts === null || derivedFacts > evaluation.maximumDerivedTuples || proofNodes === null || proofNodes > evaluation.maximumTotalProofNodes || queryMatches === null || queryMatches > OH_PROJECTION_LIMITS_V1.queryMatches || relations === null || relations > OH_PROJECTION_LIMITS_V1.relations || rounds === null || rounds > evaluation.maximumRounds || workUnits === null || workUnits > evaluation.maximumWorkUnits || relations > baseFacts + derivedFacts || rounds > derivedFacts || rounds === 0 !== (derivedFacts === 0) || queryMatches > workUnits)
|
|
1357
|
+
return null;
|
|
1358
|
+
const truncationReasons = value.stats.truncationReasons;
|
|
1359
|
+
if (truncationReasons.length > 2 || !orderedUnique(truncationReasons, (reason) => reason === "query-limit" ? "0" : reason === "result-bytes" ? "1" : "x") || truncationReasons.some((reason) => reason !== "query-limit" && reason !== "result-bytes") || value.stats.truncated !== truncationReasons.length > 0)
|
|
1360
|
+
return null;
|
|
1361
|
+
const budget = {
|
|
1362
|
+
bytes: 0,
|
|
1363
|
+
maximumBytes: evaluation.maximumResultBytes,
|
|
1364
|
+
maximumDepth: evaluation.maximumProofDepth,
|
|
1365
|
+
maximumNodes: evaluation.maximumTotalProofNodes,
|
|
1366
|
+
nodes: 0
|
|
1367
|
+
};
|
|
1368
|
+
const rows = [];
|
|
1369
|
+
let supportCount = 0;
|
|
1370
|
+
for (const row of value.rows) {
|
|
1371
|
+
const parsed = parseProjectionResultRow(row, evaluation, budget);
|
|
1372
|
+
if (parsed === null)
|
|
1373
|
+
return null;
|
|
1374
|
+
rows.push(parsed.row);
|
|
1375
|
+
supportCount += parsed.row.supportCount;
|
|
1376
|
+
if (supportCount > queryMatches)
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
if (!orderedUnique(rows, (row) => canonicalJson(row.values)) || budget.nodes !== proofNodes || value.stats.proofsTruncated !== rows.some((row) => row.proofsTruncated) || (value.stats.truncated ? supportCount >= queryMatches : supportCount !== queryMatches))
|
|
1380
|
+
return null;
|
|
1381
|
+
const reasons = truncationReasons;
|
|
1382
|
+
const payload = {
|
|
1383
|
+
authority: "derived",
|
|
1384
|
+
cache: { strategy: "full-rebuild", v: 1 },
|
|
1385
|
+
engine,
|
|
1386
|
+
evaluation,
|
|
1387
|
+
identity,
|
|
1388
|
+
rows,
|
|
1389
|
+
stats: {
|
|
1390
|
+
baseFacts,
|
|
1391
|
+
derivedFacts,
|
|
1392
|
+
proofNodes,
|
|
1393
|
+
proofsTruncated: value.stats.proofsTruncated,
|
|
1394
|
+
queryMatches,
|
|
1395
|
+
relations,
|
|
1396
|
+
rounds,
|
|
1397
|
+
truncated: value.stats.truncated,
|
|
1398
|
+
truncationReasons: reasons,
|
|
1399
|
+
v: 1,
|
|
1400
|
+
workUnits
|
|
1401
|
+
},
|
|
1402
|
+
v: 1
|
|
1403
|
+
};
|
|
1404
|
+
const serialized = canonicalJson(payload);
|
|
1405
|
+
return utf8ByteLength(serialized) <= evaluation.maximumResultBytes && sha256Hex(serialized) === resultSha256 ? { ...payload, resultSha256 } : null;
|
|
1406
|
+
} catch {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function buildProjectionResult(input) {
|
|
1411
|
+
const matches = matchBody(input.materialized.relations, input.query.where, OH_PROJECTION_LIMITS_V1.queryMatches, input.work);
|
|
1412
|
+
const byValues = new Map;
|
|
1413
|
+
for (const match of matches) {
|
|
1414
|
+
const values = input.query.find.map((name) => match.binding.get(name));
|
|
1415
|
+
const key = tupleKey(values);
|
|
1416
|
+
const existing = byValues.get(key);
|
|
1417
|
+
if (existing === undefined)
|
|
1418
|
+
byValues.set(key, { match, supportCount: 1 });
|
|
1419
|
+
else
|
|
1420
|
+
byValues.set(key, { match: compareCanonical(match.premises, existing.match.premises) < 0 ? match : existing.match, supportCount: existing.supportCount + 1 });
|
|
1421
|
+
}
|
|
1422
|
+
const ordered = [...byValues.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
1423
|
+
const resultBudget = {
|
|
1424
|
+
bytes: 0,
|
|
1425
|
+
maximumBytes: input.options.maximumResultBytes - 64 * 1024,
|
|
1426
|
+
nodes: 0
|
|
1427
|
+
};
|
|
1428
|
+
const rows = [];
|
|
1429
|
+
let resultBytesTruncated = false;
|
|
1430
|
+
for (const [key, support] of ordered.slice(0, input.query.limit)) {
|
|
1431
|
+
const values = JSON.parse(key);
|
|
1432
|
+
if (!reserveResultBytes(resultBudget, {
|
|
1433
|
+
proofs: [],
|
|
1434
|
+
proofsTruncated: false,
|
|
1435
|
+
supportCount: support.supportCount,
|
|
1436
|
+
values,
|
|
1437
|
+
v: 1
|
|
1438
|
+
})) {
|
|
1439
|
+
resultBytesTruncated = true;
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
const budget = { nodes: 0, result: resultBudget };
|
|
1443
|
+
const proofs = [];
|
|
1444
|
+
for (const premise of support.match.premises) {
|
|
1445
|
+
const proof = proofForReference(input.materialized.relations, premise, budget, input.options, 0, new Set);
|
|
1446
|
+
if (proof === null)
|
|
1447
|
+
break;
|
|
1448
|
+
proofs.push(proof);
|
|
1449
|
+
}
|
|
1450
|
+
const proofsTruncated = proofs.length !== support.match.premises.length || proofs.some(proofIsTruncated);
|
|
1451
|
+
rows.push({ proofs, proofsTruncated, supportCount: support.supportCount, values, v: 1 });
|
|
1452
|
+
}
|
|
1453
|
+
const queryLimitTruncated = ordered.length > input.query.limit;
|
|
1454
|
+
const truncationReasons = [
|
|
1455
|
+
...queryLimitTruncated ? ["query-limit"] : [],
|
|
1456
|
+
...resultBytesTruncated ? ["result-bytes"] : []
|
|
1457
|
+
];
|
|
1458
|
+
const truncated = truncationReasons.length > 0;
|
|
1459
|
+
const identity = createOhProjectionIdentityV1({
|
|
1460
|
+
dataset: input.dataset,
|
|
1461
|
+
query: input.query,
|
|
1462
|
+
engine: input.engine,
|
|
1463
|
+
options: input.options,
|
|
1464
|
+
rulePack: input.rulePack,
|
|
1465
|
+
snapshot: input.snapshot
|
|
1466
|
+
});
|
|
1467
|
+
const payload = {
|
|
1468
|
+
authority: "derived",
|
|
1469
|
+
cache: { strategy: "full-rebuild", v: 1 },
|
|
1470
|
+
engine: input.engine,
|
|
1471
|
+
evaluation: { ...input.options, v: 1 },
|
|
1472
|
+
identity,
|
|
1473
|
+
rows,
|
|
1474
|
+
stats: {
|
|
1475
|
+
baseFacts: input.materialized.baseFacts,
|
|
1476
|
+
derivedFacts: input.materialized.derivedFacts,
|
|
1477
|
+
proofNodes: resultBudget.nodes,
|
|
1478
|
+
proofsTruncated: rows.some((row) => row.proofsTruncated),
|
|
1479
|
+
queryMatches: matches.length,
|
|
1480
|
+
relations: input.materialized.relations.size,
|
|
1481
|
+
rounds: input.materialized.rounds,
|
|
1482
|
+
truncated,
|
|
1483
|
+
truncationReasons,
|
|
1484
|
+
v: 1,
|
|
1485
|
+
workUnits: input.work.units
|
|
1486
|
+
},
|
|
1487
|
+
v: 1
|
|
1488
|
+
};
|
|
1489
|
+
const serialized = canonicalJson(payload);
|
|
1490
|
+
if (utf8ByteLength(serialized) > input.options.maximumResultBytes) {
|
|
1491
|
+
throw new RangeError("Projection result exceeds its canonical byte bound.");
|
|
1492
|
+
}
|
|
1493
|
+
return { ...payload, resultSha256: sha256Hex(serialized) };
|
|
1494
|
+
}
|
|
1495
|
+
function evaluateOhProjectionV1(input) {
|
|
1496
|
+
const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
|
|
1497
|
+
const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot);
|
|
1498
|
+
const rulePack = parseOhProjectionRulePackV1(input.rulePack);
|
|
1499
|
+
const query = parseOhProjectionQueryV1(input.query);
|
|
1500
|
+
if (snapshot === null || dataset === null || rulePack === null || query === null) {
|
|
1501
|
+
throw new TypeError("Invalid projection snapshot, dataset, rule pack, or query.");
|
|
1502
|
+
}
|
|
1503
|
+
const options = resolveEvaluationOptions(input.options ?? {});
|
|
1504
|
+
validateProgramArities(dataset, rulePack, query);
|
|
1505
|
+
const work = { maximum: options.maximumWorkUnits, units: 0 };
|
|
1506
|
+
const materialized = materializeNaive({
|
|
1507
|
+
dataset,
|
|
1508
|
+
maximumDerivedTuples: options.maximumDerivedTuples,
|
|
1509
|
+
maximumRounds: options.maximumRounds,
|
|
1510
|
+
rulePack,
|
|
1511
|
+
work
|
|
1512
|
+
});
|
|
1513
|
+
return buildProjectionResult({
|
|
1514
|
+
dataset,
|
|
1515
|
+
engine: OH_PROJECTION_INTERNAL_ENGINE_V1,
|
|
1516
|
+
materialized,
|
|
1517
|
+
options,
|
|
1518
|
+
query,
|
|
1519
|
+
rulePack,
|
|
1520
|
+
snapshot,
|
|
1521
|
+
work
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
function evaluateOhProjectionWithMaterializerV1(input) {
|
|
1525
|
+
const snapshot = parseOhProjectionSnapshotV1(input.snapshot);
|
|
1526
|
+
const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot);
|
|
1527
|
+
const rulePack = parseOhProjectionRulePackV1(input.rulePack);
|
|
1528
|
+
const query = parseOhProjectionQueryV1(input.query);
|
|
1529
|
+
const engine = safeCode(input.engine, 256);
|
|
1530
|
+
if (snapshot === null || dataset === null || rulePack === null || query === null || engine === null) {
|
|
1531
|
+
throw new TypeError("Invalid projection adapter input.");
|
|
1532
|
+
}
|
|
1533
|
+
const options = resolveEvaluationOptions(input.options ?? {});
|
|
1534
|
+
validateProgramArities(dataset, rulePack, query);
|
|
1535
|
+
const work = { maximum: options.maximumWorkUnits, units: 0 };
|
|
1536
|
+
const witnessMaterialization = materializeNaive({
|
|
1537
|
+
dataset,
|
|
1538
|
+
maximumDerivedTuples: options.maximumDerivedTuples,
|
|
1539
|
+
maximumRounds: options.maximumRounds,
|
|
1540
|
+
rulePack,
|
|
1541
|
+
work
|
|
1542
|
+
});
|
|
1543
|
+
const external = input.materialize({
|
|
1544
|
+
dataset,
|
|
1545
|
+
maximumDerivedTuples: options.maximumDerivedTuples,
|
|
1546
|
+
maximumRounds: options.maximumRounds,
|
|
1547
|
+
query,
|
|
1548
|
+
rulePack
|
|
1549
|
+
});
|
|
1550
|
+
const externalCanonical = new Map;
|
|
1551
|
+
for (const [relationName, tuples] of external.relationFacts) {
|
|
1552
|
+
const relation = projectionName(relationName);
|
|
1553
|
+
if (relation === null || tuples.length > OH_PROJECTION_LIMITS_V1.facts + options.maximumDerivedTuples) {
|
|
1554
|
+
throw new TypeError("Projection adapter returned an invalid relation.");
|
|
1555
|
+
}
|
|
1556
|
+
const parsed = tuples.map(tuple);
|
|
1557
|
+
if (parsed.some((value) => value === null))
|
|
1558
|
+
throw new TypeError("Projection adapter returned an invalid tuple.");
|
|
1559
|
+
const keys = [];
|
|
1560
|
+
for (const value of parsed) {
|
|
1561
|
+
if (value === null)
|
|
1562
|
+
throw new TypeError("Projection adapter returned an invalid tuple.");
|
|
1563
|
+
keys.push(tupleKey(value));
|
|
1564
|
+
}
|
|
1565
|
+
externalCanonical.set(relation, [...new Set(keys)].sort());
|
|
1566
|
+
}
|
|
1567
|
+
const expectedCanonical = new Map([...witnessMaterialization.relations.entries()].map(([relation, states]) => [relation, [...states.values()].map((state) => tupleKey(state.tuple)).sort()]));
|
|
1568
|
+
const relationNames = [...new Set([...externalCanonical.keys(), ...expectedCanonical.keys()])].sort();
|
|
1569
|
+
for (const relation of relationNames) {
|
|
1570
|
+
if (canonicalJson(externalCanonical.get(relation) ?? []) !== canonicalJson(expectedCanonical.get(relation) ?? [])) {
|
|
1571
|
+
throw new Error(`Projection adapter disagrees with Oh semantics for relation ${relation}.`);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return buildProjectionResult({
|
|
1575
|
+
dataset,
|
|
1576
|
+
engine,
|
|
1577
|
+
materialized: witnessMaterialization,
|
|
1578
|
+
options,
|
|
1579
|
+
query,
|
|
1580
|
+
rulePack,
|
|
1581
|
+
snapshot,
|
|
1582
|
+
work
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
function createOhProjectionRecordFactsV1(records, options = {}) {
|
|
1586
|
+
if (records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot)
|
|
1587
|
+
throw new RangeError("Too many records for projection facts.");
|
|
1588
|
+
const parsedRecords = [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((candidate) => {
|
|
1589
|
+
const record = parseKnowledgeGraphRecordV1(candidate);
|
|
1590
|
+
if (record === null)
|
|
1591
|
+
throw new TypeError("Invalid graph record for projection facts.");
|
|
1592
|
+
return record;
|
|
1593
|
+
});
|
|
1594
|
+
let projectedFactCount = 0;
|
|
1595
|
+
for (const record of parsedRecords) {
|
|
1596
|
+
if (options.includeRecords !== false)
|
|
1597
|
+
projectedFactCount += 1;
|
|
1598
|
+
if (options.includeDependencies !== false)
|
|
1599
|
+
projectedFactCount += record.dependencies.length;
|
|
1600
|
+
if (projectedFactCount > OH_PROJECTION_LIMITS_V1.facts) {
|
|
1601
|
+
throw new RangeError("Structural projection exceeds its fact bound.");
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const facts = [];
|
|
1605
|
+
for (const record of parsedRecords) {
|
|
1606
|
+
const source = [{ key: record.key, recordSha256: record.recordSha256, v: 1 }];
|
|
1607
|
+
if (options.includeRecords !== false) {
|
|
1608
|
+
facts.push(createOhProjectionFactV1({
|
|
1609
|
+
relation: "oh.record",
|
|
1610
|
+
sources: source,
|
|
1611
|
+
tuple: [record.key, record.kind, record.recordSha256]
|
|
1612
|
+
}));
|
|
1613
|
+
}
|
|
1614
|
+
if (options.includeDependencies !== false) {
|
|
1615
|
+
for (const dependency of record.dependencies) {
|
|
1616
|
+
facts.push(createOhProjectionFactV1({
|
|
1617
|
+
relation: "oh.dependency",
|
|
1618
|
+
sources: source,
|
|
1619
|
+
tuple: [record.key, dependency]
|
|
1620
|
+
}));
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return facts.sort(compareProjectionFacts);
|
|
1625
|
+
}
|
|
1626
|
+
function isOhProjectionRecordKindV1(value) {
|
|
1627
|
+
return OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === value);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/projection-suss.ts
|
|
1631
|
+
var OH_PROJECTION_SUSS_VERSION_V1 = "0.20.0";
|
|
1632
|
+
var OH_PROJECTION_SUSS_ENGINE_V1 = "suss.datalog.v0-20-0.equivalence";
|
|
1633
|
+
function encodeAtom(value) {
|
|
1634
|
+
return canonicalJson(value);
|
|
1635
|
+
}
|
|
1636
|
+
function decodeAtom(value) {
|
|
1637
|
+
if (typeof value !== "string")
|
|
1638
|
+
throw new TypeError("Suss returned a non-encoded projection atom.");
|
|
1639
|
+
const decoded = parseCanonicalJson(value, 16 * 1024);
|
|
1640
|
+
if (decoded !== null && typeof decoded === "object") {
|
|
1641
|
+
throw new TypeError("Suss returned a non-atomic projection value.");
|
|
1642
|
+
}
|
|
1643
|
+
return decoded;
|
|
1644
|
+
}
|
|
1645
|
+
function sussTerm(term) {
|
|
1646
|
+
return term.kind === "constant" ? constant(encodeAtom(term.value)) : variable(term.name);
|
|
1647
|
+
}
|
|
1648
|
+
function sussLiteral(literal) {
|
|
1649
|
+
return lit(literal.relation, ...literal.terms.map(sussTerm));
|
|
1650
|
+
}
|
|
1651
|
+
function sussRule(input) {
|
|
1652
|
+
return rule(input.head.relation, input.head.terms.map(sussTerm), input.body.map(sussLiteral), input.ruleId);
|
|
1653
|
+
}
|
|
1654
|
+
function projectionDomainSize(dataset, rulePack, query) {
|
|
1655
|
+
const atoms = new Set;
|
|
1656
|
+
for (const fact of dataset.facts)
|
|
1657
|
+
for (const value of fact.tuple)
|
|
1658
|
+
atoms.add(encodeAtom(value));
|
|
1659
|
+
const addTerms = (literal) => {
|
|
1660
|
+
for (const term of literal.terms)
|
|
1661
|
+
if (term.kind === "constant")
|
|
1662
|
+
atoms.add(encodeAtom(term.value));
|
|
1663
|
+
};
|
|
1664
|
+
for (const item of rulePack.rules) {
|
|
1665
|
+
addTerms(item.head);
|
|
1666
|
+
for (const literal of item.body)
|
|
1667
|
+
addTerms(literal);
|
|
1668
|
+
}
|
|
1669
|
+
for (const literal of query.where)
|
|
1670
|
+
addTerms(literal);
|
|
1671
|
+
return atoms.size;
|
|
1672
|
+
}
|
|
1673
|
+
function assertConservativeOutputBound(input) {
|
|
1674
|
+
const domainSize = BigInt(projectionDomainSize(input.dataset, input.rulePack, input.query));
|
|
1675
|
+
const heads = new Map;
|
|
1676
|
+
for (const item of input.rulePack.rules)
|
|
1677
|
+
heads.set(item.head.relation, item.head.terms.length);
|
|
1678
|
+
const baseCounts = new Map;
|
|
1679
|
+
for (const fact of input.dataset.facts) {
|
|
1680
|
+
if (heads.has(fact.relation))
|
|
1681
|
+
baseCounts.set(fact.relation, (baseCounts.get(fact.relation) ?? 0) + 1);
|
|
1682
|
+
}
|
|
1683
|
+
let possible = 0n;
|
|
1684
|
+
const maximum = BigInt(input.maximumDerivedTuples);
|
|
1685
|
+
for (const [relation, arity] of heads) {
|
|
1686
|
+
const relationSpace = domainSize ** BigInt(arity);
|
|
1687
|
+
const existing = BigInt(baseCounts.get(relation) ?? 0);
|
|
1688
|
+
possible += relationSpace > existing ? relationSpace - existing : 0n;
|
|
1689
|
+
if (possible > maximum) {
|
|
1690
|
+
throw new RangeError("The Suss equivalence adapter cannot prove the requested derived-tuple bound before evaluation; use the bounded Oh evaluator.");
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
function evaluateOhProjectionWithSussV1(input) {
|
|
1695
|
+
return evaluateOhProjectionWithMaterializerV1({
|
|
1696
|
+
dataset: input.dataset,
|
|
1697
|
+
engine: OH_PROJECTION_SUSS_ENGINE_V1,
|
|
1698
|
+
...input.options === undefined ? {} : { options: input.options },
|
|
1699
|
+
query: input.query,
|
|
1700
|
+
rulePack: input.rulePack,
|
|
1701
|
+
snapshot: input.snapshot,
|
|
1702
|
+
materialize: (program) => {
|
|
1703
|
+
assertConservativeOutputBound(program);
|
|
1704
|
+
const database = new Database;
|
|
1705
|
+
for (const fact of program.dataset.facts) {
|
|
1706
|
+
database.add(fact.relation, fact.tuple.map(encodeAtom));
|
|
1707
|
+
}
|
|
1708
|
+
evaluate(database, program.rulePack.rules.map(sussRule));
|
|
1709
|
+
const relationFacts = new Map;
|
|
1710
|
+
for (const relation of database.relationNames()) {
|
|
1711
|
+
relationFacts.set(relation, database.facts(relation).map((values) => values.map(decodeAtom)));
|
|
1712
|
+
}
|
|
1713
|
+
return { relationFacts };
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
export {
|
|
1718
|
+
evaluateOhProjectionWithSussV1,
|
|
1719
|
+
OH_PROJECTION_SUSS_VERSION_V1,
|
|
1720
|
+
OH_PROJECTION_SUSS_ENGINE_V1
|
|
1721
|
+
};
|