@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
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,3072 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/canonical.ts
|
|
3
|
+
import { createHash, randomBytes } from "crypto";
|
|
4
|
+
|
|
5
|
+
class OhValidationError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
path;
|
|
8
|
+
constructor(code, path, message) {
|
|
9
|
+
super(`${path}: ${message}`);
|
|
10
|
+
this.name = "OhValidationError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.path = path;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function isPlainRecord(value) {
|
|
16
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
17
|
+
return false;
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
}
|
|
21
|
+
function hasExactKeys(value, keys) {
|
|
22
|
+
const actual = Object.keys(value);
|
|
23
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
24
|
+
}
|
|
25
|
+
function assertUnicodeScalarString(value, path) {
|
|
26
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
27
|
+
const code = value.charCodeAt(index);
|
|
28
|
+
if (code >= 55296 && code <= 56319) {
|
|
29
|
+
const next = value.charCodeAt(index + 1);
|
|
30
|
+
if (!(next >= 56320 && next <= 57343)) {
|
|
31
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
32
|
+
}
|
|
33
|
+
index += 1;
|
|
34
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
35
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function encodeCanonical(value, path, ancestors) {
|
|
40
|
+
if (value === null || typeof value === "boolean")
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
assertUnicodeScalarString(value, path);
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "number") {
|
|
47
|
+
if (!Number.isFinite(value)) {
|
|
48
|
+
throw new OhValidationError("non-json-number", path, "must be finite");
|
|
49
|
+
}
|
|
50
|
+
if (Object.is(value, -0)) {
|
|
51
|
+
throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical");
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
if (typeof value !== "object" || value === null) {
|
|
56
|
+
throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`);
|
|
57
|
+
}
|
|
58
|
+
if (ancestors.has(value)) {
|
|
59
|
+
throw new OhValidationError("cycle", path, "contains a cycle");
|
|
60
|
+
}
|
|
61
|
+
ancestors.add(value);
|
|
62
|
+
try {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
const encoded = [];
|
|
65
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
66
|
+
if (!Object.hasOwn(value, index)) {
|
|
67
|
+
throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
|
|
68
|
+
}
|
|
69
|
+
encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors));
|
|
70
|
+
}
|
|
71
|
+
const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length));
|
|
72
|
+
if (extraKeys.length > 0) {
|
|
73
|
+
throw new OhValidationError("non-json-property", path, "array has non-index properties");
|
|
74
|
+
}
|
|
75
|
+
return `[${encoded.join(",")}]`;
|
|
76
|
+
}
|
|
77
|
+
if (!isPlainRecord(value)) {
|
|
78
|
+
throw new OhValidationError("non-plain-object", path, "must be a plain object");
|
|
79
|
+
}
|
|
80
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
81
|
+
if (ownKeys.some((key) => typeof key !== "string")) {
|
|
82
|
+
throw new OhValidationError("non-json-property", path, "object has a symbol property");
|
|
83
|
+
}
|
|
84
|
+
const keys = ownKeys;
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
87
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
88
|
+
throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
keys.sort();
|
|
92
|
+
const entries = keys.map((key) => {
|
|
93
|
+
assertUnicodeScalarString(key, `${path}.<key>`);
|
|
94
|
+
return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`;
|
|
95
|
+
});
|
|
96
|
+
return `{${entries.join(",")}}`;
|
|
97
|
+
} finally {
|
|
98
|
+
ancestors.delete(value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function canonicalJson(value) {
|
|
102
|
+
return encodeCanonical(value, "$", new Set);
|
|
103
|
+
}
|
|
104
|
+
function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) {
|
|
105
|
+
if (utf8ByteLength(text) > maximumBytes) {
|
|
106
|
+
throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit");
|
|
107
|
+
}
|
|
108
|
+
let value;
|
|
109
|
+
try {
|
|
110
|
+
value = JSON.parse(text);
|
|
111
|
+
} catch {
|
|
112
|
+
throw new OhValidationError("invalid-json", "$", "is not valid JSON");
|
|
113
|
+
}
|
|
114
|
+
if (canonicalJson(value) !== text) {
|
|
115
|
+
throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical");
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
function utf8ByteLength(value) {
|
|
120
|
+
return Buffer.byteLength(value, "utf8");
|
|
121
|
+
}
|
|
122
|
+
function sha256Hex(value) {
|
|
123
|
+
return createHash("sha256").update(value).digest("hex");
|
|
124
|
+
}
|
|
125
|
+
function canonicalSha256(value) {
|
|
126
|
+
return sha256Hex(canonicalJson(value));
|
|
127
|
+
}
|
|
128
|
+
function parseSha256Hex(value) {
|
|
129
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
|
|
130
|
+
}
|
|
131
|
+
function parseCanonicalInstantV1(value) {
|
|
132
|
+
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)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const timestamp = Date.parse(value);
|
|
136
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null;
|
|
137
|
+
}
|
|
138
|
+
function canonicalNow() {
|
|
139
|
+
return new Date().toISOString();
|
|
140
|
+
}
|
|
141
|
+
function opaqueId(prefix) {
|
|
142
|
+
if (!/^[a-z][a-z0-9_]{1,15}$/u.test(prefix)) {
|
|
143
|
+
throw new OhValidationError("invalid-prefix", "prefix", "must be a short lowercase code");
|
|
144
|
+
}
|
|
145
|
+
return `${prefix}${randomBytes(12).toString("hex")}`;
|
|
146
|
+
}
|
|
147
|
+
function safeCode(value, maximumLength = 128) {
|
|
148
|
+
return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
149
|
+
}
|
|
150
|
+
function boundedText(value, maximumBytes = 64 * 1024) {
|
|
151
|
+
if (typeof value !== "string" || value.length === 0 || value.normalize("NFC") !== value || utf8ByteLength(value) > maximumBytes)
|
|
152
|
+
return null;
|
|
153
|
+
try {
|
|
154
|
+
assertUnicodeScalarString(value, "$text");
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
for (const character of value) {
|
|
159
|
+
const code = character.codePointAt(0) ?? 0;
|
|
160
|
+
if (code <= 8 || code >= 11 && code <= 12 || code >= 14 && code <= 31 || code >= 127 && code <= 159)
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
function orderedUnique(values, key) {
|
|
166
|
+
return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value));
|
|
167
|
+
}
|
|
168
|
+
function sortUnique(values, key) {
|
|
169
|
+
const sorted = [...values].sort((left, right) => {
|
|
170
|
+
const leftKey = key(left);
|
|
171
|
+
const rightKey = key(right);
|
|
172
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
173
|
+
});
|
|
174
|
+
if (!orderedUnique(sorted, key)) {
|
|
175
|
+
throw new OhValidationError("duplicate", "$", "contains duplicate canonical values");
|
|
176
|
+
}
|
|
177
|
+
return sorted;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/graph.ts
|
|
181
|
+
var OH_GRAPH_FORMAT_VERSION_V1 = 1;
|
|
182
|
+
var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
183
|
+
changesPerOperation: 8192,
|
|
184
|
+
dependenciesPerRecord: 4096,
|
|
185
|
+
recordBytes: 1024 * 1024,
|
|
186
|
+
recordsPerSnapshot: 65536
|
|
187
|
+
});
|
|
188
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
189
|
+
"activity",
|
|
190
|
+
"assertion",
|
|
191
|
+
"context",
|
|
192
|
+
"dependency-manifest",
|
|
193
|
+
"edition",
|
|
194
|
+
"entity",
|
|
195
|
+
"evidence",
|
|
196
|
+
"identity-operation",
|
|
197
|
+
"inquiry",
|
|
198
|
+
"inquiry-event",
|
|
199
|
+
"review-decision",
|
|
200
|
+
"rights-decision",
|
|
201
|
+
"schema",
|
|
202
|
+
"shape",
|
|
203
|
+
"statement",
|
|
204
|
+
"type-membership",
|
|
205
|
+
"view",
|
|
206
|
+
"vocabulary"
|
|
207
|
+
];
|
|
208
|
+
function recordKey(value) {
|
|
209
|
+
return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
210
|
+
}
|
|
211
|
+
function createKnowledgeGraphRecordV1(input) {
|
|
212
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies))
|
|
213
|
+
throw new TypeError("Invalid graph record input.");
|
|
214
|
+
const key = recordKey(input.key);
|
|
215
|
+
const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
|
|
216
|
+
if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
217
|
+
throw new TypeError("Invalid graph record identity.");
|
|
218
|
+
const dependencies = input.dependencies.map(recordKey);
|
|
219
|
+
if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
|
|
220
|
+
throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
|
|
221
|
+
}
|
|
222
|
+
const valueJson = canonicalJson(input.value);
|
|
223
|
+
if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) {
|
|
224
|
+
throw new RangeError("Graph record value exceeds its canonical byte limit.");
|
|
225
|
+
}
|
|
226
|
+
const payload = { dependencies, key, kind, v: 1, value: input.value };
|
|
227
|
+
return { ...payload, recordSha256: canonicalSha256(payload) };
|
|
228
|
+
}
|
|
229
|
+
function parseKnowledgeGraphRecordV1(value) {
|
|
230
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256"))
|
|
231
|
+
return null;
|
|
232
|
+
const recordSha256 = parseSha256Hex(value.recordSha256);
|
|
233
|
+
const { recordSha256: _digest, ...input } = value;
|
|
234
|
+
try {
|
|
235
|
+
const created = createKnowledgeGraphRecordV1(input);
|
|
236
|
+
return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function knowledgeGraphRecordRefV1(record) {
|
|
242
|
+
return {
|
|
243
|
+
dependencies: record.dependencies,
|
|
244
|
+
key: record.key,
|
|
245
|
+
kind: record.kind,
|
|
246
|
+
sha256: record.recordSha256,
|
|
247
|
+
v: 1
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function changeKey(change) {
|
|
251
|
+
return change.kind === "put" ? change.record.key : change.key;
|
|
252
|
+
}
|
|
253
|
+
function canonicalKnowledgeGraphChangesV1(changes) {
|
|
254
|
+
const normalized = [];
|
|
255
|
+
for (const change of changes) {
|
|
256
|
+
if (!isPlainRecord(change) || change.v !== 1)
|
|
257
|
+
throw new TypeError("Invalid graph change.");
|
|
258
|
+
if (change.kind === "put") {
|
|
259
|
+
const record = parseKnowledgeGraphRecordV1(change.record);
|
|
260
|
+
if (record === null)
|
|
261
|
+
throw new TypeError("Invalid graph record in change.");
|
|
262
|
+
normalized.push({ kind: "put", record, v: 1 });
|
|
263
|
+
} else if (change.kind === "tombstone") {
|
|
264
|
+
const key = recordKey(change.key);
|
|
265
|
+
const priorSha256 = parseSha256Hex(change.priorSha256);
|
|
266
|
+
if (key === null || priorSha256 === null)
|
|
267
|
+
throw new TypeError("Invalid graph tombstone.");
|
|
268
|
+
normalized.push({ key, kind: "tombstone", priorSha256, v: 1 });
|
|
269
|
+
} else
|
|
270
|
+
throw new TypeError("Unknown graph change kind.");
|
|
271
|
+
}
|
|
272
|
+
return sortUnique(normalized, changeKey);
|
|
273
|
+
}
|
|
274
|
+
function graphRevisionSha256V1(input) {
|
|
275
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
276
|
+
const operationId = safeCode(input.operationId);
|
|
277
|
+
const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256);
|
|
278
|
+
const recordsSha256 = parseSha256Hex(input.recordsSha256);
|
|
279
|
+
const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null;
|
|
280
|
+
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)) {
|
|
281
|
+
throw new TypeError("Invalid graph revision digest input.");
|
|
282
|
+
}
|
|
283
|
+
return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 });
|
|
284
|
+
}
|
|
285
|
+
function createKnowledgeGraphRevisionV1(input) {
|
|
286
|
+
if (input.parent !== null && parseKnowledgeGraphRevisionV1(input.parent) === null) {
|
|
287
|
+
throw new TypeError("Invalid parent graph revision.");
|
|
288
|
+
}
|
|
289
|
+
const operationId = safeCode(input.operationId);
|
|
290
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
291
|
+
if (operationId === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation)
|
|
292
|
+
throw new TypeError("Invalid graph revision.");
|
|
293
|
+
const byKey = new Map((input.parent?.recordRefs ?? []).map((ref) => [ref.key, ref]));
|
|
294
|
+
for (const change of changes) {
|
|
295
|
+
if (change.kind === "put") {
|
|
296
|
+
for (const dependency of change.record.dependencies) {
|
|
297
|
+
if (!byKey.has(dependency) && !changes.some((candidate) => candidate.kind === "put" && candidate.record.key === dependency)) {
|
|
298
|
+
throw new TypeError(`Missing graph dependency: ${dependency}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
byKey.set(change.record.key, knowledgeGraphRecordRefV1(change.record));
|
|
302
|
+
} else {
|
|
303
|
+
const prior = byKey.get(change.key);
|
|
304
|
+
if (prior === undefined || prior.sha256 !== change.priorSha256)
|
|
305
|
+
throw new TypeError("Tombstone prior digest does not match.");
|
|
306
|
+
byKey.delete(change.key);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (byKey.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
310
|
+
throw new RangeError("Graph revision exceeds its record snapshot limit.");
|
|
311
|
+
}
|
|
312
|
+
for (const ref of byKey.values()) {
|
|
313
|
+
if (ref.dependencies.some((dependency) => !byKey.has(dependency))) {
|
|
314
|
+
throw new TypeError(`Missing graph dependency after revision: ${ref.key}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const recordRefs = [...byKey.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
|
|
318
|
+
const recordsSha256 = canonicalSha256(recordRefs);
|
|
319
|
+
const payload = {
|
|
320
|
+
changes,
|
|
321
|
+
operationId,
|
|
322
|
+
parentGraphRevisionSha256: input.parent?.graphRevisionSha256 ?? null,
|
|
323
|
+
recordRefs,
|
|
324
|
+
recordsSha256,
|
|
325
|
+
revision: (input.parent?.revision ?? 0) + 1,
|
|
326
|
+
v: 1
|
|
327
|
+
};
|
|
328
|
+
return { ...payload, graphRevisionSha256: graphRevisionSha256V1(payload) };
|
|
329
|
+
}
|
|
330
|
+
function parseKnowledgeGraphRevisionV1(value) {
|
|
331
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
332
|
+
"changes",
|
|
333
|
+
"graphRevisionSha256",
|
|
334
|
+
"operationId",
|
|
335
|
+
"parentGraphRevisionSha256",
|
|
336
|
+
"recordRefs",
|
|
337
|
+
"recordsSha256",
|
|
338
|
+
"revision",
|
|
339
|
+
"v"
|
|
340
|
+
]) || value.v !== 1 || !Array.isArray(value.changes) || !Array.isArray(value.recordRefs) || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot)
|
|
341
|
+
return null;
|
|
342
|
+
const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256);
|
|
343
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
344
|
+
const parentGraphRevisionSha256 = value.parentGraphRevisionSha256 === null ? null : parseSha256Hex(value.parentGraphRevisionSha256);
|
|
345
|
+
const operationId = safeCode(value.operationId);
|
|
346
|
+
const revision = Number.isSafeInteger(value.revision) && value.revision > 0 ? value.revision : null;
|
|
347
|
+
let changes;
|
|
348
|
+
try {
|
|
349
|
+
changes = canonicalKnowledgeGraphChangesV1(value.changes);
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const refs = [];
|
|
354
|
+
for (const item of value.recordRefs) {
|
|
355
|
+
if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "sha256", "v"]) || item.v !== 1 || !Array.isArray(item.dependencies))
|
|
356
|
+
return null;
|
|
357
|
+
const dependencies = item.dependencies.map(recordKey);
|
|
358
|
+
const key = recordKey(item.key);
|
|
359
|
+
const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === item.kind);
|
|
360
|
+
const sha256 = parseSha256Hex(item.sha256);
|
|
361
|
+
if (key === null || kind === undefined || sha256 === null || dependencies.some((dependency) => dependency === null) || dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord || !orderedUnique(dependencies, String) || dependencies.includes(key))
|
|
362
|
+
return null;
|
|
363
|
+
refs.push({ dependencies, key, kind, sha256, v: 1 });
|
|
364
|
+
}
|
|
365
|
+
if (graphRevisionSha256 === null || recordsSha256 === null || operationId === null || revision === null || value.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || !orderedUnique(refs, (ref) => ref.key) || canonicalSha256(refs) !== recordsSha256)
|
|
366
|
+
return null;
|
|
367
|
+
const keys = new Set(refs.map((ref) => ref.key));
|
|
368
|
+
if (refs.some((ref) => ref.dependencies.some((dependency) => !keys.has(dependency))))
|
|
369
|
+
return null;
|
|
370
|
+
const payload = {
|
|
371
|
+
changes,
|
|
372
|
+
operationId,
|
|
373
|
+
parentGraphRevisionSha256,
|
|
374
|
+
recordRefs: refs,
|
|
375
|
+
recordsSha256,
|
|
376
|
+
revision,
|
|
377
|
+
v: 1
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
return graphRevisionSha256V1(payload) === graphRevisionSha256 ? { ...payload, graphRevisionSha256 } : null;
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function reduceKnowledgeGraphRevisionsV1(revisions) {
|
|
386
|
+
if (revisions.length === 0 || revisions.length > 65536)
|
|
387
|
+
return null;
|
|
388
|
+
const ordered = [...revisions].sort((left, right) => left.revision - right.revision);
|
|
389
|
+
let parent = null;
|
|
390
|
+
const operationIds = new Set;
|
|
391
|
+
for (const candidate of ordered) {
|
|
392
|
+
const current = parseKnowledgeGraphRevisionV1(candidate);
|
|
393
|
+
if (current === null || current.revision !== (parent?.revision ?? 0) + 1 || current.parentGraphRevisionSha256 !== (parent?.graphRevisionSha256 ?? null) || operationIds.has(current.operationId))
|
|
394
|
+
return null;
|
|
395
|
+
try {
|
|
396
|
+
const rebuilt = createKnowledgeGraphRevisionV1({ changes: current.changes, operationId: current.operationId, parent });
|
|
397
|
+
if (rebuilt.graphRevisionSha256 !== current.graphRevisionSha256)
|
|
398
|
+
return null;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
operationIds.add(current.operationId);
|
|
403
|
+
parent = current;
|
|
404
|
+
}
|
|
405
|
+
return parent;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/ontology.ts
|
|
409
|
+
var OH_ONTOLOGY_VERSION_V1 = "1.0.0";
|
|
410
|
+
var OH_CONTRACT_ID_V1 = "oh.ontology.v1";
|
|
411
|
+
var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({
|
|
412
|
+
dimensions: 64,
|
|
413
|
+
listValues: 256,
|
|
414
|
+
qualifiers: 128,
|
|
415
|
+
statementBytes: 256 * 1024,
|
|
416
|
+
textBytes: 64 * 1024
|
|
417
|
+
});
|
|
418
|
+
var OH_KNOWLEDGE_KERNEL_CONCEPTS_V1 = [
|
|
419
|
+
{ code: "entity", description: "A stable identity anchor for something that can be referred to.", label: "Entity" },
|
|
420
|
+
{ code: "statement", description: "An immutable proposition with a subject, predicate, object, and qualifiers.", label: "Statement" },
|
|
421
|
+
{ code: "assertion", description: "An attributable stance toward a statement.", label: "Assertion" },
|
|
422
|
+
{ code: "evidence", description: "A typed account of how an observation bears on an assertion.", label: "Evidence" },
|
|
423
|
+
{ code: "context", description: "The scenario and dimensions in which knowledge applies.", label: "Context" },
|
|
424
|
+
{ code: "inquiry", description: "A question and its durable investigation trail.", label: "Inquiry" },
|
|
425
|
+
{ code: "projection", description: "A reproducible view derived from exact knowledge.", label: "Projection" }
|
|
426
|
+
];
|
|
427
|
+
function success(value) {
|
|
428
|
+
return { ok: true, value };
|
|
429
|
+
}
|
|
430
|
+
function failure(field, code = "invalid-input") {
|
|
431
|
+
return { error: { code, field }, ok: false };
|
|
432
|
+
}
|
|
433
|
+
function parseOpaqueId(value, prefix) {
|
|
434
|
+
return typeof value === "string" && new RegExp(`^${prefix}[a-z0-9]{24}$`, "u").test(value) ? value : null;
|
|
435
|
+
}
|
|
436
|
+
function parseKnowledgeEntityId(value) {
|
|
437
|
+
return parseOpaqueId(value, "kent_");
|
|
438
|
+
}
|
|
439
|
+
function parseKnowledgeAssertionId(value) {
|
|
440
|
+
return parseOpaqueId(value, "kast_");
|
|
441
|
+
}
|
|
442
|
+
function parseKnowledgeEvidenceId(value) {
|
|
443
|
+
return parseOpaqueId(value, "kevd_");
|
|
444
|
+
}
|
|
445
|
+
function parseKnowledgeInquiryId(value) {
|
|
446
|
+
return parseOpaqueId(value, "kinq_");
|
|
447
|
+
}
|
|
448
|
+
var OH_KNOWLEDGE_ENTITY_STATES_V1 = ["active", "quarantined", "redirected", "tombstoned"];
|
|
449
|
+
function parseKnowledgeEntityV1(value) {
|
|
450
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
451
|
+
"entityId",
|
|
452
|
+
"identityOperationId",
|
|
453
|
+
"identityRevision",
|
|
454
|
+
"redirectEntityId",
|
|
455
|
+
"state",
|
|
456
|
+
"v"
|
|
457
|
+
]) || value.v !== 1)
|
|
458
|
+
return failure("entity");
|
|
459
|
+
const entityId = parseKnowledgeEntityId(value.entityId);
|
|
460
|
+
const identityOperationId = safeCode(value.identityOperationId);
|
|
461
|
+
const identityRevision = Number.isSafeInteger(value.identityRevision) && value.identityRevision > 0 ? value.identityRevision : null;
|
|
462
|
+
const redirectEntityId = value.redirectEntityId === null ? null : parseKnowledgeEntityId(value.redirectEntityId);
|
|
463
|
+
const state = OH_KNOWLEDGE_ENTITY_STATES_V1.find((candidate) => candidate === value.state);
|
|
464
|
+
return entityId !== null && identityOperationId !== null && identityRevision !== null && (value.redirectEntityId === null || redirectEntityId !== null) && state !== undefined && state === "redirected" === (redirectEntityId !== null) && redirectEntityId !== entityId ? success({ entityId, identityOperationId, identityRevision, redirectEntityId, state, v: 1 }) : failure("entity");
|
|
465
|
+
}
|
|
466
|
+
function parseKnowledgeSchemaRefV1(value) {
|
|
467
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["code", "namespace", "revision", "schemaSha256", "v"]) || value.v !== 1)
|
|
468
|
+
return failure("schemaRef");
|
|
469
|
+
const code = safeCode(value.code);
|
|
470
|
+
const namespace = safeCode(value.namespace);
|
|
471
|
+
const revision = Number.isSafeInteger(value.revision) && value.revision > 0 ? value.revision : null;
|
|
472
|
+
const schemaSha256 = parseSha256Hex(value.schemaSha256);
|
|
473
|
+
return code !== null && namespace !== null && revision !== null && schemaSha256 !== null ? success({ code, namespace, revision, schemaSha256, v: 1 }) : failure("schemaRef");
|
|
474
|
+
}
|
|
475
|
+
var INTEGER = /^(?:0|-[1-9][0-9]*|[1-9][0-9]*)$/u;
|
|
476
|
+
var DECIMAL = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*[1-9])?$/u;
|
|
477
|
+
function parseKnowledgeValueInternal(value, depth) {
|
|
478
|
+
if (!isPlainRecord(value) || value.v !== 1 || depth > 8)
|
|
479
|
+
return null;
|
|
480
|
+
switch (value.kind) {
|
|
481
|
+
case "entity": {
|
|
482
|
+
if (!hasExactKeys(value, ["entityId", "kind", "v"]))
|
|
483
|
+
return null;
|
|
484
|
+
const entityId = parseKnowledgeEntityId(value.entityId);
|
|
485
|
+
return entityId === null ? null : { entityId, kind: "entity", v: 1 };
|
|
486
|
+
}
|
|
487
|
+
case "text": {
|
|
488
|
+
if (!hasExactKeys(value, ["kind", "language", "text", "v"]))
|
|
489
|
+
return null;
|
|
490
|
+
const language = typeof value.language === "string" && /^(?:und|[a-z]{2,3}(?:-[a-z0-9]{2,8})*)$/u.test(value.language) ? value.language : null;
|
|
491
|
+
const text = boundedText(value.text);
|
|
492
|
+
return language !== null && text !== null ? { kind: "text", language, text, v: 1 } : null;
|
|
493
|
+
}
|
|
494
|
+
case "string": {
|
|
495
|
+
const parsed = boundedText(value.value);
|
|
496
|
+
return hasExactKeys(value, ["kind", "v", "value"]) && parsed !== null ? { kind: "string", v: 1, value: parsed } : null;
|
|
497
|
+
}
|
|
498
|
+
case "boolean":
|
|
499
|
+
return hasExactKeys(value, ["kind", "v", "value"]) && typeof value.value === "boolean" ? { kind: "boolean", v: 1, value: value.value } : null;
|
|
500
|
+
case "integer":
|
|
501
|
+
case "decimal": {
|
|
502
|
+
const valid = typeof value.value === "string" && value.value.length <= 1024 && (value.kind === "integer" ? INTEGER.test(value.value) : DECIMAL.test(value.value) && value.value !== "-0");
|
|
503
|
+
return hasExactKeys(value, ["kind", "v", "value"]) && valid ? { kind: value.kind, v: 1, value: value.value } : null;
|
|
504
|
+
}
|
|
505
|
+
case "uri": {
|
|
506
|
+
if (!hasExactKeys(value, ["kind", "uri", "v"]) || typeof value.uri !== "string" || value.uri.length > 4096)
|
|
507
|
+
return null;
|
|
508
|
+
try {
|
|
509
|
+
const url = new URL(value.uri);
|
|
510
|
+
return url.href === value.uri && url.username === "" && url.password === "" && !["data:", "file:", "javascript:"].includes(url.protocol) ? { kind: "uri", uri: value.uri, v: 1 } : null;
|
|
511
|
+
} catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
case "list":
|
|
516
|
+
case "set": {
|
|
517
|
+
if (!hasExactKeys(value, ["kind", "v", "values"]) || !Array.isArray(value.values) || value.values.length > OH_KNOWLEDGE_LIMITS_V1.listValues)
|
|
518
|
+
return null;
|
|
519
|
+
const values = [];
|
|
520
|
+
for (const item of value.values) {
|
|
521
|
+
const parsed = parseKnowledgeValueInternal(item, depth + 1);
|
|
522
|
+
if (parsed === null)
|
|
523
|
+
return null;
|
|
524
|
+
values.push(parsed);
|
|
525
|
+
}
|
|
526
|
+
if (value.kind === "set" && !orderedUnique(values, canonicalJson))
|
|
527
|
+
return null;
|
|
528
|
+
return { kind: value.kind, v: 1, values };
|
|
529
|
+
}
|
|
530
|
+
case "extension": {
|
|
531
|
+
if (!hasExactKeys(value, ["canonicalizerSha256", "canonicalValue", "kind", "mediaType", "schema", "v", "valueSha256"]))
|
|
532
|
+
return null;
|
|
533
|
+
const canonicalizerSha256 = parseSha256Hex(value.canonicalizerSha256);
|
|
534
|
+
const canonicalValue = boundedText(value.canonicalValue, 64 * 1024);
|
|
535
|
+
const mediaType = typeof value.mediaType === "string" && /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u.test(value.mediaType) ? value.mediaType : null;
|
|
536
|
+
const schema = parseKnowledgeSchemaRefV1(value.schema);
|
|
537
|
+
const valueSha256 = parseSha256Hex(value.valueSha256);
|
|
538
|
+
return canonicalizerSha256 !== null && canonicalValue !== null && mediaType !== null && schema.ok && valueSha256 !== null ? { canonicalizerSha256, canonicalValue, kind: "extension", mediaType, schema: schema.value, v: 1, valueSha256 } : null;
|
|
539
|
+
}
|
|
540
|
+
default:
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function parseKnowledgeValueV1(value) {
|
|
545
|
+
const parsed = parseKnowledgeValueInternal(value, 0);
|
|
546
|
+
return parsed === null ? failure("value") : success(parsed);
|
|
547
|
+
}
|
|
548
|
+
function verifyKnowledgeValueV1(value) {
|
|
549
|
+
const parsed = parseKnowledgeValueV1(value);
|
|
550
|
+
if (!parsed.ok)
|
|
551
|
+
return parsed;
|
|
552
|
+
if (parsed.value.kind === "extension" && sha256Hex(parsed.value.canonicalValue) !== parsed.value.valueSha256) {
|
|
553
|
+
return failure("valueSha256", "digest-mismatch");
|
|
554
|
+
}
|
|
555
|
+
if (parsed.value.kind === "list" || parsed.value.kind === "set") {
|
|
556
|
+
for (const child of parsed.value.values) {
|
|
557
|
+
const verified = verifyKnowledgeValueV1(child);
|
|
558
|
+
if (!verified.ok)
|
|
559
|
+
return verified;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return success(parsed.value);
|
|
563
|
+
}
|
|
564
|
+
function parseDimension(value) {
|
|
565
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["predicate", "v", "value"]) || value.v !== 1)
|
|
566
|
+
return null;
|
|
567
|
+
const predicate = parseKnowledgeSchemaRefV1(value.predicate);
|
|
568
|
+
const parsedValue = parseKnowledgeValueV1(value.value);
|
|
569
|
+
return predicate.ok && parsedValue.ok ? { predicate: predicate.value, v: 1, value: parsedValue.value } : null;
|
|
570
|
+
}
|
|
571
|
+
function createKnowledgeContextV1(input) {
|
|
572
|
+
if (!isPlainRecord(input) || input.v !== 1 || !Array.isArray(input.dimensions) || input.dimensions.length > OH_KNOWLEDGE_LIMITS_V1.dimensions || !["actual", "counterfactual", "hypothetical", "planned"].includes(input.scenario))
|
|
573
|
+
return failure("context");
|
|
574
|
+
const dimensions = [];
|
|
575
|
+
for (const item of input.dimensions) {
|
|
576
|
+
const parsed = parseDimension(item);
|
|
577
|
+
if (parsed === null)
|
|
578
|
+
return failure("dimensions");
|
|
579
|
+
const verified = verifyKnowledgeValueV1(parsed.value);
|
|
580
|
+
if (!verified.ok)
|
|
581
|
+
return verified;
|
|
582
|
+
dimensions.push(parsed);
|
|
583
|
+
}
|
|
584
|
+
let canonicalDimensions;
|
|
585
|
+
try {
|
|
586
|
+
canonicalDimensions = sortUnique(dimensions, canonicalJson);
|
|
587
|
+
} catch {
|
|
588
|
+
return failure("dimensions", "noncanonical-input");
|
|
589
|
+
}
|
|
590
|
+
const payload = { dimensions: canonicalDimensions, scenario: input.scenario, v: 1 };
|
|
591
|
+
return success({ ...payload, contextSha256: canonicalSha256(payload) });
|
|
592
|
+
}
|
|
593
|
+
function parseKnowledgeContextV1(value) {
|
|
594
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["contextSha256", "dimensions", "scenario", "v"]))
|
|
595
|
+
return failure("context");
|
|
596
|
+
const digest = parseSha256Hex(value.contextSha256);
|
|
597
|
+
if (digest === null)
|
|
598
|
+
return failure("contextSha256");
|
|
599
|
+
const created = createKnowledgeContextV1({ dimensions: value.dimensions, scenario: value.scenario, v: value.v });
|
|
600
|
+
return created.ok && created.value.contextSha256 === digest && canonicalJson(created.value.dimensions) === canonicalJson(value.dimensions) ? success({ ...created.value, contextSha256: digest }) : failure("contextSha256", "digest-mismatch");
|
|
601
|
+
}
|
|
602
|
+
function createKnowledgeStatementV1(input) {
|
|
603
|
+
const object = parseKnowledgeValueV1(input.object);
|
|
604
|
+
const predicate = parseKnowledgeSchemaRefV1(input.predicate);
|
|
605
|
+
const subject = parseKnowledgeEntityId(input.subject);
|
|
606
|
+
if (input.v !== 1 || !object.ok || !predicate.ok || subject === null || !Array.isArray(input.qualifiers) || input.qualifiers.length > OH_KNOWLEDGE_LIMITS_V1.qualifiers)
|
|
607
|
+
return failure("statement");
|
|
608
|
+
const verifiedObject = verifyKnowledgeValueV1(object.value);
|
|
609
|
+
if (!verifiedObject.ok)
|
|
610
|
+
return verifiedObject;
|
|
611
|
+
const qualifiers = [];
|
|
612
|
+
for (const item of input.qualifiers) {
|
|
613
|
+
const parsed = parseDimension(item);
|
|
614
|
+
if (parsed === null)
|
|
615
|
+
return failure("qualifiers");
|
|
616
|
+
const verified = verifyKnowledgeValueV1(parsed.value);
|
|
617
|
+
if (!verified.ok)
|
|
618
|
+
return verified;
|
|
619
|
+
qualifiers.push(parsed);
|
|
620
|
+
}
|
|
621
|
+
let canonicalQualifiers;
|
|
622
|
+
try {
|
|
623
|
+
canonicalQualifiers = sortUnique(qualifiers, canonicalJson);
|
|
624
|
+
} catch {
|
|
625
|
+
return failure("qualifiers", "noncanonical-input");
|
|
626
|
+
}
|
|
627
|
+
const payload = { object: object.value, predicate: predicate.value, qualifiers: canonicalQualifiers, subject, v: 1 };
|
|
628
|
+
if (Buffer.byteLength(canonicalJson(payload), "utf8") > OH_KNOWLEDGE_LIMITS_V1.statementBytes)
|
|
629
|
+
return failure("statement", "limit-exceeded");
|
|
630
|
+
return success({ ...payload, statementSha256: canonicalSha256(payload) });
|
|
631
|
+
}
|
|
632
|
+
function parseKnowledgeStatementV1(value) {
|
|
633
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["object", "predicate", "qualifiers", "statementSha256", "subject", "v"]))
|
|
634
|
+
return failure("statement");
|
|
635
|
+
const digest = parseSha256Hex(value.statementSha256);
|
|
636
|
+
const created = createKnowledgeStatementV1(value);
|
|
637
|
+
return digest !== null && created.ok && created.value.statementSha256 === digest && canonicalJson(created.value.qualifiers) === canonicalJson(value.qualifiers) ? success({ ...created.value, statementSha256: digest }) : failure("statementSha256", "digest-mismatch");
|
|
638
|
+
}
|
|
639
|
+
function parseKnowledgeAgentRefV1(value) {
|
|
640
|
+
if (!isPlainRecord(value) || value.v !== 1)
|
|
641
|
+
return null;
|
|
642
|
+
if (value.kind === "entity" && hasExactKeys(value, ["entityId", "kind", "v"])) {
|
|
643
|
+
const entityId = parseKnowledgeEntityId(value.entityId);
|
|
644
|
+
return entityId === null ? null : { entityId, kind: "entity", v: 1 };
|
|
645
|
+
}
|
|
646
|
+
if (value.kind === "model" && hasExactKeys(value, ["kind", "model", "receiptSha256", "v"])) {
|
|
647
|
+
const model = parseKnowledgeSchemaRefV1(value.model);
|
|
648
|
+
const receiptSha256 = parseSha256Hex(value.receiptSha256);
|
|
649
|
+
return model.ok && receiptSha256 !== null ? { kind: "model", model: model.value, receiptSha256, v: 1 } : null;
|
|
650
|
+
}
|
|
651
|
+
if (value.kind === "system" && hasExactKeys(value, ["authority", "kind", "receiptSha256", "v"])) {
|
|
652
|
+
const authority = parseKnowledgeSchemaRefV1(value.authority);
|
|
653
|
+
const receiptSha256 = parseSha256Hex(value.receiptSha256);
|
|
654
|
+
return authority.ok && receiptSha256 !== null ? { authority: authority.value, kind: "system", receiptSha256, v: 1 } : null;
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
function parseDigestArray(value, maximum = 2048) {
|
|
659
|
+
if (!Array.isArray(value) || value.length > maximum)
|
|
660
|
+
return null;
|
|
661
|
+
const digests = value.map(parseSha256Hex);
|
|
662
|
+
return digests.every((digest) => digest !== null) && orderedUnique(digests, String) ? digests : null;
|
|
663
|
+
}
|
|
664
|
+
var OH_KNOWLEDGE_ACTIVITY_KINDS_V1 = [
|
|
665
|
+
"extraction",
|
|
666
|
+
"human-entry",
|
|
667
|
+
"human-review",
|
|
668
|
+
"import",
|
|
669
|
+
"model-proposal",
|
|
670
|
+
"normalization",
|
|
671
|
+
"publication",
|
|
672
|
+
"resolution",
|
|
673
|
+
"transformation"
|
|
674
|
+
];
|
|
675
|
+
function parseActivityInput(value) {
|
|
676
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
677
|
+
"actor",
|
|
678
|
+
"inputSha256s",
|
|
679
|
+
"kind",
|
|
680
|
+
"occurredAt",
|
|
681
|
+
"outputSha256s",
|
|
682
|
+
"policySha256",
|
|
683
|
+
"tool",
|
|
684
|
+
"v"
|
|
685
|
+
]) || value.v !== 1)
|
|
686
|
+
return null;
|
|
687
|
+
const actor = parseKnowledgeAgentRefV1(value.actor);
|
|
688
|
+
const inputSha256s = parseDigestArray(value.inputSha256s);
|
|
689
|
+
const kind = OH_KNOWLEDGE_ACTIVITY_KINDS_V1.find((candidate) => candidate === value.kind);
|
|
690
|
+
const occurredAt = parseCanonicalInstantV1(value.occurredAt);
|
|
691
|
+
const outputSha256s = parseDigestArray(value.outputSha256s);
|
|
692
|
+
const policySha256 = parseSha256Hex(value.policySha256);
|
|
693
|
+
const tool = value.tool === null ? null : parseKnowledgeSchemaRefV1(value.tool);
|
|
694
|
+
const parsedTool = tool === null ? null : tool.ok ? tool.value : null;
|
|
695
|
+
return actor !== null && inputSha256s !== null && kind !== undefined && occurredAt !== null && outputSha256s !== null && policySha256 !== null && (value.tool === null || parsedTool !== null) ? { actor, inputSha256s, kind, occurredAt, outputSha256s, policySha256, tool: parsedTool, v: 1 } : null;
|
|
696
|
+
}
|
|
697
|
+
function createKnowledgeActivityV1(input) {
|
|
698
|
+
const parsed = parseActivityInput(input);
|
|
699
|
+
return parsed === null ? failure("activity") : success({ ...parsed, activitySha256: canonicalSha256(parsed) });
|
|
700
|
+
}
|
|
701
|
+
function parseKnowledgeActivityV1(value) {
|
|
702
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "activitySha256"))
|
|
703
|
+
return failure("activity");
|
|
704
|
+
const activitySha256 = parseSha256Hex(value.activitySha256);
|
|
705
|
+
const { activitySha256: _digest, ...input } = value;
|
|
706
|
+
const parsed = parseActivityInput(input);
|
|
707
|
+
return activitySha256 !== null && parsed !== null && canonicalSha256(parsed) === activitySha256 ? success({ ...parsed, activitySha256 }) : failure("activitySha256", "digest-mismatch");
|
|
708
|
+
}
|
|
709
|
+
var OH_KNOWLEDGE_ASSERTION_STANCES_V1 = ["questions", "refutes", "reports", "supports", "undetermined"];
|
|
710
|
+
var OH_KNOWLEDGE_ASSERTION_STATES_V1 = [
|
|
711
|
+
"accepted-for-purpose",
|
|
712
|
+
"disputed",
|
|
713
|
+
"proposed",
|
|
714
|
+
"reviewed",
|
|
715
|
+
"superseded",
|
|
716
|
+
"withdrawn"
|
|
717
|
+
];
|
|
718
|
+
function parseStringCodes(value, maximum) {
|
|
719
|
+
if (!Array.isArray(value) || value.length > maximum)
|
|
720
|
+
return null;
|
|
721
|
+
const codes = value.map((item) => safeCode(item));
|
|
722
|
+
return codes.every((code) => code !== null) && orderedUnique(codes, String) ? codes : null;
|
|
723
|
+
}
|
|
724
|
+
function parseAssertionInput(value) {
|
|
725
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
726
|
+
"acceptedPurposes",
|
|
727
|
+
"assertionId",
|
|
728
|
+
"assertor",
|
|
729
|
+
"confidence",
|
|
730
|
+
"contextSha256",
|
|
731
|
+
"provenanceActivitySha256",
|
|
732
|
+
"reviewActivitySha256",
|
|
733
|
+
"stance",
|
|
734
|
+
"state",
|
|
735
|
+
"statementSha256",
|
|
736
|
+
"v"
|
|
737
|
+
]) || value.v !== 1)
|
|
738
|
+
return null;
|
|
739
|
+
const acceptedPurposes = parseStringCodes(value.acceptedPurposes, 32);
|
|
740
|
+
const assertionId = parseKnowledgeAssertionId(value.assertionId);
|
|
741
|
+
const assertor = parseKnowledgeAgentRefV1(value.assertor);
|
|
742
|
+
const confidence = value.confidence === null ? null : parseKnowledgeSchemaRefV1(value.confidence);
|
|
743
|
+
const parsedConfidence = confidence === null ? null : confidence.ok ? confidence.value : null;
|
|
744
|
+
const contextSha256 = value.contextSha256 === null ? null : parseSha256Hex(value.contextSha256);
|
|
745
|
+
const provenanceActivitySha256 = parseSha256Hex(value.provenanceActivitySha256);
|
|
746
|
+
const reviewActivitySha256 = value.reviewActivitySha256 === null ? null : parseSha256Hex(value.reviewActivitySha256);
|
|
747
|
+
const stance = OH_KNOWLEDGE_ASSERTION_STANCES_V1.find((candidate) => candidate === value.stance);
|
|
748
|
+
const state = OH_KNOWLEDGE_ASSERTION_STATES_V1.find((candidate) => candidate === value.state);
|
|
749
|
+
const statementSha256 = parseSha256Hex(value.statementSha256);
|
|
750
|
+
if (acceptedPurposes === null || assertionId === null || assertor === null || value.confidence !== null && parsedConfidence === null || value.contextSha256 !== null && contextSha256 === null || provenanceActivitySha256 === null || value.reviewActivitySha256 !== null && reviewActivitySha256 === null || stance === undefined || state === undefined || statementSha256 === null)
|
|
751
|
+
return null;
|
|
752
|
+
if (assertor.kind === "model" && (state !== "proposed" || acceptedPurposes.length !== 0 || reviewActivitySha256 !== null))
|
|
753
|
+
return null;
|
|
754
|
+
if (state === "accepted-for-purpose" !== acceptedPurposes.length > 0 || state !== "proposed" && reviewActivitySha256 === null)
|
|
755
|
+
return null;
|
|
756
|
+
return {
|
|
757
|
+
acceptedPurposes,
|
|
758
|
+
assertionId,
|
|
759
|
+
assertor,
|
|
760
|
+
confidence: parsedConfidence,
|
|
761
|
+
contextSha256,
|
|
762
|
+
provenanceActivitySha256,
|
|
763
|
+
reviewActivitySha256,
|
|
764
|
+
stance,
|
|
765
|
+
state,
|
|
766
|
+
statementSha256,
|
|
767
|
+
v: 1
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function createKnowledgeAssertionV1(input) {
|
|
771
|
+
const parsed = parseAssertionInput(input);
|
|
772
|
+
return parsed === null ? failure("assertion") : success({ ...parsed, assertionSha256: canonicalSha256(parsed) });
|
|
773
|
+
}
|
|
774
|
+
function parseKnowledgeAssertionV1(value) {
|
|
775
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "assertionSha256"))
|
|
776
|
+
return failure("assertion");
|
|
777
|
+
const assertionSha256 = parseSha256Hex(value.assertionSha256);
|
|
778
|
+
const { assertionSha256: _digest, ...input } = value;
|
|
779
|
+
const parsed = parseAssertionInput(input);
|
|
780
|
+
return assertionSha256 !== null && parsed !== null && canonicalSha256(parsed) === assertionSha256 ? success({ ...parsed, assertionSha256 }) : failure("assertionSha256", "digest-mismatch");
|
|
781
|
+
}
|
|
782
|
+
var OH_KNOWLEDGE_EVIDENCE_BEARINGS_V1 = [
|
|
783
|
+
"background",
|
|
784
|
+
"contradicts",
|
|
785
|
+
"corroborates",
|
|
786
|
+
"direct-observation",
|
|
787
|
+
"method",
|
|
788
|
+
"quotation",
|
|
789
|
+
"registry-record",
|
|
790
|
+
"supports"
|
|
791
|
+
];
|
|
792
|
+
function parseEvidenceInput(value) {
|
|
793
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
794
|
+
"assertionSha256",
|
|
795
|
+
"bearing",
|
|
796
|
+
"disclosure",
|
|
797
|
+
"evidenceId",
|
|
798
|
+
"observationSha256",
|
|
799
|
+
"provenanceActivitySha256",
|
|
800
|
+
"selector",
|
|
801
|
+
"sourceEntityId",
|
|
802
|
+
"v"
|
|
803
|
+
]) || value.v !== 1)
|
|
804
|
+
return null;
|
|
805
|
+
const assertionSha256 = parseSha256Hex(value.assertionSha256);
|
|
806
|
+
const bearing = OH_KNOWLEDGE_EVIDENCE_BEARINGS_V1.find((candidate) => candidate === value.bearing);
|
|
807
|
+
const evidenceId = parseKnowledgeEvidenceId(value.evidenceId);
|
|
808
|
+
const observationSha256 = value.observationSha256 === null ? null : parseSha256Hex(value.observationSha256);
|
|
809
|
+
const provenanceActivitySha256 = parseSha256Hex(value.provenanceActivitySha256);
|
|
810
|
+
const selector = value.selector === null ? null : boundedText(value.selector, 8192);
|
|
811
|
+
const sourceEntityId = value.sourceEntityId === null ? null : parseKnowledgeEntityId(value.sourceEntityId);
|
|
812
|
+
return assertionSha256 !== null && bearing !== undefined && (value.disclosure === "private" || value.disclosure === "public" || value.disclosure === "shared") && evidenceId !== null && (value.observationSha256 === null || observationSha256 !== null) && provenanceActivitySha256 !== null && (value.selector === null || selector !== null) && (value.sourceEntityId === null || sourceEntityId !== null) && (observationSha256 !== null || sourceEntityId !== null) ? {
|
|
813
|
+
assertionSha256,
|
|
814
|
+
bearing,
|
|
815
|
+
disclosure: value.disclosure,
|
|
816
|
+
evidenceId,
|
|
817
|
+
observationSha256,
|
|
818
|
+
provenanceActivitySha256,
|
|
819
|
+
selector,
|
|
820
|
+
sourceEntityId,
|
|
821
|
+
v: 1
|
|
822
|
+
} : null;
|
|
823
|
+
}
|
|
824
|
+
function createKnowledgeEvidenceLinkV1(input) {
|
|
825
|
+
const parsed = parseEvidenceInput(input);
|
|
826
|
+
return parsed === null ? failure("evidence") : success({ ...parsed, evidenceSha256: canonicalSha256(parsed) });
|
|
827
|
+
}
|
|
828
|
+
function parseKnowledgeEvidenceLinkV1(value) {
|
|
829
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "evidenceSha256"))
|
|
830
|
+
return failure("evidence");
|
|
831
|
+
const evidenceSha256 = parseSha256Hex(value.evidenceSha256);
|
|
832
|
+
const { evidenceSha256: _digest, ...input } = value;
|
|
833
|
+
const parsed = parseEvidenceInput(input);
|
|
834
|
+
return evidenceSha256 !== null && parsed !== null && canonicalSha256(parsed) === evidenceSha256 ? success({ ...parsed, evidenceSha256 }) : failure("evidenceSha256", "digest-mismatch");
|
|
835
|
+
}
|
|
836
|
+
function createKnowledgeInquiryV1(input) {
|
|
837
|
+
const answerForm = safeCode(input.answerForm);
|
|
838
|
+
const authorEntityId = parseKnowledgeEntityId(input.authorEntityId);
|
|
839
|
+
const contextSha256 = input.contextSha256 === null ? null : parseSha256Hex(input.contextSha256);
|
|
840
|
+
const createdAt = parseCanonicalInstantV1(input.createdAt);
|
|
841
|
+
const inquiryId = parseKnowledgeInquiryId(input.inquiryId);
|
|
842
|
+
const language = typeof input.language === "string" && /^(?:und|[a-z]{2,3}(?:-[a-z0-9]{2,8})*)$/u.test(input.language) ? input.language : null;
|
|
843
|
+
const parents = Array.isArray(input.parentInquiryIds) ? input.parentInquiryIds.map(parseKnowledgeInquiryId) : null;
|
|
844
|
+
const question = boundedText(input.question, 16384);
|
|
845
|
+
if (input.v !== 1 || answerForm === null || authorEntityId === null || input.contextSha256 !== null && contextSha256 === null || createdAt === null || inquiryId === null || language === null || parents === null || parents.some((item) => item === null) || !orderedUnique(parents, String) || !["private", "public", "shared"].includes(input.privacy) || question === null || !["abandoned", "open", "paused", "resolved"].includes(input.status))
|
|
846
|
+
return failure("inquiry");
|
|
847
|
+
const payload = {
|
|
848
|
+
answerForm,
|
|
849
|
+
authorEntityId,
|
|
850
|
+
contextSha256,
|
|
851
|
+
createdAt,
|
|
852
|
+
inquiryId,
|
|
853
|
+
language,
|
|
854
|
+
parentInquiryIds: parents,
|
|
855
|
+
privacy: input.privacy,
|
|
856
|
+
question,
|
|
857
|
+
status: input.status,
|
|
858
|
+
v: 1
|
|
859
|
+
};
|
|
860
|
+
return success({ ...payload, inquirySha256: canonicalSha256(payload) });
|
|
861
|
+
}
|
|
862
|
+
function parseKnowledgeInquiryV1(value) {
|
|
863
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "inquirySha256"))
|
|
864
|
+
return failure("inquiry");
|
|
865
|
+
const digest = parseSha256Hex(value.inquirySha256);
|
|
866
|
+
const { inquirySha256: _digest, ...input } = value;
|
|
867
|
+
const created = createKnowledgeInquiryV1(input);
|
|
868
|
+
return digest !== null && created.ok && created.value.inquirySha256 === digest ? success({ ...created.value, inquirySha256: digest }) : failure("inquirySha256", "digest-mismatch");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/schema.ts
|
|
872
|
+
var OH_SCHEMA_FORMAT_VERSION_V1 = 1;
|
|
873
|
+
var OH_SCHEMA_KINDS_V1 = ["concept", "mapping", "predicate", "shape", "unit", "vocabulary"];
|
|
874
|
+
function parseLocalizedTexts(value) {
|
|
875
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 128)
|
|
876
|
+
return null;
|
|
877
|
+
const output = [];
|
|
878
|
+
for (const item of value) {
|
|
879
|
+
if (!isPlainRecord(item) || !hasExactKeys(item, ["language", "text", "v"]) || item.v !== 1)
|
|
880
|
+
return null;
|
|
881
|
+
const language = typeof item.language === "string" && /^(?:und|[a-z]{2,3}(?:-[a-z0-9]{2,8})*)$/u.test(item.language) ? item.language : null;
|
|
882
|
+
const text = boundedText(item.text, 16384);
|
|
883
|
+
if (language === null || text === null)
|
|
884
|
+
return null;
|
|
885
|
+
output.push({ language, text, v: 1 });
|
|
886
|
+
}
|
|
887
|
+
return orderedUnique(output, canonicalJson) ? output : null;
|
|
888
|
+
}
|
|
889
|
+
function parseSchemaInput(value) {
|
|
890
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
891
|
+
"body",
|
|
892
|
+
"code",
|
|
893
|
+
"compatibility",
|
|
894
|
+
"description",
|
|
895
|
+
"kind",
|
|
896
|
+
"labels",
|
|
897
|
+
"namespace",
|
|
898
|
+
"previousSchemaSha256",
|
|
899
|
+
"revision",
|
|
900
|
+
"v"
|
|
901
|
+
]) || value.v !== 1 || !isPlainRecord(value.body))
|
|
902
|
+
return null;
|
|
903
|
+
try {
|
|
904
|
+
canonicalJson(value.body);
|
|
905
|
+
} catch {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
const code = safeCode(value.code);
|
|
909
|
+
const namespace = safeCode(value.namespace);
|
|
910
|
+
const kind = OH_SCHEMA_KINDS_V1.find((candidate) => candidate === value.kind);
|
|
911
|
+
const labels = parseLocalizedTexts(value.labels);
|
|
912
|
+
const description = parseLocalizedTexts(value.description);
|
|
913
|
+
const previousSchemaSha256 = value.previousSchemaSha256 === null ? null : parseSha256Hex(value.previousSchemaSha256);
|
|
914
|
+
const revision = Number.isSafeInteger(value.revision) && value.revision > 0 ? value.revision : null;
|
|
915
|
+
const compatibility = value.compatibility === "additive" || value.compatibility === "breaking" ? value.compatibility : null;
|
|
916
|
+
return code !== null && namespace !== null && kind !== undefined && labels !== null && description !== null && (value.previousSchemaSha256 === null || previousSchemaSha256 !== null) && revision !== null && compatibility !== null && revision === 1 === (previousSchemaSha256 === null) && (revision !== 1 || compatibility === "additive") ? {
|
|
917
|
+
body: value.body,
|
|
918
|
+
code,
|
|
919
|
+
compatibility,
|
|
920
|
+
description,
|
|
921
|
+
kind,
|
|
922
|
+
labels,
|
|
923
|
+
namespace,
|
|
924
|
+
previousSchemaSha256,
|
|
925
|
+
revision,
|
|
926
|
+
v: 1
|
|
927
|
+
} : null;
|
|
928
|
+
}
|
|
929
|
+
function createKnowledgeSchemaRevisionV1(input) {
|
|
930
|
+
const parsed = parseSchemaInput(input);
|
|
931
|
+
if (parsed === null)
|
|
932
|
+
throw new TypeError("Invalid schema revision input.");
|
|
933
|
+
return { ...parsed, schemaSha256: canonicalSha256(parsed) };
|
|
934
|
+
}
|
|
935
|
+
function parseKnowledgeSchemaRevisionV1(value) {
|
|
936
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "schemaSha256"))
|
|
937
|
+
return null;
|
|
938
|
+
const schemaSha256 = parseSha256Hex(value.schemaSha256);
|
|
939
|
+
const { schemaSha256: _digest, ...input } = value;
|
|
940
|
+
const parsed = parseSchemaInput(input);
|
|
941
|
+
return schemaSha256 !== null && parsed !== null && canonicalSha256(parsed) === schemaSha256 ? { ...parsed, schemaSha256 } : null;
|
|
942
|
+
}
|
|
943
|
+
function knowledgeSchemaRefV1(schema) {
|
|
944
|
+
return {
|
|
945
|
+
code: schema.code,
|
|
946
|
+
namespace: schema.namespace,
|
|
947
|
+
revision: schema.revision,
|
|
948
|
+
schemaSha256: schema.schemaSha256,
|
|
949
|
+
v: 1
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function additiveBodyRetainsPrior(prior, next) {
|
|
953
|
+
return Object.entries(prior).every(([key, value]) => Object.hasOwn(next, key) && canonicalJson(next[key]) === canonicalJson(value));
|
|
954
|
+
}
|
|
955
|
+
function verifyKnowledgeSchemaEvolutionV1(prior, next) {
|
|
956
|
+
if (parseKnowledgeSchemaRevisionV1(prior) === null || parseKnowledgeSchemaRevisionV1(next) === null) {
|
|
957
|
+
return { ok: false, reason: "invalid-schema" };
|
|
958
|
+
}
|
|
959
|
+
if (prior.namespace !== next.namespace || prior.code !== next.code || prior.kind !== next.kind) {
|
|
960
|
+
return { ok: false, reason: "identity-changed" };
|
|
961
|
+
}
|
|
962
|
+
if (next.revision !== prior.revision + 1 || next.previousSchemaSha256 !== prior.schemaSha256) {
|
|
963
|
+
return { ok: false, reason: "chain-broken" };
|
|
964
|
+
}
|
|
965
|
+
if (next.compatibility === "additive" && !additiveBodyRetainsPrior(prior.body, next.body)) {
|
|
966
|
+
return { ok: false, reason: "false-additive-claim" };
|
|
967
|
+
}
|
|
968
|
+
return { ok: true };
|
|
969
|
+
}
|
|
970
|
+
function createKnowledgeVocabularyRevisionV1(input) {
|
|
971
|
+
const namespace = safeCode(input.namespace);
|
|
972
|
+
if (namespace === null || input.v !== 1 || !Number.isSafeInteger(input.revision) || input.revision < 1 || !Array.isArray(input.schemaRefs) || input.schemaRefs.length > 65536) {
|
|
973
|
+
throw new TypeError("Invalid vocabulary revision input.");
|
|
974
|
+
}
|
|
975
|
+
const refs = [];
|
|
976
|
+
for (const candidate of input.schemaRefs) {
|
|
977
|
+
const parsed = parseKnowledgeSchemaRefV1(candidate);
|
|
978
|
+
if (!parsed.ok || parsed.value.namespace !== namespace)
|
|
979
|
+
throw new TypeError("Invalid vocabulary schema reference.");
|
|
980
|
+
refs.push(parsed.value);
|
|
981
|
+
}
|
|
982
|
+
if (!orderedUnique(refs, canonicalJson))
|
|
983
|
+
throw new TypeError("Vocabulary schema references must be ordered and unique.");
|
|
984
|
+
const payload = { namespace, revision: input.revision, schemaRefs: refs, v: 1 };
|
|
985
|
+
return { ...payload, vocabularySha256: canonicalSha256(payload) };
|
|
986
|
+
}
|
|
987
|
+
function parseKnowledgeVocabularyRevisionV1(value) {
|
|
988
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["namespace", "revision", "schemaRefs", "v", "vocabularySha256"]))
|
|
989
|
+
return null;
|
|
990
|
+
const digest = parseSha256Hex(value.vocabularySha256);
|
|
991
|
+
try {
|
|
992
|
+
const created = createKnowledgeVocabularyRevisionV1({
|
|
993
|
+
namespace: value.namespace,
|
|
994
|
+
revision: value.revision,
|
|
995
|
+
schemaRefs: value.schemaRefs,
|
|
996
|
+
v: value.v
|
|
997
|
+
});
|
|
998
|
+
return digest !== null && created.vocabularySha256 === digest ? { ...created, vocabularySha256: digest } : null;
|
|
999
|
+
} catch {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/contract.ts
|
|
1005
|
+
var manifestPayload = Object.freeze({
|
|
1006
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
1007
|
+
graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1,
|
|
1008
|
+
ontologyVersion: OH_ONTOLOGY_VERSION_V1,
|
|
1009
|
+
recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1,
|
|
1010
|
+
schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1,
|
|
1011
|
+
v: 1
|
|
1012
|
+
});
|
|
1013
|
+
var OH_CONTRACT_MANIFEST_V1 = Object.freeze({
|
|
1014
|
+
...manifestPayload,
|
|
1015
|
+
contractSha256: canonicalSha256(manifestPayload)
|
|
1016
|
+
});
|
|
1017
|
+
function parseOhContractManifestV1(value) {
|
|
1018
|
+
try {
|
|
1019
|
+
return canonicalJson(value) === canonicalJson(OH_CONTRACT_MANIFEST_V1) ? OH_CONTRACT_MANIFEST_V1 : null;
|
|
1020
|
+
} catch {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
class OhRecordCodecRegistry {
|
|
1026
|
+
#codecs = new Map;
|
|
1027
|
+
#sealed = false;
|
|
1028
|
+
register(codec) {
|
|
1029
|
+
if (this.#sealed)
|
|
1030
|
+
throw new TypeError("The codec registry is sealed.");
|
|
1031
|
+
if (this.#codecs.has(codec.kind))
|
|
1032
|
+
throw new TypeError(`A codec is already registered for ${codec.kind}.`);
|
|
1033
|
+
this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse }));
|
|
1034
|
+
return this;
|
|
1035
|
+
}
|
|
1036
|
+
parse(kind, value) {
|
|
1037
|
+
const codec = this.#codecs.get(kind);
|
|
1038
|
+
if (codec !== undefined)
|
|
1039
|
+
return codec.parse(value);
|
|
1040
|
+
try {
|
|
1041
|
+
canonicalJson(value);
|
|
1042
|
+
return value;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
has(kind) {
|
|
1048
|
+
return this.#codecs.has(kind);
|
|
1049
|
+
}
|
|
1050
|
+
parseRequired(kind, value) {
|
|
1051
|
+
const codec = this.#codecs.get(kind);
|
|
1052
|
+
if (codec === undefined)
|
|
1053
|
+
return null;
|
|
1054
|
+
try {
|
|
1055
|
+
const parsed = codec.parse(value);
|
|
1056
|
+
if (parsed === null)
|
|
1057
|
+
return null;
|
|
1058
|
+
canonicalJson(parsed);
|
|
1059
|
+
return parsed;
|
|
1060
|
+
} catch {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
seal() {
|
|
1065
|
+
this.#sealed = true;
|
|
1066
|
+
return this;
|
|
1067
|
+
}
|
|
1068
|
+
get sealed() {
|
|
1069
|
+
return this.#sealed;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/operation.ts
|
|
1074
|
+
var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
|
|
1075
|
+
function parsePayload(value) {
|
|
1076
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1077
|
+
"actorId",
|
|
1078
|
+
"changes",
|
|
1079
|
+
"contractId",
|
|
1080
|
+
"graphRevisionSha256",
|
|
1081
|
+
"instant",
|
|
1082
|
+
"operationId",
|
|
1083
|
+
"parentOperationSha256",
|
|
1084
|
+
"recordsSha256",
|
|
1085
|
+
"sequence",
|
|
1086
|
+
"spaceId",
|
|
1087
|
+
"v"
|
|
1088
|
+
]) || value.v !== 1 || value.contractId !== OH_CONTRACT_ID_V1 || !Array.isArray(value.changes))
|
|
1089
|
+
return null;
|
|
1090
|
+
const actorId = safeCode(value.actorId);
|
|
1091
|
+
const operationId = safeCode(value.operationId);
|
|
1092
|
+
const spaceId = safeCode(value.spaceId);
|
|
1093
|
+
const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256);
|
|
1094
|
+
const parentOperationSha256 = value.parentOperationSha256 === null ? null : parseSha256Hex(value.parentOperationSha256);
|
|
1095
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
1096
|
+
const instant = parseCanonicalInstantV1(value.instant);
|
|
1097
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence > 0 ? value.sequence : null;
|
|
1098
|
+
let changes;
|
|
1099
|
+
try {
|
|
1100
|
+
changes = canonicalKnowledgeGraphChangesV1(value.changes);
|
|
1101
|
+
} catch {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation)
|
|
1105
|
+
return null;
|
|
1106
|
+
return actorId !== null && operationId !== null && spaceId !== null && graphRevisionSha256 !== null && recordsSha256 !== null && instant !== null && sequence !== null && (value.parentOperationSha256 === null || parentOperationSha256 !== null) && sequence === 1 === (parentOperationSha256 === null) ? {
|
|
1107
|
+
actorId,
|
|
1108
|
+
changes,
|
|
1109
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
1110
|
+
graphRevisionSha256,
|
|
1111
|
+
instant,
|
|
1112
|
+
operationId,
|
|
1113
|
+
parentOperationSha256,
|
|
1114
|
+
recordsSha256,
|
|
1115
|
+
sequence,
|
|
1116
|
+
spaceId,
|
|
1117
|
+
v: 1
|
|
1118
|
+
} : null;
|
|
1119
|
+
}
|
|
1120
|
+
function createOhOperationV1(input) {
|
|
1121
|
+
const payload = parsePayload(input);
|
|
1122
|
+
if (payload === null)
|
|
1123
|
+
throw new TypeError("Invalid Oh operation payload.");
|
|
1124
|
+
const operation = { ...payload, operationSha256: canonicalSha256(payload) };
|
|
1125
|
+
if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) {
|
|
1126
|
+
throw new RangeError("Oh operation exceeds its canonical byte limit.");
|
|
1127
|
+
}
|
|
1128
|
+
return operation;
|
|
1129
|
+
}
|
|
1130
|
+
function parseOhOperationV1(value) {
|
|
1131
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "operationSha256"))
|
|
1132
|
+
return null;
|
|
1133
|
+
const operationSha256 = parseSha256Hex(value.operationSha256);
|
|
1134
|
+
const { operationSha256: _digest, ...input } = value;
|
|
1135
|
+
const payload = parsePayload(input);
|
|
1136
|
+
return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/sync.ts
|
|
1140
|
+
var OH_SYNC_PROTOCOL_V1 = "oh.sync.v1";
|
|
1141
|
+
function createOhSyncBundleV1(spaceId, operations) {
|
|
1142
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
1143
|
+
if (parsedSpaceId === null || operations.length > 1000)
|
|
1144
|
+
throw new TypeError("Invalid sync bundle.");
|
|
1145
|
+
let priorSequence = null;
|
|
1146
|
+
let priorSha256 = null;
|
|
1147
|
+
const parsed = [];
|
|
1148
|
+
for (const candidate of operations) {
|
|
1149
|
+
const operation = parseOhOperationV1(candidate);
|
|
1150
|
+
if (operation === null || operation.spaceId !== parsedSpaceId || priorSequence !== null && operation.sequence !== priorSequence + 1 || priorSequence !== null && operation.parentOperationSha256 !== priorSha256) {
|
|
1151
|
+
throw new TypeError("Sync operations must form one ordered chain.");
|
|
1152
|
+
}
|
|
1153
|
+
parsed.push(operation);
|
|
1154
|
+
priorSequence = operation.sequence;
|
|
1155
|
+
priorSha256 = operation.operationSha256;
|
|
1156
|
+
}
|
|
1157
|
+
const payload = {
|
|
1158
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
1159
|
+
operations: parsed,
|
|
1160
|
+
protocol: OH_SYNC_PROTOCOL_V1,
|
|
1161
|
+
spaceId: parsedSpaceId,
|
|
1162
|
+
v: 1
|
|
1163
|
+
};
|
|
1164
|
+
return { ...payload, bundleSha256: canonicalSha256(payload) };
|
|
1165
|
+
}
|
|
1166
|
+
function parseOhSyncBundleV1(value) {
|
|
1167
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["bundleSha256", "contractSha256", "operations", "protocol", "spaceId", "v"]) || value.protocol !== OH_SYNC_PROTOCOL_V1 || value.v !== 1 || !Array.isArray(value.operations))
|
|
1168
|
+
return null;
|
|
1169
|
+
const bundleSha256 = parseSha256Hex(value.bundleSha256);
|
|
1170
|
+
const contractSha256 = parseSha256Hex(value.contractSha256);
|
|
1171
|
+
if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256)
|
|
1172
|
+
return null;
|
|
1173
|
+
try {
|
|
1174
|
+
const created = createOhSyncBundleV1(value.spaceId, value.operations);
|
|
1175
|
+
return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
|
|
1176
|
+
} catch {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function synchronizeOhStoreV1(store, transport, options = {}) {
|
|
1181
|
+
const batchSize = options.batchSize ?? 100;
|
|
1182
|
+
const maximumRounds = options.maximumRounds ?? 100;
|
|
1183
|
+
const remoteId = safeCode(options.remoteId ?? "default");
|
|
1184
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1000 || !Number.isSafeInteger(maximumRounds) || maximumRounds < 1 || maximumRounds > 1e4 || remoteId === null)
|
|
1185
|
+
throw new TypeError("Invalid sync options.");
|
|
1186
|
+
await transport.handshake(OH_CONTRACT_MANIFEST_V1);
|
|
1187
|
+
let pulled = 0;
|
|
1188
|
+
let pushed = 0;
|
|
1189
|
+
for (let round = 1;round <= maximumRounds; round += 1) {
|
|
1190
|
+
const local = store.head();
|
|
1191
|
+
const remote = await transport.head(store.spaceId);
|
|
1192
|
+
if (local.sequence === remote.sequence) {
|
|
1193
|
+
if (local.operationSha256 !== remote.operationSha256) {
|
|
1194
|
+
throw new Error("Sync conflict: equal sequence numbers have different heads.");
|
|
1195
|
+
}
|
|
1196
|
+
store.updateSyncState(remoteId, {
|
|
1197
|
+
pulledSequence: local.sequence,
|
|
1198
|
+
pushedSequence: local.sequence,
|
|
1199
|
+
remoteHeadSha256: remote.operationSha256
|
|
1200
|
+
});
|
|
1201
|
+
return { head: remote, pulled, pushed, rounds: round, v: 1 };
|
|
1202
|
+
}
|
|
1203
|
+
if (local.sequence < remote.sequence) {
|
|
1204
|
+
const bundle = parseOhSyncBundleV1(await transport.pull(store.spaceId, local.sequence, batchSize));
|
|
1205
|
+
if (bundle === null || bundle.operations.length === 0 || bundle.operations[0]?.sequence !== local.sequence + 1 || bundle.operations[0]?.parentOperationSha256 !== local.operationSha256) {
|
|
1206
|
+
throw new Error("Sync conflict: remote history does not extend the local head.");
|
|
1207
|
+
}
|
|
1208
|
+
for (const operation of bundle.operations) {
|
|
1209
|
+
store.importOperation(operation);
|
|
1210
|
+
pulled += 1;
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
const operations = store.exportOperations(remote.sequence, batchSize);
|
|
1214
|
+
if (operations.length === 0 || operations[0]?.sequence !== remote.sequence + 1 || operations[0]?.parentOperationSha256 !== remote.operationSha256) {
|
|
1215
|
+
throw new Error("Sync conflict: local history does not extend the remote head.");
|
|
1216
|
+
}
|
|
1217
|
+
const head = await transport.push(createOhSyncBundleV1(store.spaceId, operations));
|
|
1218
|
+
if (head.sequence !== operations.at(-1)?.sequence || head.operationSha256 !== operations.at(-1)?.operationSha256) {
|
|
1219
|
+
throw new Error("The sync transport acknowledged a different head.");
|
|
1220
|
+
}
|
|
1221
|
+
pushed += operations.length;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
throw new Error("Sync did not settle within maximumRounds.");
|
|
1225
|
+
}
|
|
1226
|
+
function rowValue(row, key, index) {
|
|
1227
|
+
return Array.isArray(row) ? row[index] : row[key];
|
|
1228
|
+
}
|
|
1229
|
+
function createLibSqlOperationSyncTransportV1(client) {
|
|
1230
|
+
let ready = null;
|
|
1231
|
+
const setup = async (manifest) => {
|
|
1232
|
+
if (parseOhContractManifestV1(manifest) === null)
|
|
1233
|
+
throw new Error("Unsupported contract manifest.");
|
|
1234
|
+
await client.batch([
|
|
1235
|
+
{ sql: `CREATE TABLE IF NOT EXISTS oh_sync_contracts (
|
|
1236
|
+
contract_id TEXT PRIMARY KEY, contract_sha256 TEXT NOT NULL, manifest_json TEXT NOT NULL
|
|
1237
|
+
) STRICT` },
|
|
1238
|
+
{ sql: `CREATE TABLE IF NOT EXISTS oh_sync_operations (
|
|
1239
|
+
space_id TEXT NOT NULL, sequence INTEGER NOT NULL, operation_sha256 TEXT NOT NULL UNIQUE,
|
|
1240
|
+
operation_json TEXT NOT NULL, PRIMARY KEY(space_id, sequence)
|
|
1241
|
+
) STRICT` },
|
|
1242
|
+
{
|
|
1243
|
+
sql: "INSERT INTO oh_sync_contracts(contract_id, contract_sha256, manifest_json) VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING",
|
|
1244
|
+
args: [manifest.contractId, manifest.contractSha256, canonicalJson(manifest)]
|
|
1245
|
+
}
|
|
1246
|
+
], "write");
|
|
1247
|
+
const result = await client.execute({
|
|
1248
|
+
sql: "SELECT contract_sha256, manifest_json FROM oh_sync_contracts WHERE contract_id = ?",
|
|
1249
|
+
args: [manifest.contractId]
|
|
1250
|
+
});
|
|
1251
|
+
const row = result.rows[0];
|
|
1252
|
+
if (row === undefined || rowValue(row, "contract_sha256", 0) !== manifest.contractSha256 || rowValue(row, "manifest_json", 1) !== canonicalJson(manifest)) {
|
|
1253
|
+
throw new Error("Remote contract manifest mismatch.");
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
const ensure = (manifest = OH_CONTRACT_MANIFEST_V1) => {
|
|
1257
|
+
ready ??= setup(manifest).catch((error) => {
|
|
1258
|
+
ready = null;
|
|
1259
|
+
throw error;
|
|
1260
|
+
});
|
|
1261
|
+
return ready;
|
|
1262
|
+
};
|
|
1263
|
+
const head = async (spaceId) => {
|
|
1264
|
+
await ensure();
|
|
1265
|
+
const result = await client.execute({ sql: `SELECT sequence, operation_sha256 FROM oh_sync_operations
|
|
1266
|
+
WHERE space_id = ? ORDER BY sequence DESC LIMIT 1`, args: [spaceId] });
|
|
1267
|
+
const row = result.rows[0];
|
|
1268
|
+
if (row === undefined)
|
|
1269
|
+
return { operationSha256: null, sequence: 0, v: 1 };
|
|
1270
|
+
const sequence = Number(rowValue(row, "sequence", 0));
|
|
1271
|
+
const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
|
|
1272
|
+
if (!Number.isSafeInteger(sequence) || sequence < 1 || operationSha256 === null)
|
|
1273
|
+
throw new Error("Invalid remote head.");
|
|
1274
|
+
return { operationSha256, sequence, v: 1 };
|
|
1275
|
+
};
|
|
1276
|
+
return {
|
|
1277
|
+
handshake: async (manifest) => {
|
|
1278
|
+
const parsed = parseOhContractManifestV1(manifest);
|
|
1279
|
+
if (parsed === null)
|
|
1280
|
+
throw new Error("Unsupported contract manifest.");
|
|
1281
|
+
await ensure(parsed);
|
|
1282
|
+
},
|
|
1283
|
+
head,
|
|
1284
|
+
pull: async (spaceId, afterSequence, limit) => {
|
|
1285
|
+
await ensure();
|
|
1286
|
+
const result = await client.execute({ sql: `SELECT operation_json FROM oh_sync_operations
|
|
1287
|
+
WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`, args: [spaceId, afterSequence, limit] });
|
|
1288
|
+
const operations = result.rows.map((row) => {
|
|
1289
|
+
const json = rowValue(row, "operation_json", 0);
|
|
1290
|
+
if (typeof json !== "string")
|
|
1291
|
+
throw new Error("Invalid remote operation JSON.");
|
|
1292
|
+
const operation = parseOhOperationV1(JSON.parse(json));
|
|
1293
|
+
if (operation === null || canonicalJson(operation) !== json)
|
|
1294
|
+
throw new Error("Invalid remote operation.");
|
|
1295
|
+
return operation;
|
|
1296
|
+
});
|
|
1297
|
+
return createOhSyncBundleV1(spaceId, operations);
|
|
1298
|
+
},
|
|
1299
|
+
push: async (value) => {
|
|
1300
|
+
await ensure();
|
|
1301
|
+
const bundle = parseOhSyncBundleV1(value);
|
|
1302
|
+
if (bundle === null)
|
|
1303
|
+
throw new Error("Invalid outgoing sync bundle.");
|
|
1304
|
+
if (bundle.operations.length === 0)
|
|
1305
|
+
return head(bundle.spaceId);
|
|
1306
|
+
const remote = await head(bundle.spaceId);
|
|
1307
|
+
const first = bundle.operations[0];
|
|
1308
|
+
const last = bundle.operations.at(-1);
|
|
1309
|
+
if (remote.sequence === last.sequence && remote.operationSha256 === last.operationSha256)
|
|
1310
|
+
return remote;
|
|
1311
|
+
if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
|
|
1312
|
+
throw new Error("Sync conflict: pushed history does not extend the remote head.");
|
|
1313
|
+
}
|
|
1314
|
+
await client.batch(bundle.operations.map((operation) => ({
|
|
1315
|
+
sql: "INSERT INTO oh_sync_operations(space_id, sequence, operation_sha256, operation_json) VALUES (?, ?, ?, ?)",
|
|
1316
|
+
args: [bundle.spaceId, operation.sequence, operation.operationSha256, canonicalJson(operation)]
|
|
1317
|
+
})), "write");
|
|
1318
|
+
return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/search.ts
|
|
1324
|
+
async function searchOhV1(input) {
|
|
1325
|
+
const limit = input.limit ?? 10;
|
|
1326
|
+
const mode = input.mode ?? "keyword";
|
|
1327
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
1328
|
+
throw new RangeError("Search limit must be 1 through 100.");
|
|
1329
|
+
const keyword = mode === "semantic" ? [] : input.store.searchKeyword(input.query, Math.min(100, limit * 3));
|
|
1330
|
+
let semantic = [];
|
|
1331
|
+
const diagnostics = [];
|
|
1332
|
+
if (mode !== "keyword") {
|
|
1333
|
+
if (input.backend === undefined) {
|
|
1334
|
+
diagnostics.push({ code: "semantic-unavailable", message: "No local semantic backend is configured.", v: 1 });
|
|
1335
|
+
} else {
|
|
1336
|
+
try {
|
|
1337
|
+
semantic = await input.backend.search(input.query, Math.min(100, limit * 3), input.store);
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
diagnostics.push({
|
|
1340
|
+
code: "semantic-unavailable",
|
|
1341
|
+
message: error instanceof Error ? error.message : "Local semantic search failed.",
|
|
1342
|
+
v: 1
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const byKey = new Map;
|
|
1348
|
+
const add = (key, lane, rank, laneScore) => {
|
|
1349
|
+
const contribution = (lane === "keyword" ? 2 : 1) / (60 + rank);
|
|
1350
|
+
const current = byKey.get(key) ?? { evidence: [], score: 0 };
|
|
1351
|
+
current.evidence.push({ lane, rank, score: laneScore, v: 1 });
|
|
1352
|
+
current.score += contribution;
|
|
1353
|
+
byKey.set(key, current);
|
|
1354
|
+
};
|
|
1355
|
+
keyword.forEach((result, index) => add(result.key, "keyword", index + 1, result.score));
|
|
1356
|
+
semantic.forEach((result, index) => add(result.key, "semantic", index + 1, result.score));
|
|
1357
|
+
const results = [];
|
|
1358
|
+
for (const [key, rank] of [...byKey.entries()].sort((left, right) => right[1].score - left[1].score || left[0].localeCompare(right[0]))) {
|
|
1359
|
+
const record = input.store.get(key);
|
|
1360
|
+
if (record === null)
|
|
1361
|
+
continue;
|
|
1362
|
+
results.push({ evidence: rank.evidence, record, score: rank.score, v: 1 });
|
|
1363
|
+
if (results.length === limit)
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
return { diagnostics, mode, results, v: 1 };
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/sqlite/store.ts
|
|
1370
|
+
import { mkdirSync } from "fs";
|
|
1371
|
+
import { dirname } from "path";
|
|
1372
|
+
|
|
1373
|
+
// src/store.ts
|
|
1374
|
+
class OhConflictError extends Error {
|
|
1375
|
+
constructor(message) {
|
|
1376
|
+
super(message);
|
|
1377
|
+
this.name = "OhConflictError";
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
class OhIntegrityError extends Error {
|
|
1382
|
+
constructor(message) {
|
|
1383
|
+
super(message);
|
|
1384
|
+
this.name = "OhIntegrityError";
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
class OhDependencyError extends Error {
|
|
1389
|
+
constructor(message) {
|
|
1390
|
+
super(message);
|
|
1391
|
+
this.name = "OhDependencyError";
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
class OhProfileError extends Error {
|
|
1396
|
+
constructor(message) {
|
|
1397
|
+
super(message);
|
|
1398
|
+
this.name = "OhProfileError";
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
1402
|
+
applicationProfileSha256: null,
|
|
1403
|
+
capabilities: {
|
|
1404
|
+
changesSince: true,
|
|
1405
|
+
dependencyClosureExport: true,
|
|
1406
|
+
exactSnapshots: true,
|
|
1407
|
+
operationReplication: true,
|
|
1408
|
+
semanticBundleCommit: true,
|
|
1409
|
+
v: 1,
|
|
1410
|
+
wholeSpacePurge: false
|
|
1411
|
+
},
|
|
1412
|
+
profileId: "oh.store.canonical.v1",
|
|
1413
|
+
profileKind: "canonical",
|
|
1414
|
+
v: 1
|
|
1415
|
+
});
|
|
1416
|
+
var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
1417
|
+
applicationProfileSha256: null,
|
|
1418
|
+
capabilities: {
|
|
1419
|
+
changesSince: true,
|
|
1420
|
+
dependencyClosureExport: true,
|
|
1421
|
+
exactSnapshots: true,
|
|
1422
|
+
operationReplication: false,
|
|
1423
|
+
semanticBundleCommit: true,
|
|
1424
|
+
v: 1,
|
|
1425
|
+
wholeSpacePurge: true
|
|
1426
|
+
},
|
|
1427
|
+
profileId: "oh.store.working.v1",
|
|
1428
|
+
profileKind: "working",
|
|
1429
|
+
v: 1
|
|
1430
|
+
});
|
|
1431
|
+
var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({
|
|
1432
|
+
bytes: 64 * 1024 * 1024,
|
|
1433
|
+
records: 8192,
|
|
1434
|
+
roots: 1024
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
class OhPurgedSpaceError extends Error {
|
|
1438
|
+
receipt;
|
|
1439
|
+
constructor(receipt) {
|
|
1440
|
+
super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`);
|
|
1441
|
+
this.name = "OhPurgedSpaceError";
|
|
1442
|
+
this.receipt = receipt;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
var EMPTY_RECORDS_SHA256 = canonicalSha256([]);
|
|
1446
|
+
function emptyOhHeadV1() {
|
|
1447
|
+
return {
|
|
1448
|
+
generation: 0,
|
|
1449
|
+
graphRevisionSha256: null,
|
|
1450
|
+
operationSha256: null,
|
|
1451
|
+
recordsSha256: EMPTY_RECORDS_SHA256,
|
|
1452
|
+
sequence: 0,
|
|
1453
|
+
v: 1
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function parseOhHeadV1(value) {
|
|
1457
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1458
|
+
"generation",
|
|
1459
|
+
"graphRevisionSha256",
|
|
1460
|
+
"operationSha256",
|
|
1461
|
+
"recordsSha256",
|
|
1462
|
+
"sequence",
|
|
1463
|
+
"v"
|
|
1464
|
+
]) || value.v !== 1)
|
|
1465
|
+
return null;
|
|
1466
|
+
const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256);
|
|
1467
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
1468
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
1469
|
+
const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null;
|
|
1470
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null;
|
|
1471
|
+
return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null;
|
|
1472
|
+
}
|
|
1473
|
+
function parseOhHeadRefV1(value) {
|
|
1474
|
+
const complete = parseOhHeadV1(value);
|
|
1475
|
+
if (complete !== null) {
|
|
1476
|
+
return { operationSha256: complete.operationSha256, sequence: complete.sequence };
|
|
1477
|
+
}
|
|
1478
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"]))
|
|
1479
|
+
return null;
|
|
1480
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
1481
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null;
|
|
1482
|
+
return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null;
|
|
1483
|
+
}
|
|
1484
|
+
function parseCapabilities(value) {
|
|
1485
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1486
|
+
"changesSince",
|
|
1487
|
+
"dependencyClosureExport",
|
|
1488
|
+
"exactSnapshots",
|
|
1489
|
+
"operationReplication",
|
|
1490
|
+
"semanticBundleCommit",
|
|
1491
|
+
"v",
|
|
1492
|
+
"wholeSpacePurge"
|
|
1493
|
+
]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean")
|
|
1494
|
+
return null;
|
|
1495
|
+
return {
|
|
1496
|
+
changesSince: true,
|
|
1497
|
+
dependencyClosureExport: true,
|
|
1498
|
+
exactSnapshots: true,
|
|
1499
|
+
operationReplication: value.operationReplication,
|
|
1500
|
+
semanticBundleCommit: true,
|
|
1501
|
+
v: 1,
|
|
1502
|
+
wholeSpacePurge: value.wholeSpacePurge
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
function createOhStoreProfileV1(input) {
|
|
1506
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, [
|
|
1507
|
+
"applicationProfileSha256",
|
|
1508
|
+
"capabilities",
|
|
1509
|
+
"profileId",
|
|
1510
|
+
"profileKind",
|
|
1511
|
+
"v"
|
|
1512
|
+
]) || input.v !== 1)
|
|
1513
|
+
throw new TypeError("Invalid Oh store profile input.");
|
|
1514
|
+
const profileId = safeCode(input.profileId);
|
|
1515
|
+
const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256);
|
|
1516
|
+
const capabilities = parseCapabilities(input.capabilities);
|
|
1517
|
+
if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") {
|
|
1518
|
+
throw new TypeError("Invalid Oh store profile input.");
|
|
1519
|
+
}
|
|
1520
|
+
if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) {
|
|
1521
|
+
throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge.");
|
|
1522
|
+
}
|
|
1523
|
+
if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) {
|
|
1524
|
+
throw new OhProfileError("A canonical profile cannot permit whole-space purge.");
|
|
1525
|
+
}
|
|
1526
|
+
const payload = {
|
|
1527
|
+
applicationProfileSha256,
|
|
1528
|
+
capabilities: Object.freeze(capabilities),
|
|
1529
|
+
profileId,
|
|
1530
|
+
profileKind: input.profileKind,
|
|
1531
|
+
v: 1
|
|
1532
|
+
};
|
|
1533
|
+
return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) });
|
|
1534
|
+
}
|
|
1535
|
+
function parseOhStoreProfileV1(value) {
|
|
1536
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256"))
|
|
1537
|
+
return null;
|
|
1538
|
+
const digest = parseSha256Hex(value.profileSha256);
|
|
1539
|
+
const { profileSha256: _profileSha256, ...input } = value;
|
|
1540
|
+
try {
|
|
1541
|
+
const created = createOhStoreProfileV1(input);
|
|
1542
|
+
return digest !== null && created.profileSha256 === digest ? created : null;
|
|
1543
|
+
} catch {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function createOhStoreBindingV1(input) {
|
|
1548
|
+
const profile = parseOhStoreProfileV1(input.profile);
|
|
1549
|
+
const realmId = safeCode(input.realmId);
|
|
1550
|
+
const spaceId = safeCode(input.spaceId);
|
|
1551
|
+
if (input.v !== 1 || profile === null || realmId === null || spaceId === null) {
|
|
1552
|
+
throw new TypeError("Invalid Oh store binding input.");
|
|
1553
|
+
}
|
|
1554
|
+
const payload = {
|
|
1555
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
1556
|
+
profile,
|
|
1557
|
+
realmId,
|
|
1558
|
+
spaceId,
|
|
1559
|
+
v: 1
|
|
1560
|
+
};
|
|
1561
|
+
return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) });
|
|
1562
|
+
}
|
|
1563
|
+
function parseOhStoreBindingV1(value) {
|
|
1564
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1565
|
+
"bindingSha256",
|
|
1566
|
+
"contractSha256",
|
|
1567
|
+
"profile",
|
|
1568
|
+
"realmId",
|
|
1569
|
+
"spaceId",
|
|
1570
|
+
"v"
|
|
1571
|
+
]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256)
|
|
1572
|
+
return null;
|
|
1573
|
+
const bindingSha256 = parseSha256Hex(value.bindingSha256);
|
|
1574
|
+
const profile = parseOhStoreProfileV1(value.profile);
|
|
1575
|
+
try {
|
|
1576
|
+
if (bindingSha256 === null || profile === null)
|
|
1577
|
+
return null;
|
|
1578
|
+
const created = createOhStoreBindingV1({
|
|
1579
|
+
profile,
|
|
1580
|
+
realmId: value.realmId,
|
|
1581
|
+
spaceId: value.spaceId,
|
|
1582
|
+
v: 1
|
|
1583
|
+
});
|
|
1584
|
+
return created.bindingSha256 === bindingSha256 ? created : null;
|
|
1585
|
+
} catch {
|
|
1586
|
+
return null;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function sortedRecords(records) {
|
|
1590
|
+
return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
|
|
1591
|
+
}
|
|
1592
|
+
function verifyDependencies(records) {
|
|
1593
|
+
for (const record of records.values()) {
|
|
1594
|
+
for (const dependency of record.dependencies) {
|
|
1595
|
+
if (!records.has(dependency))
|
|
1596
|
+
throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
1601
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
1602
|
+
if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
1603
|
+
throw new TypeError("Invalid operation replay input.");
|
|
1604
|
+
}
|
|
1605
|
+
const records = new Map;
|
|
1606
|
+
const operationIds = new Set;
|
|
1607
|
+
let head = emptyOhHeadV1();
|
|
1608
|
+
for (const value of values) {
|
|
1609
|
+
const operation = parseOhOperationV1(value);
|
|
1610
|
+
if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) {
|
|
1611
|
+
throw new OhIntegrityError("Operation replay chain is broken.");
|
|
1612
|
+
}
|
|
1613
|
+
operationIds.add(operation.operationId);
|
|
1614
|
+
for (const change of operation.changes) {
|
|
1615
|
+
if (change.kind === "put")
|
|
1616
|
+
records.set(change.record.key, change.record);
|
|
1617
|
+
else {
|
|
1618
|
+
const prior = records.get(change.key);
|
|
1619
|
+
if (prior?.recordSha256 !== change.priorSha256) {
|
|
1620
|
+
throw new OhIntegrityError("Replay tombstone does not match its prior record.");
|
|
1621
|
+
}
|
|
1622
|
+
records.delete(change.key);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (records.size > maximumRecords)
|
|
1626
|
+
throw new RangeError("Operation replay exceeds its record bound.");
|
|
1627
|
+
verifyDependencies(records);
|
|
1628
|
+
const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1);
|
|
1629
|
+
const recordsSha256 = canonicalSha256(refs);
|
|
1630
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
1631
|
+
changes: operation.changes,
|
|
1632
|
+
operationId: operation.operationId,
|
|
1633
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
1634
|
+
recordsSha256,
|
|
1635
|
+
revision: operation.sequence
|
|
1636
|
+
});
|
|
1637
|
+
if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
1638
|
+
throw new OhIntegrityError("Replay does not reproduce an operation head.");
|
|
1639
|
+
}
|
|
1640
|
+
head = {
|
|
1641
|
+
generation: operation.sequence,
|
|
1642
|
+
graphRevisionSha256,
|
|
1643
|
+
operationSha256: operation.operationSha256,
|
|
1644
|
+
recordsSha256,
|
|
1645
|
+
sequence: operation.sequence,
|
|
1646
|
+
v: 1
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
return { head, records: sortedRecords(records.values()), v: 1 };
|
|
1650
|
+
}
|
|
1651
|
+
function transitionOhSnapshotV1(input) {
|
|
1652
|
+
const actorId = safeCode(input.actorId);
|
|
1653
|
+
const operationId = safeCode(input.operationId);
|
|
1654
|
+
const spaceId = safeCode(input.spaceId);
|
|
1655
|
+
const instant = parseCanonicalInstantV1(input.instant);
|
|
1656
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
1657
|
+
if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) {
|
|
1658
|
+
throw new TypeError("Invalid graph transition input.");
|
|
1659
|
+
}
|
|
1660
|
+
const head = parseOhHeadV1(input.snapshot.head);
|
|
1661
|
+
if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) {
|
|
1662
|
+
throw new OhIntegrityError("The transition snapshot is invalid.");
|
|
1663
|
+
}
|
|
1664
|
+
const records = new Map;
|
|
1665
|
+
for (const value of input.snapshot.records) {
|
|
1666
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
1667
|
+
if (record === null || records.has(record.key)) {
|
|
1668
|
+
throw new OhIntegrityError("The transition snapshot contains an invalid record.");
|
|
1669
|
+
}
|
|
1670
|
+
records.set(record.key, record);
|
|
1671
|
+
}
|
|
1672
|
+
verifyDependencies(records);
|
|
1673
|
+
const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1));
|
|
1674
|
+
if (priorRecordsSha256 !== head.recordsSha256) {
|
|
1675
|
+
throw new OhIntegrityError("The transition snapshot does not reproduce its head.");
|
|
1676
|
+
}
|
|
1677
|
+
for (const change of changes) {
|
|
1678
|
+
if (change.kind === "put")
|
|
1679
|
+
records.set(change.record.key, change.record);
|
|
1680
|
+
else {
|
|
1681
|
+
const prior = records.get(change.key);
|
|
1682
|
+
if (prior === undefined || prior.recordSha256 !== change.priorSha256) {
|
|
1683
|
+
throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`);
|
|
1684
|
+
}
|
|
1685
|
+
records.delete(change.key);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
1689
|
+
throw new RangeError("Graph transition exceeds its record snapshot limit.");
|
|
1690
|
+
}
|
|
1691
|
+
verifyDependencies(records);
|
|
1692
|
+
const nextRecords = sortedRecords(records.values());
|
|
1693
|
+
const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1));
|
|
1694
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
1695
|
+
changes,
|
|
1696
|
+
operationId,
|
|
1697
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
1698
|
+
recordsSha256,
|
|
1699
|
+
revision: head.sequence + 1
|
|
1700
|
+
});
|
|
1701
|
+
const operation = createOhOperationV1({
|
|
1702
|
+
actorId,
|
|
1703
|
+
changes,
|
|
1704
|
+
contractId: OH_CONTRACT_MANIFEST_V1.contractId,
|
|
1705
|
+
graphRevisionSha256,
|
|
1706
|
+
instant,
|
|
1707
|
+
operationId,
|
|
1708
|
+
parentOperationSha256: head.operationSha256,
|
|
1709
|
+
recordsSha256,
|
|
1710
|
+
sequence: head.sequence + 1,
|
|
1711
|
+
spaceId,
|
|
1712
|
+
v: 1
|
|
1713
|
+
});
|
|
1714
|
+
const nextHead = {
|
|
1715
|
+
generation: operation.sequence,
|
|
1716
|
+
graphRevisionSha256,
|
|
1717
|
+
operationSha256: operation.operationSha256,
|
|
1718
|
+
recordsSha256,
|
|
1719
|
+
sequence: operation.sequence,
|
|
1720
|
+
v: 1
|
|
1721
|
+
};
|
|
1722
|
+
return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } };
|
|
1723
|
+
}
|
|
1724
|
+
function normalizeRoots(values) {
|
|
1725
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) {
|
|
1726
|
+
throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`);
|
|
1727
|
+
}
|
|
1728
|
+
const roots = values.map((value) => safeCode(value, 512));
|
|
1729
|
+
if (roots.some((value) => value === null))
|
|
1730
|
+
throw new TypeError("Invalid dependency closure root.");
|
|
1731
|
+
const sorted = [...roots].sort();
|
|
1732
|
+
if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) {
|
|
1733
|
+
throw new TypeError("Dependency closure roots must be unique.");
|
|
1734
|
+
}
|
|
1735
|
+
return sorted;
|
|
1736
|
+
}
|
|
1737
|
+
function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) {
|
|
1738
|
+
const selected = new Map;
|
|
1739
|
+
const pending = [...roots];
|
|
1740
|
+
let selectedBytes = 0;
|
|
1741
|
+
while (pending.length > 0) {
|
|
1742
|
+
const key = pending.pop();
|
|
1743
|
+
if (selected.has(key))
|
|
1744
|
+
continue;
|
|
1745
|
+
const record = available.get(key);
|
|
1746
|
+
if (record === undefined)
|
|
1747
|
+
throw new OhDependencyError(`Dependency closure record ${key} is missing.`);
|
|
1748
|
+
selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1;
|
|
1749
|
+
if (selectedBytes > maximumBytes)
|
|
1750
|
+
throw new RangeError("Dependency closure exceeds its canonical byte bound.");
|
|
1751
|
+
selected.set(key, record);
|
|
1752
|
+
if (selected.size > maximumRecords)
|
|
1753
|
+
throw new RangeError("Dependency closure exceeds its record bound.");
|
|
1754
|
+
pending.push(...record.dependencies);
|
|
1755
|
+
}
|
|
1756
|
+
return sortedRecords(selected.values());
|
|
1757
|
+
}
|
|
1758
|
+
function createOhDependencyClosureV1(input) {
|
|
1759
|
+
const binding = parseOhStoreBindingV1(input.binding);
|
|
1760
|
+
const head = parseOhHeadV1(input.snapshot.head);
|
|
1761
|
+
const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records;
|
|
1762
|
+
if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) {
|
|
1763
|
+
throw new TypeError("Invalid dependency closure input.");
|
|
1764
|
+
}
|
|
1765
|
+
const roots = normalizeRoots(input.roots);
|
|
1766
|
+
const available = new Map;
|
|
1767
|
+
for (const value of input.snapshot.records) {
|
|
1768
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
1769
|
+
if (record === null || available.has(record.key))
|
|
1770
|
+
throw new OhIntegrityError("Snapshot contains an invalid record.");
|
|
1771
|
+
available.set(record.key, record);
|
|
1772
|
+
}
|
|
1773
|
+
const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1));
|
|
1774
|
+
if (recordsSha256 !== head.recordsSha256)
|
|
1775
|
+
throw new OhIntegrityError("Snapshot records do not reproduce its head.");
|
|
1776
|
+
const records = closureRecords(available, roots, maximumRecords);
|
|
1777
|
+
const payload = { binding, head, records, roots, v: 1 };
|
|
1778
|
+
const closure = { ...payload, closureSha256: canonicalSha256(payload) };
|
|
1779
|
+
if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) {
|
|
1780
|
+
throw new RangeError("Dependency closure exceeds its canonical byte bound.");
|
|
1781
|
+
}
|
|
1782
|
+
return Object.freeze(closure);
|
|
1783
|
+
}
|
|
1784
|
+
function parseOhDependencyClosureV1(value) {
|
|
1785
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1786
|
+
"binding",
|
|
1787
|
+
"closureSha256",
|
|
1788
|
+
"head",
|
|
1789
|
+
"records",
|
|
1790
|
+
"roots",
|
|
1791
|
+
"v"
|
|
1792
|
+
]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records)
|
|
1793
|
+
return null;
|
|
1794
|
+
const binding = parseOhStoreBindingV1(value.binding);
|
|
1795
|
+
const head = parseOhHeadV1(value.head);
|
|
1796
|
+
const closureSha256 = parseSha256Hex(value.closureSha256);
|
|
1797
|
+
if (binding === null || head === null || closureSha256 === null)
|
|
1798
|
+
return null;
|
|
1799
|
+
const records = new Map;
|
|
1800
|
+
for (const item of value.records) {
|
|
1801
|
+
const record = parseKnowledgeGraphRecordV1(item);
|
|
1802
|
+
if (record === null || records.has(record.key))
|
|
1803
|
+
return null;
|
|
1804
|
+
records.set(record.key, record);
|
|
1805
|
+
}
|
|
1806
|
+
try {
|
|
1807
|
+
const roots = normalizeRoots(value.roots);
|
|
1808
|
+
if (canonicalJson(roots) !== canonicalJson(value.roots))
|
|
1809
|
+
return null;
|
|
1810
|
+
const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records);
|
|
1811
|
+
if (canonicalJson(exact) !== canonicalJson(value.records))
|
|
1812
|
+
return null;
|
|
1813
|
+
const payload = { binding, head, records: exact, roots, v: 1 };
|
|
1814
|
+
const parsed = { ...payload, closureSha256 };
|
|
1815
|
+
return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null;
|
|
1816
|
+
} catch {
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function verifyOhDependencyClosureV1(value) {
|
|
1821
|
+
const closure = parseOhDependencyClosureV1(value);
|
|
1822
|
+
return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true };
|
|
1823
|
+
}
|
|
1824
|
+
function verifyOhDependencyClosureAgainstV1(value, expected) {
|
|
1825
|
+
const binding = parseOhStoreBindingV1(expected.binding);
|
|
1826
|
+
const head = parseOhHeadV1(expected.head);
|
|
1827
|
+
if (binding === null || head === null)
|
|
1828
|
+
return { ok: false, reason: "invalid-expectation" };
|
|
1829
|
+
const closure = parseOhDependencyClosureV1(value);
|
|
1830
|
+
if (closure === null)
|
|
1831
|
+
return { ok: false, reason: "invalid-closure" };
|
|
1832
|
+
if (closure.binding.bindingSha256 !== binding.bindingSha256)
|
|
1833
|
+
return { ok: false, reason: "binding-mismatch" };
|
|
1834
|
+
if (canonicalJson(closure.head) !== canonicalJson(head))
|
|
1835
|
+
return { ok: false, reason: "head-mismatch" };
|
|
1836
|
+
return { closure, ok: true, verification: "expected-authority-and-head" };
|
|
1837
|
+
}
|
|
1838
|
+
function createOhSpacePurgeReceiptV1(input) {
|
|
1839
|
+
const binding = parseOhStoreBindingV1(input.binding);
|
|
1840
|
+
const priorHead = parseOhHeadV1(input.priorHead);
|
|
1841
|
+
const purgedAt = parseCanonicalInstantV1(input.purgedAt);
|
|
1842
|
+
if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) {
|
|
1843
|
+
throw new OhProfileError("Only a bound working realm can produce a purge receipt.");
|
|
1844
|
+
}
|
|
1845
|
+
const payload = {
|
|
1846
|
+
bindingSha256: binding.bindingSha256,
|
|
1847
|
+
priorHead,
|
|
1848
|
+
purgedAt,
|
|
1849
|
+
spaceId: binding.spaceId,
|
|
1850
|
+
v: 1
|
|
1851
|
+
};
|
|
1852
|
+
return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) });
|
|
1853
|
+
}
|
|
1854
|
+
function parseOhSpacePurgeReceiptV1(value) {
|
|
1855
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1856
|
+
"bindingSha256",
|
|
1857
|
+
"priorHead",
|
|
1858
|
+
"purgedAt",
|
|
1859
|
+
"receiptSha256",
|
|
1860
|
+
"spaceId",
|
|
1861
|
+
"v"
|
|
1862
|
+
]) || value.v !== 1)
|
|
1863
|
+
return null;
|
|
1864
|
+
const bindingSha256 = parseSha256Hex(value.bindingSha256);
|
|
1865
|
+
const priorHead = parseOhHeadV1(value.priorHead);
|
|
1866
|
+
const purgedAt = parseCanonicalInstantV1(value.purgedAt);
|
|
1867
|
+
const receiptSha256 = parseSha256Hex(value.receiptSha256);
|
|
1868
|
+
const spaceId = safeCode(value.spaceId);
|
|
1869
|
+
if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null)
|
|
1870
|
+
return null;
|
|
1871
|
+
const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 };
|
|
1872
|
+
return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
class OhSemanticBundleIngressV1 {
|
|
1876
|
+
#codecs;
|
|
1877
|
+
#store;
|
|
1878
|
+
constructor(store, codecs) {
|
|
1879
|
+
this.#store = store;
|
|
1880
|
+
this.#codecs = codecs.seal();
|
|
1881
|
+
}
|
|
1882
|
+
async commit(value) {
|
|
1883
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
1884
|
+
"actorId",
|
|
1885
|
+
"expectedHead",
|
|
1886
|
+
"instant",
|
|
1887
|
+
"operationId",
|
|
1888
|
+
"puts",
|
|
1889
|
+
"tombstones",
|
|
1890
|
+
"v"
|
|
1891
|
+
]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) {
|
|
1892
|
+
throw new TypeError("Invalid semantic bundle.");
|
|
1893
|
+
}
|
|
1894
|
+
const actorId = safeCode(value.actorId);
|
|
1895
|
+
const operationId = safeCode(value.operationId);
|
|
1896
|
+
const expected = value.expectedHead;
|
|
1897
|
+
const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant);
|
|
1898
|
+
if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null)
|
|
1899
|
+
throw new TypeError("Invalid semantic bundle identity.");
|
|
1900
|
+
const changes = [];
|
|
1901
|
+
for (const item of value.puts) {
|
|
1902
|
+
if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) {
|
|
1903
|
+
throw new TypeError("Invalid semantic bundle put.");
|
|
1904
|
+
}
|
|
1905
|
+
const parsed = this.#codecs.parseRequired(item.kind, item.value);
|
|
1906
|
+
if (parsed === null)
|
|
1907
|
+
throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`);
|
|
1908
|
+
const record = createKnowledgeGraphRecordV1({
|
|
1909
|
+
dependencies: item.dependencies,
|
|
1910
|
+
key: item.key,
|
|
1911
|
+
kind: item.kind,
|
|
1912
|
+
v: 1,
|
|
1913
|
+
value: parsed
|
|
1914
|
+
});
|
|
1915
|
+
changes.push({ kind: "put", record, v: 1 });
|
|
1916
|
+
}
|
|
1917
|
+
for (const item of value.tombstones) {
|
|
1918
|
+
if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) {
|
|
1919
|
+
throw new TypeError("Invalid semantic bundle tombstone.");
|
|
1920
|
+
}
|
|
1921
|
+
const priorSha256 = parseSha256Hex(item.priorSha256);
|
|
1922
|
+
if (typeof item.key !== "string" || priorSha256 === null)
|
|
1923
|
+
throw new TypeError("Invalid semantic bundle tombstone.");
|
|
1924
|
+
changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 });
|
|
1925
|
+
}
|
|
1926
|
+
const canonical = canonicalKnowledgeGraphChangesV1(changes);
|
|
1927
|
+
return await this.#store.commit({
|
|
1928
|
+
actorId,
|
|
1929
|
+
changes: canonical,
|
|
1930
|
+
expectedHead: {
|
|
1931
|
+
generation: expected.generation,
|
|
1932
|
+
operationSha256: expected.operationSha256
|
|
1933
|
+
},
|
|
1934
|
+
...typeof instant === "string" ? { instant } : {},
|
|
1935
|
+
operationId
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/sqlite/driver.ts
|
|
1941
|
+
import { existsSync } from "fs";
|
|
1942
|
+
import { Database } from "bun:sqlite";
|
|
1943
|
+
|
|
1944
|
+
// src/sqlite/runtime.ts
|
|
1945
|
+
var MACOS_SQLITE_LIBRARY_CANDIDATES = Object.freeze([
|
|
1946
|
+
"/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib",
|
|
1947
|
+
"/usr/local/opt/sqlite/lib/libsqlite3.dylib"
|
|
1948
|
+
]);
|
|
1949
|
+
function macosSqliteLibraryCandidates() {
|
|
1950
|
+
return MACOS_SQLITE_LIBRARY_CANDIDATES;
|
|
1951
|
+
}
|
|
1952
|
+
function createOhSqliteRuntime(dependencies) {
|
|
1953
|
+
let customLibrary = null;
|
|
1954
|
+
if (dependencies.platform === "darwin") {
|
|
1955
|
+
for (const candidate of macosSqliteLibraryCandidates()) {
|
|
1956
|
+
if (!dependencies.exists(candidate))
|
|
1957
|
+
continue;
|
|
1958
|
+
try {
|
|
1959
|
+
if (!dependencies.setCustomSQLite(candidate))
|
|
1960
|
+
continue;
|
|
1961
|
+
customLibrary = candidate;
|
|
1962
|
+
break;
|
|
1963
|
+
} catch {}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return Object.freeze({
|
|
1967
|
+
customLibrary,
|
|
1968
|
+
open: (path) => dependencies.open(path, { create: true, strict: true })
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/sqlite/driver.ts
|
|
1973
|
+
var SQLITE_RUNTIME = createOhSqliteRuntime({
|
|
1974
|
+
exists: existsSync,
|
|
1975
|
+
open: (path, options) => new Database(path, options),
|
|
1976
|
+
platform: process.platform,
|
|
1977
|
+
setCustomSQLite: (path) => Database.setCustomSQLite(path)
|
|
1978
|
+
});
|
|
1979
|
+
function openOhSqliteDatabase(path) {
|
|
1980
|
+
const database = SQLITE_RUNTIME.open(path);
|
|
1981
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
1982
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
1983
|
+
database.exec("PRAGMA synchronous = NORMAL");
|
|
1984
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1985
|
+
database.exec("PRAGMA trusted_schema = OFF");
|
|
1986
|
+
return database;
|
|
1987
|
+
}
|
|
1988
|
+
function withImmediateTransaction(database, work) {
|
|
1989
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1990
|
+
try {
|
|
1991
|
+
const result = work();
|
|
1992
|
+
database.exec("COMMIT");
|
|
1993
|
+
return result;
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
try {
|
|
1996
|
+
database.exec("ROLLBACK");
|
|
1997
|
+
} catch {}
|
|
1998
|
+
throw error;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
function withReadTransaction(database, work) {
|
|
2002
|
+
database.exec("BEGIN");
|
|
2003
|
+
try {
|
|
2004
|
+
const result = work();
|
|
2005
|
+
database.exec("COMMIT");
|
|
2006
|
+
return result;
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
try {
|
|
2009
|
+
database.exec("ROLLBACK");
|
|
2010
|
+
} catch {}
|
|
2011
|
+
throw error;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// src/sqlite/migrations.ts
|
|
2016
|
+
var OH_SQLITE_SCHEMA_VERSION = 2;
|
|
2017
|
+
var OH_SQLITE_MIGRATIONS = Object.freeze([
|
|
2018
|
+
Object.freeze({
|
|
2019
|
+
name: "0001_oh_core",
|
|
2020
|
+
version: 1,
|
|
2021
|
+
sql: `
|
|
2022
|
+
CREATE TABLE oh_contracts (
|
|
2023
|
+
contract_id TEXT PRIMARY KEY,
|
|
2024
|
+
contract_sha256 TEXT NOT NULL CHECK(length(contract_sha256) = 64),
|
|
2025
|
+
manifest_json TEXT NOT NULL CHECK(json_valid(manifest_json)),
|
|
2026
|
+
created_at TEXT NOT NULL
|
|
2027
|
+
) STRICT;
|
|
2028
|
+
|
|
2029
|
+
CREATE TABLE oh_spaces (
|
|
2030
|
+
space_id TEXT PRIMARY KEY,
|
|
2031
|
+
contract_id TEXT NOT NULL REFERENCES oh_contracts(contract_id),
|
|
2032
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
2033
|
+
head_operation_sha256 TEXT CHECK(head_operation_sha256 IS NULL OR length(head_operation_sha256) = 64),
|
|
2034
|
+
graph_revision_sha256 TEXT CHECK(graph_revision_sha256 IS NULL OR length(graph_revision_sha256) = 64),
|
|
2035
|
+
records_sha256 TEXT NOT NULL CHECK(length(records_sha256) = 64),
|
|
2036
|
+
sequence INTEGER NOT NULL CHECK(sequence >= 0),
|
|
2037
|
+
created_at TEXT NOT NULL,
|
|
2038
|
+
updated_at TEXT NOT NULL,
|
|
2039
|
+
CHECK(generation = sequence),
|
|
2040
|
+
CHECK((sequence = 0) = (head_operation_sha256 IS NULL)),
|
|
2041
|
+
CHECK((sequence = 0) = (graph_revision_sha256 IS NULL))
|
|
2042
|
+
) STRICT;
|
|
2043
|
+
|
|
2044
|
+
CREATE TABLE oh_operations (
|
|
2045
|
+
operation_sha256 TEXT PRIMARY KEY CHECK(length(operation_sha256) = 64),
|
|
2046
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2047
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
2048
|
+
operation_id TEXT NOT NULL,
|
|
2049
|
+
parent_operation_sha256 TEXT CHECK(parent_operation_sha256 IS NULL OR length(parent_operation_sha256) = 64),
|
|
2050
|
+
graph_revision_sha256 TEXT NOT NULL CHECK(length(graph_revision_sha256) = 64),
|
|
2051
|
+
records_sha256 TEXT NOT NULL CHECK(length(records_sha256) = 64),
|
|
2052
|
+
operation_json TEXT NOT NULL CHECK(json_valid(operation_json)),
|
|
2053
|
+
instant TEXT NOT NULL,
|
|
2054
|
+
UNIQUE(space_id, sequence),
|
|
2055
|
+
UNIQUE(space_id, operation_id)
|
|
2056
|
+
) STRICT;
|
|
2057
|
+
|
|
2058
|
+
CREATE TABLE oh_operation_records (
|
|
2059
|
+
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
2060
|
+
ordinal INTEGER NOT NULL CHECK(ordinal >= 0),
|
|
2061
|
+
record_key TEXT NOT NULL,
|
|
2062
|
+
change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')),
|
|
2063
|
+
record_sha256 TEXT CHECK(record_sha256 IS NULL OR length(record_sha256) = 64),
|
|
2064
|
+
PRIMARY KEY(operation_sha256, ordinal),
|
|
2065
|
+
UNIQUE(operation_sha256, record_key)
|
|
2066
|
+
) STRICT;
|
|
2067
|
+
|
|
2068
|
+
CREATE TABLE oh_records (
|
|
2069
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2070
|
+
record_key TEXT NOT NULL,
|
|
2071
|
+
kind TEXT NOT NULL,
|
|
2072
|
+
record_sha256 TEXT NOT NULL CHECK(length(record_sha256) = 64),
|
|
2073
|
+
record_json TEXT NOT NULL CHECK(json_valid(record_json)),
|
|
2074
|
+
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
2075
|
+
sequence INTEGER NOT NULL CHECK(sequence > 0),
|
|
2076
|
+
PRIMARY KEY(space_id, record_key)
|
|
2077
|
+
) STRICT;
|
|
2078
|
+
|
|
2079
|
+
CREATE TABLE oh_dependencies (
|
|
2080
|
+
space_id TEXT NOT NULL,
|
|
2081
|
+
record_key TEXT NOT NULL,
|
|
2082
|
+
dependency_key TEXT NOT NULL,
|
|
2083
|
+
PRIMARY KEY(space_id, record_key, dependency_key),
|
|
2084
|
+
FOREIGN KEY(space_id, record_key) REFERENCES oh_records(space_id, record_key) ON DELETE CASCADE,
|
|
2085
|
+
FOREIGN KEY(space_id, dependency_key) REFERENCES oh_records(space_id, record_key)
|
|
2086
|
+
) STRICT;
|
|
2087
|
+
|
|
2088
|
+
CREATE TABLE oh_sync_outbox (
|
|
2089
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2090
|
+
sequence INTEGER NOT NULL,
|
|
2091
|
+
operation_sha256 TEXT NOT NULL REFERENCES oh_operations(operation_sha256),
|
|
2092
|
+
PRIMARY KEY(space_id, sequence),
|
|
2093
|
+
UNIQUE(operation_sha256)
|
|
2094
|
+
) STRICT;
|
|
2095
|
+
|
|
2096
|
+
CREATE TABLE oh_sync_state (
|
|
2097
|
+
remote_id TEXT NOT NULL,
|
|
2098
|
+
space_id TEXT NOT NULL REFERENCES oh_spaces(space_id),
|
|
2099
|
+
pulled_sequence INTEGER NOT NULL CHECK(pulled_sequence >= 0),
|
|
2100
|
+
pushed_sequence INTEGER NOT NULL CHECK(pushed_sequence >= 0),
|
|
2101
|
+
remote_head_sha256 TEXT CHECK(remote_head_sha256 IS NULL OR length(remote_head_sha256) = 64),
|
|
2102
|
+
updated_at TEXT NOT NULL,
|
|
2103
|
+
PRIMARY KEY(remote_id, space_id)
|
|
2104
|
+
) STRICT;
|
|
2105
|
+
|
|
2106
|
+
CREATE TABLE oh_search_documents (
|
|
2107
|
+
space_id TEXT NOT NULL,
|
|
2108
|
+
record_key TEXT NOT NULL,
|
|
2109
|
+
record_sha256 TEXT NOT NULL CHECK(length(record_sha256) = 64),
|
|
2110
|
+
text TEXT NOT NULL,
|
|
2111
|
+
PRIMARY KEY(space_id, record_key),
|
|
2112
|
+
FOREIGN KEY(space_id, record_key) REFERENCES oh_records(space_id, record_key) ON DELETE CASCADE
|
|
2113
|
+
) STRICT;
|
|
2114
|
+
|
|
2115
|
+
CREATE VIRTUAL TABLE oh_search_fts USING fts5(
|
|
2116
|
+
space_id UNINDEXED,
|
|
2117
|
+
record_key UNINDEXED,
|
|
2118
|
+
text,
|
|
2119
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
2120
|
+
);
|
|
2121
|
+
|
|
2122
|
+
CREATE INDEX oh_operations_space_sequence ON oh_operations(space_id, sequence);
|
|
2123
|
+
CREATE INDEX oh_records_space_kind ON oh_records(space_id, kind, record_key);
|
|
2124
|
+
CREATE INDEX oh_dependencies_dependency ON oh_dependencies(space_id, dependency_key);
|
|
2125
|
+
`
|
|
2126
|
+
}),
|
|
2127
|
+
Object.freeze({
|
|
2128
|
+
name: "0002_store_realms",
|
|
2129
|
+
version: 2,
|
|
2130
|
+
sql: `
|
|
2131
|
+
CREATE TABLE oh_space_bindings (
|
|
2132
|
+
space_id TEXT PRIMARY KEY REFERENCES oh_spaces(space_id),
|
|
2133
|
+
realm_id TEXT NOT NULL,
|
|
2134
|
+
profile_id TEXT NOT NULL,
|
|
2135
|
+
profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')),
|
|
2136
|
+
profile_sha256 TEXT NOT NULL CHECK(length(profile_sha256) = 64),
|
|
2137
|
+
binding_sha256 TEXT NOT NULL UNIQUE CHECK(length(binding_sha256) = 64),
|
|
2138
|
+
binding_json TEXT NOT NULL CHECK(json_valid(binding_json)),
|
|
2139
|
+
created_at TEXT NOT NULL
|
|
2140
|
+
) STRICT;
|
|
2141
|
+
|
|
2142
|
+
CREATE TABLE oh_space_purges (
|
|
2143
|
+
space_id TEXT PRIMARY KEY,
|
|
2144
|
+
binding_sha256 TEXT NOT NULL CHECK(length(binding_sha256) = 64),
|
|
2145
|
+
prior_operation_sha256 TEXT CHECK(prior_operation_sha256 IS NULL OR length(prior_operation_sha256) = 64),
|
|
2146
|
+
prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0),
|
|
2147
|
+
purged_at TEXT NOT NULL,
|
|
2148
|
+
receipt_sha256 TEXT NOT NULL UNIQUE CHECK(length(receipt_sha256) = 64),
|
|
2149
|
+
receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json))
|
|
2150
|
+
) STRICT;
|
|
2151
|
+
`
|
|
2152
|
+
})
|
|
2153
|
+
]);
|
|
2154
|
+
function applyOhSqliteMigrations(database) {
|
|
2155
|
+
database.exec(`CREATE TABLE IF NOT EXISTS oh_migrations (
|
|
2156
|
+
version INTEGER PRIMARY KEY,
|
|
2157
|
+
name TEXT NOT NULL UNIQUE,
|
|
2158
|
+
migration_sha256 TEXT NOT NULL CHECK(length(migration_sha256) = 64),
|
|
2159
|
+
applied_at TEXT NOT NULL
|
|
2160
|
+
) STRICT`);
|
|
2161
|
+
const select = database.query("SELECT name, migration_sha256 FROM oh_migrations WHERE version = ?");
|
|
2162
|
+
const insert = database.query("INSERT INTO oh_migrations(version, name, migration_sha256, applied_at) VALUES (?, ?, ?, ?)");
|
|
2163
|
+
for (const migration of OH_SQLITE_MIGRATIONS) {
|
|
2164
|
+
const digest = sha256Hex(migration.sql);
|
|
2165
|
+
database.exec("BEGIN IMMEDIATE");
|
|
2166
|
+
try {
|
|
2167
|
+
const existing = select.get(migration.version);
|
|
2168
|
+
if (existing !== null) {
|
|
2169
|
+
if (existing.name !== migration.name || existing.migration_sha256 !== digest) {
|
|
2170
|
+
throw new Error(`SQLite migration ${migration.version} does not match the applied migration.`);
|
|
2171
|
+
}
|
|
2172
|
+
} else {
|
|
2173
|
+
database.exec(migration.sql);
|
|
2174
|
+
insert.run(migration.version, migration.name, digest, canonicalNow());
|
|
2175
|
+
}
|
|
2176
|
+
database.exec("COMMIT");
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
try {
|
|
2179
|
+
database.exec("ROLLBACK");
|
|
2180
|
+
} catch {}
|
|
2181
|
+
throw error;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/sqlite/store.ts
|
|
2187
|
+
var EMPTY_RECORDS_SHA2562 = canonicalSha256([]);
|
|
2188
|
+
var OPERATION_COLUMNS = `operation_sha256, space_id, sequence, operation_id,
|
|
2189
|
+
parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`;
|
|
2190
|
+
var BINDING_COLUMNS = `space_id, realm_id, profile_id, profile_kind, profile_sha256,
|
|
2191
|
+
binding_sha256, binding_json`;
|
|
2192
|
+
var PURGE_COLUMNS = `space_id, binding_sha256, prior_operation_sha256, prior_sequence,
|
|
2193
|
+
purged_at, receipt_sha256, receipt_json`;
|
|
2194
|
+
function parseStoredOperationRow(row, expected = {}) {
|
|
2195
|
+
let value;
|
|
2196
|
+
try {
|
|
2197
|
+
value = JSON.parse(row.operation_json);
|
|
2198
|
+
} catch {
|
|
2199
|
+
throw new OhIntegrityError("A stored operation is not JSON.");
|
|
2200
|
+
}
|
|
2201
|
+
const operation = parseOhOperationV1(value);
|
|
2202
|
+
if (operation === null || canonicalJson(operation) !== row.operation_json || row.operation_sha256 !== operation.operationSha256 || row.space_id !== operation.spaceId || row.sequence !== operation.sequence || row.operation_id !== operation.operationId || row.parent_operation_sha256 !== operation.parentOperationSha256 || row.graph_revision_sha256 !== operation.graphRevisionSha256 || row.records_sha256 !== operation.recordsSha256 || row.instant !== operation.instant || expected.spaceId !== undefined && operation.spaceId !== expected.spaceId || expected.operationId !== undefined && operation.operationId !== expected.operationId || expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256) {
|
|
2203
|
+
throw new OhIntegrityError("Stored operation columns do not match their canonical envelope.");
|
|
2204
|
+
}
|
|
2205
|
+
return operation;
|
|
2206
|
+
}
|
|
2207
|
+
function parseStoredBindingRow(row, expectedSpaceId) {
|
|
2208
|
+
let value;
|
|
2209
|
+
try {
|
|
2210
|
+
value = JSON.parse(row.binding_json);
|
|
2211
|
+
} catch {
|
|
2212
|
+
throw new OhIntegrityError("A store binding is not JSON.");
|
|
2213
|
+
}
|
|
2214
|
+
const binding = parseOhStoreBindingV1(value);
|
|
2215
|
+
if (binding === null || canonicalJson(binding) !== row.binding_json || binding.spaceId !== expectedSpaceId || row.space_id !== binding.spaceId || row.realm_id !== binding.realmId || row.profile_id !== binding.profile.profileId || row.profile_kind !== binding.profile.profileKind || row.profile_sha256 !== binding.profile.profileSha256 || row.binding_sha256 !== binding.bindingSha256) {
|
|
2216
|
+
throw new OhIntegrityError("Stored binding columns do not match their canonical envelope.");
|
|
2217
|
+
}
|
|
2218
|
+
return binding;
|
|
2219
|
+
}
|
|
2220
|
+
function parseStoredPurgeRow(row, expectedSpaceId) {
|
|
2221
|
+
let value;
|
|
2222
|
+
try {
|
|
2223
|
+
value = JSON.parse(row.receipt_json);
|
|
2224
|
+
} catch {
|
|
2225
|
+
throw new OhIntegrityError("A purge receipt is not JSON.");
|
|
2226
|
+
}
|
|
2227
|
+
const receipt = parseOhSpacePurgeReceiptV1(value);
|
|
2228
|
+
if (receipt === null || canonicalJson(receipt) !== row.receipt_json || receipt.spaceId !== expectedSpaceId || row.space_id !== receipt.spaceId || row.binding_sha256 !== receipt.bindingSha256 || row.prior_operation_sha256 !== receipt.priorHead.operationSha256 || row.prior_sequence !== receipt.priorHead.sequence || row.purged_at !== receipt.purgedAt || row.receipt_sha256 !== receipt.receiptSha256) {
|
|
2229
|
+
throw new OhIntegrityError("Stored purge columns do not match their canonical receipt.");
|
|
2230
|
+
}
|
|
2231
|
+
return receipt;
|
|
2232
|
+
}
|
|
2233
|
+
function parseHead(row) {
|
|
2234
|
+
const operationSha256 = row.head_operation_sha256 === null ? null : parseSha256Hex(row.head_operation_sha256);
|
|
2235
|
+
const graphRevisionSha256 = row.graph_revision_sha256 === null ? null : parseSha256Hex(row.graph_revision_sha256);
|
|
2236
|
+
const recordsSha256 = parseSha256Hex(row.records_sha256);
|
|
2237
|
+
if (row.head_operation_sha256 !== null && operationSha256 === null || row.graph_revision_sha256 !== null && graphRevisionSha256 === null || recordsSha256 === null) {
|
|
2238
|
+
throw new OhIntegrityError("The stored space head contains an invalid digest.");
|
|
2239
|
+
}
|
|
2240
|
+
return {
|
|
2241
|
+
generation: row.generation,
|
|
2242
|
+
graphRevisionSha256,
|
|
2243
|
+
operationSha256,
|
|
2244
|
+
recordsSha256,
|
|
2245
|
+
sequence: row.sequence,
|
|
2246
|
+
v: 1
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
function extractSearchText(value, maximumBytes = 1024 * 1024) {
|
|
2250
|
+
const parts = [];
|
|
2251
|
+
let bytes = 0;
|
|
2252
|
+
const visit = (candidate, depth) => {
|
|
2253
|
+
if (depth > 32 || bytes >= maximumBytes)
|
|
2254
|
+
return;
|
|
2255
|
+
if (typeof candidate === "string") {
|
|
2256
|
+
const remaining = maximumBytes - bytes;
|
|
2257
|
+
const text = Buffer.from(candidate, "utf8").subarray(0, remaining).toString("utf8");
|
|
2258
|
+
if (text.length > 0) {
|
|
2259
|
+
parts.push(text);
|
|
2260
|
+
bytes += Buffer.byteLength(text, "utf8") + 1;
|
|
2261
|
+
}
|
|
2262
|
+
} else if (typeof candidate === "number" || typeof candidate === "boolean") {
|
|
2263
|
+
const text = String(candidate);
|
|
2264
|
+
parts.push(text);
|
|
2265
|
+
bytes += text.length + 1;
|
|
2266
|
+
} else if (Array.isArray(candidate)) {
|
|
2267
|
+
for (const item of candidate)
|
|
2268
|
+
visit(item, depth + 1);
|
|
2269
|
+
} else if (candidate !== null) {
|
|
2270
|
+
for (const [key, item] of Object.entries(candidate)) {
|
|
2271
|
+
parts.push(key);
|
|
2272
|
+
bytes += key.length + 1;
|
|
2273
|
+
visit(item, depth + 1);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
visit(value, 0);
|
|
2278
|
+
return parts.join(" ").normalize("NFC");
|
|
2279
|
+
}
|
|
2280
|
+
function normalizeLimit(value, fallback = 50, maximum = 1000) {
|
|
2281
|
+
if (value === undefined)
|
|
2282
|
+
return fallback;
|
|
2283
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
2284
|
+
throw new RangeError(`limit must be an integer from 1 through ${maximum}.`);
|
|
2285
|
+
}
|
|
2286
|
+
return value;
|
|
2287
|
+
}
|
|
2288
|
+
function ftsQuery(value) {
|
|
2289
|
+
const normalized = boundedText(value.normalize("NFC"), 4096);
|
|
2290
|
+
if (normalized === null)
|
|
2291
|
+
return null;
|
|
2292
|
+
const tokens = normalized.toLocaleLowerCase("en-US").match(/[\p{L}\p{N}][\p{L}\p{N}_-]{0,63}/gu)?.slice(0, 16) ?? [];
|
|
2293
|
+
return tokens.length === 0 ? null : tokens.map((token) => `"${token.replaceAll('"', '""')}"`).join(" OR ");
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
class OhSqliteStore {
|
|
2297
|
+
database;
|
|
2298
|
+
spaceId;
|
|
2299
|
+
#closed = false;
|
|
2300
|
+
constructor(options = {}) {
|
|
2301
|
+
const spaceId = safeCode(options.spaceId ?? "default");
|
|
2302
|
+
if (spaceId === null)
|
|
2303
|
+
throw new TypeError("Invalid space ID.");
|
|
2304
|
+
if (options.database !== undefined && options.path !== undefined) {
|
|
2305
|
+
throw new TypeError("Pass either a database or a path, not both.");
|
|
2306
|
+
}
|
|
2307
|
+
const path = options.path ?? "oh.sqlite";
|
|
2308
|
+
if (options.database === undefined && path !== ":memory:")
|
|
2309
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2310
|
+
this.database = options.database ?? openOhSqliteDatabase(path);
|
|
2311
|
+
this.spaceId = spaceId;
|
|
2312
|
+
applyOhSqliteMigrations(this.database);
|
|
2313
|
+
this.#registerContract();
|
|
2314
|
+
this.ensureSpace();
|
|
2315
|
+
}
|
|
2316
|
+
#assertOpen() {
|
|
2317
|
+
if (this.#closed)
|
|
2318
|
+
throw new Error("The Oh store is closed.");
|
|
2319
|
+
}
|
|
2320
|
+
#assertOperationReplication() {
|
|
2321
|
+
const binding = this.binding();
|
|
2322
|
+
if (binding !== null && !binding.profile.capabilities.operationReplication) {
|
|
2323
|
+
throw new OhProfileError("This bound store profile forbids operation replication.");
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
#registerContract() {
|
|
2327
|
+
const manifestJson = canonicalJson(OH_CONTRACT_MANIFEST_V1);
|
|
2328
|
+
this.database.query(`INSERT INTO oh_contracts(contract_id, contract_sha256, manifest_json, created_at)
|
|
2329
|
+
VALUES (?, ?, ?, ?) ON CONFLICT(contract_id) DO NOTHING`).run(OH_CONTRACT_ID_V1, OH_CONTRACT_MANIFEST_V1.contractSha256, manifestJson, canonicalNow());
|
|
2330
|
+
const existing = this.database.query("SELECT contract_sha256, manifest_json FROM oh_contracts WHERE contract_id = ?").get(OH_CONTRACT_ID_V1);
|
|
2331
|
+
if (existing === null || existing.contract_sha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || existing.manifest_json !== manifestJson) {
|
|
2332
|
+
throw new OhIntegrityError("The stored contract manifest differs from this runtime.");
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
ensureSpace() {
|
|
2336
|
+
this.#assertOpen();
|
|
2337
|
+
return withImmediateTransaction(this.database, () => {
|
|
2338
|
+
const purged = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId);
|
|
2339
|
+
if (purged !== null) {
|
|
2340
|
+
throw new OhPurgedSpaceError(parseStoredPurgeRow(purged, this.spaceId));
|
|
2341
|
+
}
|
|
2342
|
+
const now = canonicalNow();
|
|
2343
|
+
this.database.query(`INSERT INTO oh_spaces(
|
|
2344
|
+
space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256,
|
|
2345
|
+
records_sha256, sequence, created_at, updated_at
|
|
2346
|
+
) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA2562, now, now);
|
|
2347
|
+
return this.head();
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
bind(bindingValue) {
|
|
2351
|
+
this.#assertOpen();
|
|
2352
|
+
const binding = parseOhStoreBindingV1(bindingValue);
|
|
2353
|
+
if (binding === null || binding.spaceId !== this.spaceId) {
|
|
2354
|
+
throw new OhProfileError("The store binding does not identify this space.");
|
|
2355
|
+
}
|
|
2356
|
+
const bindingJson = canonicalJson(binding);
|
|
2357
|
+
this.database.query(`INSERT INTO oh_space_bindings(
|
|
2358
|
+
space_id, realm_id, profile_id, profile_kind, profile_sha256,
|
|
2359
|
+
binding_sha256, binding_json, created_at
|
|
2360
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, binding.realmId, binding.profile.profileId, binding.profile.profileKind, binding.profile.profileSha256, binding.bindingSha256, bindingJson, canonicalNow());
|
|
2361
|
+
const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId);
|
|
2362
|
+
if (row === null)
|
|
2363
|
+
throw new OhIntegrityError("The persisted store binding disappeared.");
|
|
2364
|
+
const persisted = parseStoredBindingRow(row, this.spaceId);
|
|
2365
|
+
if (canonicalJson(persisted) !== bindingJson) {
|
|
2366
|
+
throw new OhProfileError("The space is already bound to a different realm or profile.");
|
|
2367
|
+
}
|
|
2368
|
+
return binding;
|
|
2369
|
+
}
|
|
2370
|
+
binding() {
|
|
2371
|
+
this.#assertOpen();
|
|
2372
|
+
const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId);
|
|
2373
|
+
if (row === null)
|
|
2374
|
+
return null;
|
|
2375
|
+
return parseStoredBindingRow(row, this.spaceId);
|
|
2376
|
+
}
|
|
2377
|
+
head() {
|
|
2378
|
+
this.#assertOpen();
|
|
2379
|
+
const row = this.database.query(`SELECT generation, graph_revision_sha256,
|
|
2380
|
+
head_operation_sha256, records_sha256, sequence FROM oh_spaces WHERE space_id = ?`).get(this.spaceId);
|
|
2381
|
+
if (row === null)
|
|
2382
|
+
throw new OhIntegrityError("The requested space does not exist.");
|
|
2383
|
+
return parseHead(row);
|
|
2384
|
+
}
|
|
2385
|
+
#loadRecords() {
|
|
2386
|
+
const rows = this.database.query("SELECT record_json FROM oh_records WHERE space_id = ? ORDER BY record_key").all(this.spaceId);
|
|
2387
|
+
const records = new Map;
|
|
2388
|
+
for (const row of rows) {
|
|
2389
|
+
let value;
|
|
2390
|
+
try {
|
|
2391
|
+
value = JSON.parse(row.record_json);
|
|
2392
|
+
} catch {
|
|
2393
|
+
throw new OhIntegrityError("A stored record is not JSON.");
|
|
2394
|
+
}
|
|
2395
|
+
if (canonicalJson(value) !== row.record_json)
|
|
2396
|
+
throw new OhIntegrityError("A stored record is not canonical JSON.");
|
|
2397
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
2398
|
+
if (record === null || records.has(record.key))
|
|
2399
|
+
throw new OhIntegrityError("A stored graph record is invalid.");
|
|
2400
|
+
records.set(record.key, record);
|
|
2401
|
+
}
|
|
2402
|
+
return records;
|
|
2403
|
+
}
|
|
2404
|
+
#transition(head, changes, operationId) {
|
|
2405
|
+
const records = this.#loadRecords();
|
|
2406
|
+
for (const change of changes) {
|
|
2407
|
+
if (change.kind === "put") {
|
|
2408
|
+
records.set(change.record.key, change.record);
|
|
2409
|
+
} else {
|
|
2410
|
+
const prior = records.get(change.key);
|
|
2411
|
+
if (prior === undefined || prior.recordSha256 !== change.priorSha256) {
|
|
2412
|
+
throw new OhConflictError(`The prior digest for ${change.key} does not match the current record.`);
|
|
2413
|
+
}
|
|
2414
|
+
records.delete(change.key);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
2418
|
+
throw new RangeError("Graph transition exceeds its record snapshot limit.");
|
|
2419
|
+
}
|
|
2420
|
+
for (const record of records.values()) {
|
|
2421
|
+
for (const dependency of record.dependencies) {
|
|
2422
|
+
if (!records.has(dependency))
|
|
2423
|
+
throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
const refs = [...records.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map(knowledgeGraphRecordRefV1);
|
|
2427
|
+
const recordsSha256 = canonicalSha256(refs);
|
|
2428
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
2429
|
+
changes,
|
|
2430
|
+
operationId,
|
|
2431
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
2432
|
+
recordsSha256,
|
|
2433
|
+
revision: head.sequence + 1
|
|
2434
|
+
});
|
|
2435
|
+
return { graphRevisionSha256, records, recordsSha256 };
|
|
2436
|
+
}
|
|
2437
|
+
#assertCurrentHeadAuthority(head) {
|
|
2438
|
+
const summary = this.database.query(`SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum
|
|
2439
|
+
FROM oh_operations WHERE space_id = ?`).get(this.spaceId);
|
|
2440
|
+
if (summary === null || summary.count !== head.sequence || head.sequence === 0 && (summary.minimum !== null || summary.maximum !== null) || head.sequence > 0 && (summary.minimum !== 1 || summary.maximum !== head.sequence)) {
|
|
2441
|
+
throw new OhIntegrityError("The operation history does not exactly cover the current space head.");
|
|
2442
|
+
}
|
|
2443
|
+
if (head.sequence === 0)
|
|
2444
|
+
return;
|
|
2445
|
+
if (head.operationSha256 === null)
|
|
2446
|
+
throw new OhIntegrityError("A nonempty space head has no operation digest.");
|
|
2447
|
+
const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, head.sequence);
|
|
2448
|
+
if (row === null)
|
|
2449
|
+
throw new OhIntegrityError("The current space head operation is missing.");
|
|
2450
|
+
const operation = parseStoredOperationRow(row, {
|
|
2451
|
+
spaceId: this.spaceId,
|
|
2452
|
+
operationSha256: head.operationSha256
|
|
2453
|
+
});
|
|
2454
|
+
if (operation.sequence !== head.sequence || operation.graphRevisionSha256 !== head.graphRevisionSha256 || operation.recordsSha256 !== head.recordsSha256) {
|
|
2455
|
+
throw new OhIntegrityError("The current space head differs from its canonical operation.");
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
#assertOperationReachable(operation, head) {
|
|
2459
|
+
if (operation.sequence < 1 || operation.sequence > head.sequence) {
|
|
2460
|
+
throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head.");
|
|
2461
|
+
}
|
|
2462
|
+
const rows = this.database.query(`SELECT operation_sha256, parent_operation_sha256, sequence
|
|
2463
|
+
FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence);
|
|
2464
|
+
if (rows.length !== head.sequence - operation.sequence + 1) {
|
|
2465
|
+
throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head.");
|
|
2466
|
+
}
|
|
2467
|
+
let priorSha256 = operation.parentOperationSha256;
|
|
2468
|
+
for (let index = 0;index < rows.length; index += 1) {
|
|
2469
|
+
const row = rows[index];
|
|
2470
|
+
if (row === undefined || row.sequence !== operation.sequence + index || row.parent_operation_sha256 !== priorSha256 || index === 0 && row.operation_sha256 !== operation.operationSha256) {
|
|
2471
|
+
throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain.");
|
|
2472
|
+
}
|
|
2473
|
+
priorSha256 = row.operation_sha256;
|
|
2474
|
+
}
|
|
2475
|
+
if (priorSha256 !== head.operationSha256) {
|
|
2476
|
+
throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest.");
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
#persist(operation) {
|
|
2480
|
+
this.database.query(`INSERT INTO oh_operations(operation_sha256, space_id, sequence,
|
|
2481
|
+
operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256,
|
|
2482
|
+
operation_json, instant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(operation.operationSha256, this.spaceId, operation.sequence, operation.operationId, operation.parentOperationSha256, operation.graphRevisionSha256, operation.recordsSha256, canonicalJson(operation), operation.instant);
|
|
2483
|
+
const changedKeys = operation.changes.map((change) => change.kind === "put" ? change.record.key : change.key);
|
|
2484
|
+
const deleteDependencies = this.database.query("DELETE FROM oh_dependencies WHERE space_id = ? AND record_key = ?");
|
|
2485
|
+
for (const key of changedKeys)
|
|
2486
|
+
deleteDependencies.run(this.spaceId, key);
|
|
2487
|
+
const insertOperationRecord = this.database.query(`INSERT INTO oh_operation_records(
|
|
2488
|
+
operation_sha256, ordinal, record_key, change_kind, record_sha256) VALUES (?, ?, ?, ?, ?)`);
|
|
2489
|
+
const upsertRecord = this.database.query(`INSERT INTO oh_records(space_id, record_key, kind,
|
|
2490
|
+
record_sha256, record_json, operation_sha256, sequence) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2491
|
+
ON CONFLICT(space_id, record_key) DO UPDATE SET kind = excluded.kind,
|
|
2492
|
+
record_sha256 = excluded.record_sha256, record_json = excluded.record_json,
|
|
2493
|
+
operation_sha256 = excluded.operation_sha256, sequence = excluded.sequence`);
|
|
2494
|
+
const deleteRecord = this.database.query("DELETE FROM oh_records WHERE space_id = ? AND record_key = ?");
|
|
2495
|
+
const deleteSearch = this.database.query("DELETE FROM oh_search_documents WHERE space_id = ? AND record_key = ?");
|
|
2496
|
+
const deleteFts = this.database.query("DELETE FROM oh_search_fts WHERE space_id = ? AND record_key = ?");
|
|
2497
|
+
const upsertSearch = this.database.query(`INSERT INTO oh_search_documents(space_id, record_key,
|
|
2498
|
+
record_sha256, text) VALUES (?, ?, ?, ?) ON CONFLICT(space_id, record_key) DO UPDATE SET
|
|
2499
|
+
record_sha256 = excluded.record_sha256, text = excluded.text`);
|
|
2500
|
+
const insertFts = this.database.query("INSERT INTO oh_search_fts(space_id, record_key, text) VALUES (?, ?, ?)");
|
|
2501
|
+
for (const [ordinal, change] of operation.changes.entries()) {
|
|
2502
|
+
const key = change.kind === "put" ? change.record.key : change.key;
|
|
2503
|
+
const digest = change.kind === "put" ? change.record.recordSha256 : change.priorSha256;
|
|
2504
|
+
insertOperationRecord.run(operation.operationSha256, ordinal, key, change.kind, digest);
|
|
2505
|
+
deleteFts.run(this.spaceId, key);
|
|
2506
|
+
deleteSearch.run(this.spaceId, key);
|
|
2507
|
+
if (change.kind === "put") {
|
|
2508
|
+
const recordJson = canonicalJson(change.record);
|
|
2509
|
+
upsertRecord.run(this.spaceId, key, change.record.kind, change.record.recordSha256, recordJson, operation.operationSha256, operation.sequence);
|
|
2510
|
+
const text = `${key} ${change.record.kind} ${extractSearchText(change.record.value)}`;
|
|
2511
|
+
upsertSearch.run(this.spaceId, key, change.record.recordSha256, text);
|
|
2512
|
+
insertFts.run(this.spaceId, key, text);
|
|
2513
|
+
} else
|
|
2514
|
+
deleteRecord.run(this.spaceId, key);
|
|
2515
|
+
}
|
|
2516
|
+
const insertDependency = this.database.query("INSERT INTO oh_dependencies(space_id, record_key, dependency_key) VALUES (?, ?, ?)");
|
|
2517
|
+
for (const change of operation.changes) {
|
|
2518
|
+
if (change.kind === "put") {
|
|
2519
|
+
for (const dependency of change.record.dependencies)
|
|
2520
|
+
insertDependency.run(this.spaceId, change.record.key, dependency);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
this.database.query("INSERT INTO oh_sync_outbox(space_id, sequence, operation_sha256) VALUES (?, ?, ?)").run(this.spaceId, operation.sequence, operation.operationSha256);
|
|
2524
|
+
const updated = this.database.query(`UPDATE oh_spaces SET generation = ?, head_operation_sha256 = ?,
|
|
2525
|
+
graph_revision_sha256 = ?, records_sha256 = ?, sequence = ?, updated_at = ?
|
|
2526
|
+
WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?`).run(operation.sequence, operation.operationSha256, operation.graphRevisionSha256, operation.recordsSha256, operation.sequence, operation.instant, this.spaceId, operation.sequence - 1, operation.parentOperationSha256);
|
|
2527
|
+
if (updated.changes !== 1)
|
|
2528
|
+
throw new OhConflictError("The space head changed while committing.");
|
|
2529
|
+
}
|
|
2530
|
+
commit(input) {
|
|
2531
|
+
this.#assertOpen();
|
|
2532
|
+
const actorId = safeCode(input.actorId);
|
|
2533
|
+
const operationId = safeCode(input.operationId);
|
|
2534
|
+
if (actorId === null || operationId === null)
|
|
2535
|
+
throw new TypeError("Invalid actor or operation ID.");
|
|
2536
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
2537
|
+
if (changes.length === 0 || changes.length > 8192)
|
|
2538
|
+
throw new TypeError("A commit needs 1 through 8192 changes.");
|
|
2539
|
+
return withImmediateTransaction(this.database, () => {
|
|
2540
|
+
const head = this.head();
|
|
2541
|
+
this.#assertCurrentHeadAuthority(head);
|
|
2542
|
+
const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operationId);
|
|
2543
|
+
if (duplicate !== null) {
|
|
2544
|
+
const existing = parseStoredOperationRow(duplicate, { operationId, spaceId: this.spaceId });
|
|
2545
|
+
this.#assertOperationReachable(existing, head);
|
|
2546
|
+
if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) {
|
|
2547
|
+
throw new OhConflictError("The operation ID is already bound to different content.");
|
|
2548
|
+
}
|
|
2549
|
+
return existing;
|
|
2550
|
+
}
|
|
2551
|
+
if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) {
|
|
2552
|
+
throw new OhConflictError("The expected head does not match the current space head.");
|
|
2553
|
+
}
|
|
2554
|
+
const transition = this.#transition(head, changes, operationId);
|
|
2555
|
+
const operation = createOhOperationV1({
|
|
2556
|
+
actorId,
|
|
2557
|
+
changes,
|
|
2558
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
2559
|
+
graphRevisionSha256: transition.graphRevisionSha256,
|
|
2560
|
+
instant: input.instant ?? canonicalNow(),
|
|
2561
|
+
operationId,
|
|
2562
|
+
parentOperationSha256: head.operationSha256,
|
|
2563
|
+
recordsSha256: transition.recordsSha256,
|
|
2564
|
+
sequence: head.sequence + 1,
|
|
2565
|
+
spaceId: this.spaceId,
|
|
2566
|
+
v: 1
|
|
2567
|
+
});
|
|
2568
|
+
this.#persist(operation);
|
|
2569
|
+
return operation;
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
importOperation(value) {
|
|
2573
|
+
this.#assertOpen();
|
|
2574
|
+
this.#assertOperationReplication();
|
|
2575
|
+
const operation = parseOhOperationV1(value);
|
|
2576
|
+
if (operation === null || operation.spaceId !== this.spaceId)
|
|
2577
|
+
throw new OhIntegrityError("Invalid imported operation.");
|
|
2578
|
+
return withImmediateTransaction(this.database, () => {
|
|
2579
|
+
const head = this.head();
|
|
2580
|
+
this.#assertCurrentHeadAuthority(head);
|
|
2581
|
+
const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE operation_sha256 = ?`).get(operation.operationSha256);
|
|
2582
|
+
if (duplicate !== null) {
|
|
2583
|
+
const existing = parseStoredOperationRow(duplicate, {
|
|
2584
|
+
operationSha256: operation.operationSha256,
|
|
2585
|
+
spaceId: this.spaceId
|
|
2586
|
+
});
|
|
2587
|
+
this.#assertOperationReachable(existing, head);
|
|
2588
|
+
if (canonicalJson(existing) !== canonicalJson(operation)) {
|
|
2589
|
+
throw new OhIntegrityError("An operation digest is bound to different bytes.");
|
|
2590
|
+
}
|
|
2591
|
+
return { imported: false, operation };
|
|
2592
|
+
}
|
|
2593
|
+
if (operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) {
|
|
2594
|
+
throw new OhConflictError("The imported operation does not extend the local head.");
|
|
2595
|
+
}
|
|
2596
|
+
const transition = this.#transition(head, operation.changes, operation.operationId);
|
|
2597
|
+
if (transition.recordsSha256 !== operation.recordsSha256 || transition.graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
2598
|
+
throw new OhIntegrityError("The imported operation does not reproduce its declared graph head.");
|
|
2599
|
+
}
|
|
2600
|
+
this.#persist(operation);
|
|
2601
|
+
return { imported: true, operation };
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
exportOperations(afterSequence = 0, limit = 1000) {
|
|
2605
|
+
this.#assertOpen();
|
|
2606
|
+
this.#assertOperationReplication();
|
|
2607
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
|
|
2608
|
+
throw new RangeError("afterSequence must be nonnegative.");
|
|
2609
|
+
const boundedLimit = normalizeLimit(limit);
|
|
2610
|
+
const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations
|
|
2611
|
+
WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`).all(this.spaceId, afterSequence, boundedLimit);
|
|
2612
|
+
return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId }));
|
|
2613
|
+
}
|
|
2614
|
+
#headAt(reference) {
|
|
2615
|
+
const parsed = parseOhHeadRefV1(reference);
|
|
2616
|
+
if (parsed === null)
|
|
2617
|
+
throw new TypeError("Invalid Oh head reference.");
|
|
2618
|
+
if (parsed.sequence === 0)
|
|
2619
|
+
return emptyOhHeadV1();
|
|
2620
|
+
const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, parsed.sequence);
|
|
2621
|
+
if (row === null)
|
|
2622
|
+
throw new OhConflictError("The requested head is not present in this space.");
|
|
2623
|
+
const operation = parseStoredOperationRow(row, { spaceId: this.spaceId });
|
|
2624
|
+
if (operation.spaceId !== this.spaceId || operation.sequence !== parsed.sequence || operation.operationSha256 !== parsed.operationSha256) {
|
|
2625
|
+
throw new OhConflictError("The requested sequence identifies a different operation head.");
|
|
2626
|
+
}
|
|
2627
|
+
return {
|
|
2628
|
+
generation: operation.sequence,
|
|
2629
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
2630
|
+
operationSha256: operation.operationSha256,
|
|
2631
|
+
recordsSha256: operation.recordsSha256,
|
|
2632
|
+
sequence: operation.sequence,
|
|
2633
|
+
v: 1
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
snapshotAtHead(options = {}) {
|
|
2637
|
+
this.#assertOpen();
|
|
2638
|
+
const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot;
|
|
2639
|
+
if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
2640
|
+
throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`);
|
|
2641
|
+
}
|
|
2642
|
+
return withReadTransaction(this.database, () => {
|
|
2643
|
+
const current = this.head();
|
|
2644
|
+
const target = options.head === undefined ? current : this.#headAt(options.head);
|
|
2645
|
+
if (target.sequence > current.sequence)
|
|
2646
|
+
throw new OhConflictError("The requested head is ahead of this space.");
|
|
2647
|
+
const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations
|
|
2648
|
+
WHERE space_id = ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, target.sequence);
|
|
2649
|
+
const operations = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId }));
|
|
2650
|
+
const snapshot = replayOhOperationsV1(this.spaceId, operations, maximumRecords);
|
|
2651
|
+
if (snapshot.head.operationSha256 !== target.operationSha256 || snapshot.head.recordsSha256 !== target.recordsSha256) {
|
|
2652
|
+
throw new OhIntegrityError("Operation replay does not reproduce the requested head.");
|
|
2653
|
+
}
|
|
2654
|
+
return snapshot;
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
changesSince(fromValue, options = {}) {
|
|
2658
|
+
this.#assertOpen();
|
|
2659
|
+
const from = parseOhHeadRefV1(fromValue);
|
|
2660
|
+
if (from === null)
|
|
2661
|
+
throw new TypeError("Invalid change-feed cursor.");
|
|
2662
|
+
const limit = normalizeLimit(options.limit, 100, 1000);
|
|
2663
|
+
return withReadTransaction(this.database, () => {
|
|
2664
|
+
const current = this.head();
|
|
2665
|
+
const fromHead = this.#headAt(from);
|
|
2666
|
+
const through = options.through === undefined ? current : this.#headAt(options.through);
|
|
2667
|
+
if (fromHead.sequence > through.sequence || through.sequence > current.sequence) {
|
|
2668
|
+
throw new OhConflictError("The change-feed bounds do not identify one local history prefix.");
|
|
2669
|
+
}
|
|
2670
|
+
const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations
|
|
2671
|
+
WHERE space_id = ? AND sequence > ? AND sequence <= ?
|
|
2672
|
+
ORDER BY sequence LIMIT ?`).all(this.spaceId, fromHead.sequence, through.sequence, limit + 1);
|
|
2673
|
+
const parsed = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId }));
|
|
2674
|
+
if (parsed.length > limit + 1)
|
|
2675
|
+
throw new OhIntegrityError("The change feed exceeded its requested page bound.");
|
|
2676
|
+
const hasMore = parsed.length > limit;
|
|
2677
|
+
const operations = parsed.slice(0, limit);
|
|
2678
|
+
let prior = fromHead;
|
|
2679
|
+
for (const operation of parsed) {
|
|
2680
|
+
if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) {
|
|
2681
|
+
throw new OhIntegrityError("The change feed contains a gap or fork.");
|
|
2682
|
+
}
|
|
2683
|
+
prior = { operationSha256: operation.operationSha256, sequence: operation.sequence };
|
|
2684
|
+
}
|
|
2685
|
+
if (!hasMore && (prior.sequence !== through.sequence || prior.operationSha256 !== through.operationSha256)) {
|
|
2686
|
+
throw new OhIntegrityError("The change feed does not reach its pinned through head.");
|
|
2687
|
+
}
|
|
2688
|
+
const last = operations.at(-1);
|
|
2689
|
+
const to = last === undefined ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } : { operationSha256: last.operationSha256, sequence: last.sequence };
|
|
2690
|
+
return {
|
|
2691
|
+
from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence },
|
|
2692
|
+
hasMore,
|
|
2693
|
+
operations,
|
|
2694
|
+
through,
|
|
2695
|
+
to,
|
|
2696
|
+
v: 1
|
|
2697
|
+
};
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
exportDependencyClosure(input) {
|
|
2701
|
+
const binding = parseOhStoreBindingV1(input.binding);
|
|
2702
|
+
if (binding === null || binding.spaceId !== this.spaceId || canonicalJson(this.binding()) !== canonicalJson(binding)) {
|
|
2703
|
+
throw new OhProfileError("Dependency closure export requires the exact persisted store binding.");
|
|
2704
|
+
}
|
|
2705
|
+
if (!binding.profile.capabilities.dependencyClosureExport) {
|
|
2706
|
+
throw new OhProfileError("This store profile does not permit dependency-closure export.");
|
|
2707
|
+
}
|
|
2708
|
+
const snapshot = this.snapshotAtHead({
|
|
2709
|
+
...input.head === undefined ? {} : { head: input.head },
|
|
2710
|
+
...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }
|
|
2711
|
+
});
|
|
2712
|
+
return createOhDependencyClosureV1({
|
|
2713
|
+
binding,
|
|
2714
|
+
...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords },
|
|
2715
|
+
roots: input.roots,
|
|
2716
|
+
snapshot
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
get(key) {
|
|
2720
|
+
this.#assertOpen();
|
|
2721
|
+
const parsedKey = safeCode(key, 512);
|
|
2722
|
+
if (parsedKey === null)
|
|
2723
|
+
throw new TypeError("Invalid record key.");
|
|
2724
|
+
const row = this.database.query("SELECT record_json FROM oh_records WHERE space_id = ? AND record_key = ?").get(this.spaceId, parsedKey);
|
|
2725
|
+
if (row === null)
|
|
2726
|
+
return null;
|
|
2727
|
+
const record = parseKnowledgeGraphRecordV1(JSON.parse(row.record_json));
|
|
2728
|
+
if (record === null || canonicalJson(record) !== row.record_json)
|
|
2729
|
+
throw new OhIntegrityError("The stored record is invalid.");
|
|
2730
|
+
return record;
|
|
2731
|
+
}
|
|
2732
|
+
list(options = {}) {
|
|
2733
|
+
this.#assertOpen();
|
|
2734
|
+
const limit = normalizeLimit(options.limit);
|
|
2735
|
+
const rows = options.kind === undefined ? this.database.query("SELECT record_json FROM oh_records WHERE space_id = ? ORDER BY record_key LIMIT ?").all(this.spaceId, limit) : this.database.query("SELECT record_json FROM oh_records WHERE space_id = ? AND kind = ? ORDER BY record_key LIMIT ?").all(this.spaceId, options.kind, limit);
|
|
2736
|
+
return rows.map((row) => {
|
|
2737
|
+
const record = parseKnowledgeGraphRecordV1(JSON.parse(row.record_json));
|
|
2738
|
+
if (record === null || canonicalJson(record) !== row.record_json) {
|
|
2739
|
+
throw new OhIntegrityError("The stored record is invalid.");
|
|
2740
|
+
}
|
|
2741
|
+
return record;
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
snapshotRecords(maximum = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
2745
|
+
this.#assertOpen();
|
|
2746
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
2747
|
+
throw new RangeError(`maximum must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`);
|
|
2748
|
+
}
|
|
2749
|
+
const count = this.database.query("SELECT count(*) AS count FROM oh_records WHERE space_id = ?").get(this.spaceId)?.count ?? 0;
|
|
2750
|
+
if (count > maximum)
|
|
2751
|
+
throw new RangeError(`The graph contains ${count} records, above the requested snapshot bound.`);
|
|
2752
|
+
return this.database.query("SELECT record_json FROM oh_records WHERE space_id = ? ORDER BY record_key").all(this.spaceId).map((row) => {
|
|
2753
|
+
const record = parseKnowledgeGraphRecordV1(JSON.parse(row.record_json));
|
|
2754
|
+
if (record === null || canonicalJson(record) !== row.record_json) {
|
|
2755
|
+
throw new OhIntegrityError("The stored record is invalid.");
|
|
2756
|
+
}
|
|
2757
|
+
return record;
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
log(limit = 50) {
|
|
2761
|
+
this.#assertOpen();
|
|
2762
|
+
const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations
|
|
2763
|
+
WHERE space_id = ? ORDER BY sequence DESC LIMIT ?`).all(this.spaceId, normalizeLimit(limit));
|
|
2764
|
+
return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId }));
|
|
2765
|
+
}
|
|
2766
|
+
searchKeyword(query, limit = 20) {
|
|
2767
|
+
this.#assertOpen();
|
|
2768
|
+
const match = ftsQuery(query);
|
|
2769
|
+
if (match === null)
|
|
2770
|
+
return [];
|
|
2771
|
+
const rows = this.database.query(`SELECT r.record_key,
|
|
2772
|
+
r.kind, r.record_sha256, bm25(oh_search_fts) AS rank,
|
|
2773
|
+
snippet(oh_search_fts, 2, '', '', ' \u2026 ', 24) AS snippet
|
|
2774
|
+
FROM oh_search_fts JOIN oh_records r
|
|
2775
|
+
ON r.space_id = oh_search_fts.space_id AND r.record_key = oh_search_fts.record_key
|
|
2776
|
+
WHERE oh_search_fts MATCH ? AND oh_search_fts.space_id = ?
|
|
2777
|
+
ORDER BY rank, r.record_key LIMIT ?`).all(match, this.spaceId, normalizeLimit(limit, 20, 100));
|
|
2778
|
+
return rows.map((row) => {
|
|
2779
|
+
const digest = parseSha256Hex(row.record_sha256);
|
|
2780
|
+
if (digest === null)
|
|
2781
|
+
throw new OhIntegrityError("Search returned an invalid record digest.");
|
|
2782
|
+
return {
|
|
2783
|
+
key: row.record_key,
|
|
2784
|
+
kind: row.kind,
|
|
2785
|
+
recordSha256: digest,
|
|
2786
|
+
score: 1 / (1 + Math.max(0, row.rank)),
|
|
2787
|
+
snippet: row.snippet,
|
|
2788
|
+
v: 1
|
|
2789
|
+
};
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
syncState(remoteId) {
|
|
2793
|
+
this.#assertOpen();
|
|
2794
|
+
const id = safeCode(remoteId);
|
|
2795
|
+
if (id === null)
|
|
2796
|
+
throw new TypeError("Invalid remote ID.");
|
|
2797
|
+
const row = this.database.query("SELECT pulled_sequence, pushed_sequence, remote_head_sha256 FROM oh_sync_state WHERE remote_id = ? AND space_id = ?").get(id, this.spaceId);
|
|
2798
|
+
const digest = row?.remote_head_sha256 === null || row === null ? null : parseSha256Hex(row.remote_head_sha256);
|
|
2799
|
+
if (row !== null && row.remote_head_sha256 !== null && digest === null)
|
|
2800
|
+
throw new OhIntegrityError("Invalid remote head digest.");
|
|
2801
|
+
return { pulledSequence: row?.pulled_sequence ?? 0, pushedSequence: row?.pushed_sequence ?? 0, remoteHeadSha256: digest };
|
|
2802
|
+
}
|
|
2803
|
+
updateSyncState(remoteId, state) {
|
|
2804
|
+
this.#assertOpen();
|
|
2805
|
+
const id = safeCode(remoteId);
|
|
2806
|
+
if (id === null || !Number.isSafeInteger(state.pulledSequence) || state.pulledSequence < 0 || !Number.isSafeInteger(state.pushedSequence) || state.pushedSequence < 0)
|
|
2807
|
+
throw new TypeError("Invalid sync state.");
|
|
2808
|
+
this.database.query(`INSERT INTO oh_sync_state(remote_id, space_id, pulled_sequence,
|
|
2809
|
+
pushed_sequence, remote_head_sha256, updated_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
2810
|
+
ON CONFLICT(remote_id, space_id) DO UPDATE SET pulled_sequence = excluded.pulled_sequence,
|
|
2811
|
+
pushed_sequence = excluded.pushed_sequence, remote_head_sha256 = excluded.remote_head_sha256,
|
|
2812
|
+
updated_at = excluded.updated_at`).run(id, this.spaceId, state.pulledSequence, state.pushedSequence, state.remoteHeadSha256, canonicalNow());
|
|
2813
|
+
}
|
|
2814
|
+
verifyReplay() {
|
|
2815
|
+
this.#assertOpen();
|
|
2816
|
+
const integrity = this.database.query("PRAGMA integrity_check").get();
|
|
2817
|
+
if (integrity?.integrity_check !== "ok")
|
|
2818
|
+
throw new OhIntegrityError("SQLite integrity_check failed.");
|
|
2819
|
+
const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0;
|
|
2820
|
+
const operations = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? ORDER BY sequence`).all(this.spaceId).map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId }));
|
|
2821
|
+
if (operations.length !== storedCount)
|
|
2822
|
+
throw new OhIntegrityError("Operation count changed during verification.");
|
|
2823
|
+
return this.#verifyOperations(operations);
|
|
2824
|
+
}
|
|
2825
|
+
#verifyOperations(operations) {
|
|
2826
|
+
const records = new Map;
|
|
2827
|
+
const materializedBy = new Map;
|
|
2828
|
+
const operationIds = new Set;
|
|
2829
|
+
let head = {
|
|
2830
|
+
generation: 0,
|
|
2831
|
+
graphRevisionSha256: null,
|
|
2832
|
+
operationSha256: null,
|
|
2833
|
+
recordsSha256: EMPTY_RECORDS_SHA2562,
|
|
2834
|
+
sequence: 0,
|
|
2835
|
+
v: 1
|
|
2836
|
+
};
|
|
2837
|
+
for (const operation of operations) {
|
|
2838
|
+
if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId))
|
|
2839
|
+
throw new OhIntegrityError("Operation replay chain is broken.");
|
|
2840
|
+
operationIds.add(operation.operationId);
|
|
2841
|
+
for (const change of operation.changes) {
|
|
2842
|
+
if (change.kind === "put") {
|
|
2843
|
+
records.set(change.record.key, change.record);
|
|
2844
|
+
materializedBy.set(change.record.key, {
|
|
2845
|
+
operationSha256: operation.operationSha256,
|
|
2846
|
+
sequence: operation.sequence
|
|
2847
|
+
});
|
|
2848
|
+
} else {
|
|
2849
|
+
const prior = records.get(change.key);
|
|
2850
|
+
if (prior?.recordSha256 !== change.priorSha256)
|
|
2851
|
+
throw new OhIntegrityError("Replay tombstone does not match its prior record.");
|
|
2852
|
+
records.delete(change.key);
|
|
2853
|
+
materializedBy.delete(change.key);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
for (const record of records.values()) {
|
|
2857
|
+
if (record.dependencies.some((dependency) => !records.has(dependency)))
|
|
2858
|
+
throw new OhIntegrityError("Replay has a missing dependency.");
|
|
2859
|
+
}
|
|
2860
|
+
const refs = [...records.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map(knowledgeGraphRecordRefV1);
|
|
2861
|
+
const recordsSha256 = canonicalSha256(refs);
|
|
2862
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
2863
|
+
changes: operation.changes,
|
|
2864
|
+
operationId: operation.operationId,
|
|
2865
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
2866
|
+
recordsSha256,
|
|
2867
|
+
revision: operation.sequence
|
|
2868
|
+
});
|
|
2869
|
+
if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
2870
|
+
throw new OhIntegrityError("Replay does not reproduce an operation head.");
|
|
2871
|
+
}
|
|
2872
|
+
head = {
|
|
2873
|
+
generation: operation.sequence,
|
|
2874
|
+
graphRevisionSha256,
|
|
2875
|
+
operationSha256: operation.operationSha256,
|
|
2876
|
+
recordsSha256,
|
|
2877
|
+
sequence: operation.sequence,
|
|
2878
|
+
v: 1
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
const storedRows = this.database.query(`SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence
|
|
2882
|
+
FROM oh_records WHERE space_id = ? ORDER BY record_key`).all(this.spaceId);
|
|
2883
|
+
if (storedRows.length !== records.size || storedRows.some((row) => {
|
|
2884
|
+
const record = records.get(row.record_key);
|
|
2885
|
+
const provenance = materializedBy.get(row.record_key);
|
|
2886
|
+
return record === undefined || record.kind !== row.kind || record.recordSha256 !== row.record_sha256 || canonicalJson(record) !== row.record_json || provenance === undefined || provenance.operationSha256 !== row.operation_sha256 || provenance.sequence !== row.sequence;
|
|
2887
|
+
}))
|
|
2888
|
+
throw new OhIntegrityError("Materialized records do not match operation replay.");
|
|
2889
|
+
const storedDependencies = this.database.query(`SELECT record_key, dependency_key FROM oh_dependencies
|
|
2890
|
+
WHERE space_id = ? ORDER BY record_key, dependency_key`).all(this.spaceId);
|
|
2891
|
+
const expectedDependencies = [...records.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).flatMap((record) => record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key })));
|
|
2892
|
+
if (canonicalJson(storedDependencies) !== canonicalJson(expectedDependencies)) {
|
|
2893
|
+
throw new OhIntegrityError("Materialized dependencies do not match operation replay.");
|
|
2894
|
+
}
|
|
2895
|
+
const storedOperationRecords = this.database.query(`SELECT materialized.operation_sha256, materialized.ordinal, materialized.record_key,
|
|
2896
|
+
materialized.change_kind, materialized.record_sha256
|
|
2897
|
+
FROM oh_operation_records AS materialized
|
|
2898
|
+
JOIN oh_operations AS operation ON operation.operation_sha256 = materialized.operation_sha256
|
|
2899
|
+
WHERE operation.space_id = ? ORDER BY operation.sequence, materialized.ordinal`).all(this.spaceId);
|
|
2900
|
+
const expectedOperationRecords = operations.flatMap((operation) => operation.changes.map((change, ordinal) => ({
|
|
2901
|
+
change_kind: change.kind,
|
|
2902
|
+
operation_sha256: operation.operationSha256,
|
|
2903
|
+
ordinal,
|
|
2904
|
+
record_key: change.kind === "put" ? change.record.key : change.key,
|
|
2905
|
+
record_sha256: change.kind === "put" ? change.record.recordSha256 : change.priorSha256
|
|
2906
|
+
})));
|
|
2907
|
+
if (canonicalJson(storedOperationRecords) !== canonicalJson(expectedOperationRecords)) {
|
|
2908
|
+
throw new OhIntegrityError("Materialized operation records do not match operation replay.");
|
|
2909
|
+
}
|
|
2910
|
+
if (canonicalJson(head) !== canonicalJson(this.head()))
|
|
2911
|
+
throw new OhIntegrityError("The stored head does not match operation replay.");
|
|
2912
|
+
return { head, operations: operations.length, records: records.size, sqliteIntegrity: "ok", v: 1 };
|
|
2913
|
+
}
|
|
2914
|
+
contract() {
|
|
2915
|
+
return { manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION };
|
|
2916
|
+
}
|
|
2917
|
+
purgeWorkingSpace(bindingValue, purgedAt = canonicalNow()) {
|
|
2918
|
+
this.#assertOpen();
|
|
2919
|
+
const binding = parseOhStoreBindingV1(bindingValue);
|
|
2920
|
+
if (binding === null || binding.spaceId !== this.spaceId || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) {
|
|
2921
|
+
throw new OhProfileError("Whole-space purge requires a bound working profile.");
|
|
2922
|
+
}
|
|
2923
|
+
return withImmediateTransaction(this.database, () => {
|
|
2924
|
+
const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId);
|
|
2925
|
+
if (row === null) {
|
|
2926
|
+
throw new OhProfileError("Whole-space purge requires the exact persisted store binding.");
|
|
2927
|
+
}
|
|
2928
|
+
const persistedBinding = parseStoredBindingRow(row, this.spaceId);
|
|
2929
|
+
if (canonicalJson(persistedBinding) !== canonicalJson(binding)) {
|
|
2930
|
+
throw new OhProfileError("Whole-space purge requires the exact persisted store binding.");
|
|
2931
|
+
}
|
|
2932
|
+
const receipt = createOhSpacePurgeReceiptV1({ binding, priorHead: this.head(), purgedAt });
|
|
2933
|
+
this.database.query(`INSERT INTO oh_space_purges(space_id, binding_sha256,
|
|
2934
|
+
prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json)
|
|
2935
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(this.spaceId, binding.bindingSha256, receipt.priorHead.operationSha256, receipt.priorHead.sequence, receipt.purgedAt, receipt.receiptSha256, canonicalJson(receipt));
|
|
2936
|
+
this.database.query("DELETE FROM oh_search_fts WHERE space_id = ?").run(this.spaceId);
|
|
2937
|
+
this.database.query("DELETE FROM oh_search_documents WHERE space_id = ?").run(this.spaceId);
|
|
2938
|
+
this.database.query("DELETE FROM oh_dependencies WHERE space_id = ?").run(this.spaceId);
|
|
2939
|
+
this.database.query(`DELETE FROM oh_operation_records WHERE operation_sha256 IN
|
|
2940
|
+
(SELECT operation_sha256 FROM oh_operations WHERE space_id = ?)`).run(this.spaceId);
|
|
2941
|
+
this.database.query("DELETE FROM oh_sync_outbox WHERE space_id = ?").run(this.spaceId);
|
|
2942
|
+
this.database.query("DELETE FROM oh_sync_state WHERE space_id = ?").run(this.spaceId);
|
|
2943
|
+
this.database.query("DELETE FROM oh_records WHERE space_id = ?").run(this.spaceId);
|
|
2944
|
+
this.database.query("DELETE FROM oh_operations WHERE space_id = ?").run(this.spaceId);
|
|
2945
|
+
this.database.query("DELETE FROM oh_space_bindings WHERE space_id = ?").run(this.spaceId);
|
|
2946
|
+
this.database.query("DELETE FROM oh_spaces WHERE space_id = ?").run(this.spaceId);
|
|
2947
|
+
const directTables = [
|
|
2948
|
+
"oh_spaces",
|
|
2949
|
+
"oh_space_bindings",
|
|
2950
|
+
"oh_operations",
|
|
2951
|
+
"oh_records",
|
|
2952
|
+
"oh_dependencies",
|
|
2953
|
+
"oh_search_documents",
|
|
2954
|
+
"oh_sync_outbox",
|
|
2955
|
+
"oh_sync_state"
|
|
2956
|
+
];
|
|
2957
|
+
for (const table of directTables) {
|
|
2958
|
+
const count = this.database.query(`SELECT count(*) AS count FROM ${table} WHERE space_id = ?`).get(this.spaceId)?.count;
|
|
2959
|
+
if (count !== 0)
|
|
2960
|
+
throw new OhIntegrityError(`Space purge left rows in ${table}.`);
|
|
2961
|
+
}
|
|
2962
|
+
const operationRecords = this.database.query(`SELECT count(*) AS count
|
|
2963
|
+
FROM oh_operation_records AS materialized JOIN oh_operations AS operation
|
|
2964
|
+
ON operation.operation_sha256 = materialized.operation_sha256
|
|
2965
|
+
WHERE operation.space_id = ?`).get(this.spaceId)?.count;
|
|
2966
|
+
const searchRows = this.database.query("SELECT count(*) AS count FROM oh_search_fts WHERE space_id = ?").get(this.spaceId)?.count;
|
|
2967
|
+
if (operationRecords !== 0 || searchRows !== 0) {
|
|
2968
|
+
throw new OhIntegrityError("Space purge left derived private payload rows.");
|
|
2969
|
+
}
|
|
2970
|
+
const receiptRow = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId);
|
|
2971
|
+
if (receiptRow === null || canonicalJson(parseStoredPurgeRow(receiptRow, this.spaceId)) !== canonicalJson(receipt)) {
|
|
2972
|
+
throw new OhIntegrityError("The stored purge receipt differs from the requested purge.");
|
|
2973
|
+
}
|
|
2974
|
+
return receipt;
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
close() {
|
|
2978
|
+
if (this.#closed)
|
|
2979
|
+
return;
|
|
2980
|
+
this.database.close(false);
|
|
2981
|
+
this.#closed = true;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// src/sdk.ts
|
|
2986
|
+
class Oh {
|
|
2987
|
+
store;
|
|
2988
|
+
semanticBackend;
|
|
2989
|
+
#closed = false;
|
|
2990
|
+
constructor(store, semanticBackend) {
|
|
2991
|
+
this.store = store;
|
|
2992
|
+
this.semanticBackend = semanticBackend;
|
|
2993
|
+
}
|
|
2994
|
+
static open(options = {}) {
|
|
2995
|
+
return new Oh(new OhSqliteStore({
|
|
2996
|
+
path: options.databasePath ?? ".oh/oh.sqlite",
|
|
2997
|
+
...options.spaceId === undefined ? {} : { spaceId: options.spaceId }
|
|
2998
|
+
}), options.semanticBackend);
|
|
2999
|
+
}
|
|
3000
|
+
head() {
|
|
3001
|
+
return this.store.head();
|
|
3002
|
+
}
|
|
3003
|
+
put(input) {
|
|
3004
|
+
const record = createKnowledgeGraphRecordV1({
|
|
3005
|
+
dependencies: [...input.dependencies ?? []].sort(),
|
|
3006
|
+
key: input.key,
|
|
3007
|
+
kind: input.kind,
|
|
3008
|
+
v: 1,
|
|
3009
|
+
value: input.value
|
|
3010
|
+
});
|
|
3011
|
+
const head = this.store.head();
|
|
3012
|
+
return this.store.commit({
|
|
3013
|
+
actorId: input.actorId ?? "agent.local",
|
|
3014
|
+
changes: [{ kind: "put", record, v: 1 }],
|
|
3015
|
+
expectedHead: input.expectedHead ?? head,
|
|
3016
|
+
...input.instant === undefined ? {} : { instant: input.instant },
|
|
3017
|
+
operationId: input.operationId ?? opaqueId("op_")
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
tombstone(input) {
|
|
3021
|
+
const current = this.store.get(input.key);
|
|
3022
|
+
if (current === null)
|
|
3023
|
+
throw new Error(`No record exists at ${input.key}.`);
|
|
3024
|
+
const head = this.store.head();
|
|
3025
|
+
return this.store.commit({
|
|
3026
|
+
actorId: input.actorId ?? "agent.local",
|
|
3027
|
+
changes: [{ key: input.key, kind: "tombstone", priorSha256: current.recordSha256, v: 1 }],
|
|
3028
|
+
expectedHead: input.expectedHead ?? head,
|
|
3029
|
+
...input.instant === undefined ? {} : { instant: input.instant },
|
|
3030
|
+
operationId: input.operationId ?? opaqueId("op_")
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
get(key) {
|
|
3034
|
+
return this.store.get(key);
|
|
3035
|
+
}
|
|
3036
|
+
list(options) {
|
|
3037
|
+
return this.store.list(options);
|
|
3038
|
+
}
|
|
3039
|
+
async indexSemantic() {
|
|
3040
|
+
if (this.semanticBackend === undefined)
|
|
3041
|
+
throw new Error("No local semantic backend is configured.");
|
|
3042
|
+
return await this.semanticBackend.index(this.store.snapshotRecords());
|
|
3043
|
+
}
|
|
3044
|
+
async search(query, options = {}) {
|
|
3045
|
+
return await searchOhV1({
|
|
3046
|
+
...this.semanticBackend === undefined ? {} : { backend: this.semanticBackend },
|
|
3047
|
+
...options.limit === undefined ? {} : { limit: options.limit },
|
|
3048
|
+
...options.mode === undefined ? {} : { mode: options.mode },
|
|
3049
|
+
query,
|
|
3050
|
+
store: this.store
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
async sync(transport, options) {
|
|
3054
|
+
return await synchronizeOhStoreV1(this.store, transport, options);
|
|
3055
|
+
}
|
|
3056
|
+
verify() {
|
|
3057
|
+
return this.store.verifyReplay();
|
|
3058
|
+
}
|
|
3059
|
+
async close() {
|
|
3060
|
+
if (this.#closed)
|
|
3061
|
+
return;
|
|
3062
|
+
this.#closed = true;
|
|
3063
|
+
try {
|
|
3064
|
+
await this.semanticBackend?.close();
|
|
3065
|
+
} finally {
|
|
3066
|
+
this.store.close();
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
export {
|
|
3071
|
+
Oh
|
|
3072
|
+
};
|