@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/store.js
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
// src/canonical.ts
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
class OhValidationError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
path;
|
|
7
|
+
constructor(code, path, message) {
|
|
8
|
+
super(`${path}: ${message}`);
|
|
9
|
+
this.name = "OhValidationError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isPlainRecord(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
16
|
+
return false;
|
|
17
|
+
const prototype = Object.getPrototypeOf(value);
|
|
18
|
+
return prototype === Object.prototype || prototype === null;
|
|
19
|
+
}
|
|
20
|
+
function hasExactKeys(value, keys) {
|
|
21
|
+
const actual = Object.keys(value);
|
|
22
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
23
|
+
}
|
|
24
|
+
function assertUnicodeScalarString(value, path) {
|
|
25
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
26
|
+
const code = value.charCodeAt(index);
|
|
27
|
+
if (code >= 55296 && code <= 56319) {
|
|
28
|
+
const next = value.charCodeAt(index + 1);
|
|
29
|
+
if (!(next >= 56320 && next <= 57343)) {
|
|
30
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
31
|
+
}
|
|
32
|
+
index += 1;
|
|
33
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
34
|
+
throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function encodeCanonical(value, path, ancestors) {
|
|
39
|
+
if (value === null || typeof value === "boolean")
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
assertUnicodeScalarString(value, path);
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "number") {
|
|
46
|
+
if (!Number.isFinite(value)) {
|
|
47
|
+
throw new OhValidationError("non-json-number", path, "must be finite");
|
|
48
|
+
}
|
|
49
|
+
if (Object.is(value, -0)) {
|
|
50
|
+
throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical");
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
if (typeof value !== "object" || value === null) {
|
|
55
|
+
throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`);
|
|
56
|
+
}
|
|
57
|
+
if (ancestors.has(value)) {
|
|
58
|
+
throw new OhValidationError("cycle", path, "contains a cycle");
|
|
59
|
+
}
|
|
60
|
+
ancestors.add(value);
|
|
61
|
+
try {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
const encoded = [];
|
|
64
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
65
|
+
if (!Object.hasOwn(value, index)) {
|
|
66
|
+
throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
|
|
67
|
+
}
|
|
68
|
+
encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors));
|
|
69
|
+
}
|
|
70
|
+
const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length));
|
|
71
|
+
if (extraKeys.length > 0) {
|
|
72
|
+
throw new OhValidationError("non-json-property", path, "array has non-index properties");
|
|
73
|
+
}
|
|
74
|
+
return `[${encoded.join(",")}]`;
|
|
75
|
+
}
|
|
76
|
+
if (!isPlainRecord(value)) {
|
|
77
|
+
throw new OhValidationError("non-plain-object", path, "must be a plain object");
|
|
78
|
+
}
|
|
79
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
80
|
+
if (ownKeys.some((key) => typeof key !== "string")) {
|
|
81
|
+
throw new OhValidationError("non-json-property", path, "object has a symbol property");
|
|
82
|
+
}
|
|
83
|
+
const keys = ownKeys;
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
86
|
+
if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
87
|
+
throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
keys.sort();
|
|
91
|
+
const entries = keys.map((key) => {
|
|
92
|
+
assertUnicodeScalarString(key, `${path}.<key>`);
|
|
93
|
+
return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`;
|
|
94
|
+
});
|
|
95
|
+
return `{${entries.join(",")}}`;
|
|
96
|
+
} finally {
|
|
97
|
+
ancestors.delete(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function canonicalJson(value) {
|
|
101
|
+
return encodeCanonical(value, "$", new Set);
|
|
102
|
+
}
|
|
103
|
+
function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) {
|
|
104
|
+
if (utf8ByteLength(text) > maximumBytes) {
|
|
105
|
+
throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit");
|
|
106
|
+
}
|
|
107
|
+
let value;
|
|
108
|
+
try {
|
|
109
|
+
value = JSON.parse(text);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new OhValidationError("invalid-json", "$", "is not valid JSON");
|
|
112
|
+
}
|
|
113
|
+
if (canonicalJson(value) !== text) {
|
|
114
|
+
throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical");
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function utf8ByteLength(value) {
|
|
119
|
+
return Buffer.byteLength(value, "utf8");
|
|
120
|
+
}
|
|
121
|
+
function sha256Hex(value) {
|
|
122
|
+
return createHash("sha256").update(value).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
function canonicalSha256(value) {
|
|
125
|
+
return sha256Hex(canonicalJson(value));
|
|
126
|
+
}
|
|
127
|
+
function parseSha256Hex(value) {
|
|
128
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
|
|
129
|
+
}
|
|
130
|
+
function parseCanonicalInstantV1(value) {
|
|
131
|
+
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)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const timestamp = Date.parse(value);
|
|
135
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null;
|
|
136
|
+
}
|
|
137
|
+
function canonicalNow() {
|
|
138
|
+
return new Date().toISOString();
|
|
139
|
+
}
|
|
140
|
+
function safeCode(value, maximumLength = 128) {
|
|
141
|
+
return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
142
|
+
}
|
|
143
|
+
function orderedUnique(values, key) {
|
|
144
|
+
return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value));
|
|
145
|
+
}
|
|
146
|
+
function sortUnique(values, key) {
|
|
147
|
+
const sorted = [...values].sort((left, right) => {
|
|
148
|
+
const leftKey = key(left);
|
|
149
|
+
const rightKey = key(right);
|
|
150
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
151
|
+
});
|
|
152
|
+
if (!orderedUnique(sorted, key)) {
|
|
153
|
+
throw new OhValidationError("duplicate", "$", "contains duplicate canonical values");
|
|
154
|
+
}
|
|
155
|
+
return sorted;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/graph.ts
|
|
159
|
+
var OH_GRAPH_FORMAT_VERSION_V1 = 1;
|
|
160
|
+
var OH_GRAPH_LIMITS_V1 = Object.freeze({
|
|
161
|
+
changesPerOperation: 8192,
|
|
162
|
+
dependenciesPerRecord: 4096,
|
|
163
|
+
recordBytes: 1024 * 1024,
|
|
164
|
+
recordsPerSnapshot: 65536
|
|
165
|
+
});
|
|
166
|
+
var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
|
|
167
|
+
"activity",
|
|
168
|
+
"assertion",
|
|
169
|
+
"context",
|
|
170
|
+
"dependency-manifest",
|
|
171
|
+
"edition",
|
|
172
|
+
"entity",
|
|
173
|
+
"evidence",
|
|
174
|
+
"identity-operation",
|
|
175
|
+
"inquiry",
|
|
176
|
+
"inquiry-event",
|
|
177
|
+
"review-decision",
|
|
178
|
+
"rights-decision",
|
|
179
|
+
"schema",
|
|
180
|
+
"shape",
|
|
181
|
+
"statement",
|
|
182
|
+
"type-membership",
|
|
183
|
+
"view",
|
|
184
|
+
"vocabulary"
|
|
185
|
+
];
|
|
186
|
+
function recordKey(value) {
|
|
187
|
+
return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
|
|
188
|
+
}
|
|
189
|
+
function createKnowledgeGraphRecordV1(input) {
|
|
190
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies))
|
|
191
|
+
throw new TypeError("Invalid graph record input.");
|
|
192
|
+
const key = recordKey(input.key);
|
|
193
|
+
const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
|
|
194
|
+
if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
|
|
195
|
+
throw new TypeError("Invalid graph record identity.");
|
|
196
|
+
const dependencies = input.dependencies.map(recordKey);
|
|
197
|
+
if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
|
|
198
|
+
throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
|
|
199
|
+
}
|
|
200
|
+
const valueJson = canonicalJson(input.value);
|
|
201
|
+
if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) {
|
|
202
|
+
throw new RangeError("Graph record value exceeds its canonical byte limit.");
|
|
203
|
+
}
|
|
204
|
+
const payload = { dependencies, key, kind, v: 1, value: input.value };
|
|
205
|
+
return { ...payload, recordSha256: canonicalSha256(payload) };
|
|
206
|
+
}
|
|
207
|
+
function parseKnowledgeGraphRecordV1(value) {
|
|
208
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256"))
|
|
209
|
+
return null;
|
|
210
|
+
const recordSha256 = parseSha256Hex(value.recordSha256);
|
|
211
|
+
const { recordSha256: _digest, ...input } = value;
|
|
212
|
+
try {
|
|
213
|
+
const created = createKnowledgeGraphRecordV1(input);
|
|
214
|
+
return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function knowledgeGraphRecordRefV1(record) {
|
|
220
|
+
return {
|
|
221
|
+
dependencies: record.dependencies,
|
|
222
|
+
key: record.key,
|
|
223
|
+
kind: record.kind,
|
|
224
|
+
sha256: record.recordSha256,
|
|
225
|
+
v: 1
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function changeKey(change) {
|
|
229
|
+
return change.kind === "put" ? change.record.key : change.key;
|
|
230
|
+
}
|
|
231
|
+
function canonicalKnowledgeGraphChangesV1(changes) {
|
|
232
|
+
const normalized = [];
|
|
233
|
+
for (const change of changes) {
|
|
234
|
+
if (!isPlainRecord(change) || change.v !== 1)
|
|
235
|
+
throw new TypeError("Invalid graph change.");
|
|
236
|
+
if (change.kind === "put") {
|
|
237
|
+
const record = parseKnowledgeGraphRecordV1(change.record);
|
|
238
|
+
if (record === null)
|
|
239
|
+
throw new TypeError("Invalid graph record in change.");
|
|
240
|
+
normalized.push({ kind: "put", record, v: 1 });
|
|
241
|
+
} else if (change.kind === "tombstone") {
|
|
242
|
+
const key = recordKey(change.key);
|
|
243
|
+
const priorSha256 = parseSha256Hex(change.priorSha256);
|
|
244
|
+
if (key === null || priorSha256 === null)
|
|
245
|
+
throw new TypeError("Invalid graph tombstone.");
|
|
246
|
+
normalized.push({ key, kind: "tombstone", priorSha256, v: 1 });
|
|
247
|
+
} else
|
|
248
|
+
throw new TypeError("Unknown graph change kind.");
|
|
249
|
+
}
|
|
250
|
+
return sortUnique(normalized, changeKey);
|
|
251
|
+
}
|
|
252
|
+
function graphRevisionSha256V1(input) {
|
|
253
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
254
|
+
const operationId = safeCode(input.operationId);
|
|
255
|
+
const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256);
|
|
256
|
+
const recordsSha256 = parseSha256Hex(input.recordsSha256);
|
|
257
|
+
const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null;
|
|
258
|
+
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)) {
|
|
259
|
+
throw new TypeError("Invalid graph revision digest input.");
|
|
260
|
+
}
|
|
261
|
+
return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/ontology.ts
|
|
265
|
+
var OH_ONTOLOGY_VERSION_V1 = "1.0.0";
|
|
266
|
+
var OH_CONTRACT_ID_V1 = "oh.ontology.v1";
|
|
267
|
+
var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({
|
|
268
|
+
dimensions: 64,
|
|
269
|
+
listValues: 256,
|
|
270
|
+
qualifiers: 128,
|
|
271
|
+
statementBytes: 256 * 1024,
|
|
272
|
+
textBytes: 64 * 1024
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// src/schema.ts
|
|
276
|
+
var OH_SCHEMA_FORMAT_VERSION_V1 = 1;
|
|
277
|
+
|
|
278
|
+
// src/contract.ts
|
|
279
|
+
var manifestPayload = Object.freeze({
|
|
280
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
281
|
+
graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1,
|
|
282
|
+
ontologyVersion: OH_ONTOLOGY_VERSION_V1,
|
|
283
|
+
recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1,
|
|
284
|
+
schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1,
|
|
285
|
+
v: 1
|
|
286
|
+
});
|
|
287
|
+
var OH_CONTRACT_MANIFEST_V1 = Object.freeze({
|
|
288
|
+
...manifestPayload,
|
|
289
|
+
contractSha256: canonicalSha256(manifestPayload)
|
|
290
|
+
});
|
|
291
|
+
class OhRecordCodecRegistry {
|
|
292
|
+
#codecs = new Map;
|
|
293
|
+
#sealed = false;
|
|
294
|
+
register(codec) {
|
|
295
|
+
if (this.#sealed)
|
|
296
|
+
throw new TypeError("The codec registry is sealed.");
|
|
297
|
+
if (this.#codecs.has(codec.kind))
|
|
298
|
+
throw new TypeError(`A codec is already registered for ${codec.kind}.`);
|
|
299
|
+
this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse }));
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
parse(kind, value) {
|
|
303
|
+
const codec = this.#codecs.get(kind);
|
|
304
|
+
if (codec !== undefined)
|
|
305
|
+
return codec.parse(value);
|
|
306
|
+
try {
|
|
307
|
+
canonicalJson(value);
|
|
308
|
+
return value;
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
has(kind) {
|
|
314
|
+
return this.#codecs.has(kind);
|
|
315
|
+
}
|
|
316
|
+
parseRequired(kind, value) {
|
|
317
|
+
const codec = this.#codecs.get(kind);
|
|
318
|
+
if (codec === undefined)
|
|
319
|
+
return null;
|
|
320
|
+
try {
|
|
321
|
+
const parsed = codec.parse(value);
|
|
322
|
+
if (parsed === null)
|
|
323
|
+
return null;
|
|
324
|
+
canonicalJson(parsed);
|
|
325
|
+
return parsed;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
seal() {
|
|
331
|
+
this.#sealed = true;
|
|
332
|
+
return this;
|
|
333
|
+
}
|
|
334
|
+
get sealed() {
|
|
335
|
+
return this.#sealed;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// src/operation.ts
|
|
339
|
+
var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024;
|
|
340
|
+
function parsePayload(value) {
|
|
341
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
342
|
+
"actorId",
|
|
343
|
+
"changes",
|
|
344
|
+
"contractId",
|
|
345
|
+
"graphRevisionSha256",
|
|
346
|
+
"instant",
|
|
347
|
+
"operationId",
|
|
348
|
+
"parentOperationSha256",
|
|
349
|
+
"recordsSha256",
|
|
350
|
+
"sequence",
|
|
351
|
+
"spaceId",
|
|
352
|
+
"v"
|
|
353
|
+
]) || value.v !== 1 || value.contractId !== OH_CONTRACT_ID_V1 || !Array.isArray(value.changes))
|
|
354
|
+
return null;
|
|
355
|
+
const actorId = safeCode(value.actorId);
|
|
356
|
+
const operationId = safeCode(value.operationId);
|
|
357
|
+
const spaceId = safeCode(value.spaceId);
|
|
358
|
+
const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256);
|
|
359
|
+
const parentOperationSha256 = value.parentOperationSha256 === null ? null : parseSha256Hex(value.parentOperationSha256);
|
|
360
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
361
|
+
const instant = parseCanonicalInstantV1(value.instant);
|
|
362
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence > 0 ? value.sequence : null;
|
|
363
|
+
let changes;
|
|
364
|
+
try {
|
|
365
|
+
changes = canonicalKnowledgeGraphChangesV1(value.changes);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation)
|
|
370
|
+
return null;
|
|
371
|
+
return actorId !== null && operationId !== null && spaceId !== null && graphRevisionSha256 !== null && recordsSha256 !== null && instant !== null && sequence !== null && (value.parentOperationSha256 === null || parentOperationSha256 !== null) && sequence === 1 === (parentOperationSha256 === null) ? {
|
|
372
|
+
actorId,
|
|
373
|
+
changes,
|
|
374
|
+
contractId: OH_CONTRACT_ID_V1,
|
|
375
|
+
graphRevisionSha256,
|
|
376
|
+
instant,
|
|
377
|
+
operationId,
|
|
378
|
+
parentOperationSha256,
|
|
379
|
+
recordsSha256,
|
|
380
|
+
sequence,
|
|
381
|
+
spaceId,
|
|
382
|
+
v: 1
|
|
383
|
+
} : null;
|
|
384
|
+
}
|
|
385
|
+
function createOhOperationV1(input) {
|
|
386
|
+
const payload = parsePayload(input);
|
|
387
|
+
if (payload === null)
|
|
388
|
+
throw new TypeError("Invalid Oh operation payload.");
|
|
389
|
+
const operation = { ...payload, operationSha256: canonicalSha256(payload) };
|
|
390
|
+
if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) {
|
|
391
|
+
throw new RangeError("Oh operation exceeds its canonical byte limit.");
|
|
392
|
+
}
|
|
393
|
+
return operation;
|
|
394
|
+
}
|
|
395
|
+
function parseOhOperationV1(value) {
|
|
396
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "operationSha256"))
|
|
397
|
+
return null;
|
|
398
|
+
const operationSha256 = parseSha256Hex(value.operationSha256);
|
|
399
|
+
const { operationSha256: _digest, ...input } = value;
|
|
400
|
+
const payload = parsePayload(input);
|
|
401
|
+
return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/store.ts
|
|
405
|
+
class OhConflictError extends Error {
|
|
406
|
+
constructor(message) {
|
|
407
|
+
super(message);
|
|
408
|
+
this.name = "OhConflictError";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
class OhIntegrityError extends Error {
|
|
413
|
+
constructor(message) {
|
|
414
|
+
super(message);
|
|
415
|
+
this.name = "OhIntegrityError";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
class OhDependencyError extends Error {
|
|
420
|
+
constructor(message) {
|
|
421
|
+
super(message);
|
|
422
|
+
this.name = "OhDependencyError";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
class OhProfileError extends Error {
|
|
427
|
+
constructor(message) {
|
|
428
|
+
super(message);
|
|
429
|
+
this.name = "OhProfileError";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
433
|
+
applicationProfileSha256: null,
|
|
434
|
+
capabilities: {
|
|
435
|
+
changesSince: true,
|
|
436
|
+
dependencyClosureExport: true,
|
|
437
|
+
exactSnapshots: true,
|
|
438
|
+
operationReplication: true,
|
|
439
|
+
semanticBundleCommit: true,
|
|
440
|
+
v: 1,
|
|
441
|
+
wholeSpacePurge: false
|
|
442
|
+
},
|
|
443
|
+
profileId: "oh.store.canonical.v1",
|
|
444
|
+
profileKind: "canonical",
|
|
445
|
+
v: 1
|
|
446
|
+
});
|
|
447
|
+
var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({
|
|
448
|
+
applicationProfileSha256: null,
|
|
449
|
+
capabilities: {
|
|
450
|
+
changesSince: true,
|
|
451
|
+
dependencyClosureExport: true,
|
|
452
|
+
exactSnapshots: true,
|
|
453
|
+
operationReplication: false,
|
|
454
|
+
semanticBundleCommit: true,
|
|
455
|
+
v: 1,
|
|
456
|
+
wholeSpacePurge: true
|
|
457
|
+
},
|
|
458
|
+
profileId: "oh.store.working.v1",
|
|
459
|
+
profileKind: "working",
|
|
460
|
+
v: 1
|
|
461
|
+
});
|
|
462
|
+
var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({
|
|
463
|
+
bytes: 64 * 1024 * 1024,
|
|
464
|
+
records: 8192,
|
|
465
|
+
roots: 1024
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
class OhPurgedSpaceError extends Error {
|
|
469
|
+
receipt;
|
|
470
|
+
constructor(receipt) {
|
|
471
|
+
super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`);
|
|
472
|
+
this.name = "OhPurgedSpaceError";
|
|
473
|
+
this.receipt = receipt;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
var EMPTY_RECORDS_SHA256 = canonicalSha256([]);
|
|
477
|
+
function emptyOhHeadV1() {
|
|
478
|
+
return {
|
|
479
|
+
generation: 0,
|
|
480
|
+
graphRevisionSha256: null,
|
|
481
|
+
operationSha256: null,
|
|
482
|
+
recordsSha256: EMPTY_RECORDS_SHA256,
|
|
483
|
+
sequence: 0,
|
|
484
|
+
v: 1
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function parseOhHeadV1(value) {
|
|
488
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
489
|
+
"generation",
|
|
490
|
+
"graphRevisionSha256",
|
|
491
|
+
"operationSha256",
|
|
492
|
+
"recordsSha256",
|
|
493
|
+
"sequence",
|
|
494
|
+
"v"
|
|
495
|
+
]) || value.v !== 1)
|
|
496
|
+
return null;
|
|
497
|
+
const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256);
|
|
498
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
499
|
+
const recordsSha256 = parseSha256Hex(value.recordsSha256);
|
|
500
|
+
const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null;
|
|
501
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null;
|
|
502
|
+
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;
|
|
503
|
+
}
|
|
504
|
+
function parseOhHeadRefV1(value) {
|
|
505
|
+
const complete = parseOhHeadV1(value);
|
|
506
|
+
if (complete !== null) {
|
|
507
|
+
return { operationSha256: complete.operationSha256, sequence: complete.sequence };
|
|
508
|
+
}
|
|
509
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"]))
|
|
510
|
+
return null;
|
|
511
|
+
const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256);
|
|
512
|
+
const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null;
|
|
513
|
+
return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null;
|
|
514
|
+
}
|
|
515
|
+
function parseCapabilities(value) {
|
|
516
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
517
|
+
"changesSince",
|
|
518
|
+
"dependencyClosureExport",
|
|
519
|
+
"exactSnapshots",
|
|
520
|
+
"operationReplication",
|
|
521
|
+
"semanticBundleCommit",
|
|
522
|
+
"v",
|
|
523
|
+
"wholeSpacePurge"
|
|
524
|
+
]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean")
|
|
525
|
+
return null;
|
|
526
|
+
return {
|
|
527
|
+
changesSince: true,
|
|
528
|
+
dependencyClosureExport: true,
|
|
529
|
+
exactSnapshots: true,
|
|
530
|
+
operationReplication: value.operationReplication,
|
|
531
|
+
semanticBundleCommit: true,
|
|
532
|
+
v: 1,
|
|
533
|
+
wholeSpacePurge: value.wholeSpacePurge
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function createOhStoreProfileV1(input) {
|
|
537
|
+
if (!isPlainRecord(input) || !hasExactKeys(input, [
|
|
538
|
+
"applicationProfileSha256",
|
|
539
|
+
"capabilities",
|
|
540
|
+
"profileId",
|
|
541
|
+
"profileKind",
|
|
542
|
+
"v"
|
|
543
|
+
]) || input.v !== 1)
|
|
544
|
+
throw new TypeError("Invalid Oh store profile input.");
|
|
545
|
+
const profileId = safeCode(input.profileId);
|
|
546
|
+
const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256);
|
|
547
|
+
const capabilities = parseCapabilities(input.capabilities);
|
|
548
|
+
if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") {
|
|
549
|
+
throw new TypeError("Invalid Oh store profile input.");
|
|
550
|
+
}
|
|
551
|
+
if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) {
|
|
552
|
+
throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge.");
|
|
553
|
+
}
|
|
554
|
+
if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) {
|
|
555
|
+
throw new OhProfileError("A canonical profile cannot permit whole-space purge.");
|
|
556
|
+
}
|
|
557
|
+
const payload = {
|
|
558
|
+
applicationProfileSha256,
|
|
559
|
+
capabilities: Object.freeze(capabilities),
|
|
560
|
+
profileId,
|
|
561
|
+
profileKind: input.profileKind,
|
|
562
|
+
v: 1
|
|
563
|
+
};
|
|
564
|
+
return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) });
|
|
565
|
+
}
|
|
566
|
+
function parseOhStoreProfileV1(value) {
|
|
567
|
+
if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256"))
|
|
568
|
+
return null;
|
|
569
|
+
const digest = parseSha256Hex(value.profileSha256);
|
|
570
|
+
const { profileSha256: _profileSha256, ...input } = value;
|
|
571
|
+
try {
|
|
572
|
+
const created = createOhStoreProfileV1(input);
|
|
573
|
+
return digest !== null && created.profileSha256 === digest ? created : null;
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function createOhStoreBindingV1(input) {
|
|
579
|
+
const profile = parseOhStoreProfileV1(input.profile);
|
|
580
|
+
const realmId = safeCode(input.realmId);
|
|
581
|
+
const spaceId = safeCode(input.spaceId);
|
|
582
|
+
if (input.v !== 1 || profile === null || realmId === null || spaceId === null) {
|
|
583
|
+
throw new TypeError("Invalid Oh store binding input.");
|
|
584
|
+
}
|
|
585
|
+
const payload = {
|
|
586
|
+
contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
|
|
587
|
+
profile,
|
|
588
|
+
realmId,
|
|
589
|
+
spaceId,
|
|
590
|
+
v: 1
|
|
591
|
+
};
|
|
592
|
+
return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) });
|
|
593
|
+
}
|
|
594
|
+
function parseOhStoreBindingV1(value) {
|
|
595
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
596
|
+
"bindingSha256",
|
|
597
|
+
"contractSha256",
|
|
598
|
+
"profile",
|
|
599
|
+
"realmId",
|
|
600
|
+
"spaceId",
|
|
601
|
+
"v"
|
|
602
|
+
]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256)
|
|
603
|
+
return null;
|
|
604
|
+
const bindingSha256 = parseSha256Hex(value.bindingSha256);
|
|
605
|
+
const profile = parseOhStoreProfileV1(value.profile);
|
|
606
|
+
try {
|
|
607
|
+
if (bindingSha256 === null || profile === null)
|
|
608
|
+
return null;
|
|
609
|
+
const created = createOhStoreBindingV1({
|
|
610
|
+
profile,
|
|
611
|
+
realmId: value.realmId,
|
|
612
|
+
spaceId: value.spaceId,
|
|
613
|
+
v: 1
|
|
614
|
+
});
|
|
615
|
+
return created.bindingSha256 === bindingSha256 ? created : null;
|
|
616
|
+
} catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function sortedRecords(records) {
|
|
621
|
+
return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
|
|
622
|
+
}
|
|
623
|
+
function verifyDependencies(records) {
|
|
624
|
+
for (const record of records.values()) {
|
|
625
|
+
for (const dependency of record.dependencies) {
|
|
626
|
+
if (!records.has(dependency))
|
|
627
|
+
throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
632
|
+
const parsedSpaceId = safeCode(spaceId);
|
|
633
|
+
if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
634
|
+
throw new TypeError("Invalid operation replay input.");
|
|
635
|
+
}
|
|
636
|
+
const records = new Map;
|
|
637
|
+
const operationIds = new Set;
|
|
638
|
+
let head = emptyOhHeadV1();
|
|
639
|
+
for (const value of values) {
|
|
640
|
+
const operation = parseOhOperationV1(value);
|
|
641
|
+
if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) {
|
|
642
|
+
throw new OhIntegrityError("Operation replay chain is broken.");
|
|
643
|
+
}
|
|
644
|
+
operationIds.add(operation.operationId);
|
|
645
|
+
for (const change of operation.changes) {
|
|
646
|
+
if (change.kind === "put")
|
|
647
|
+
records.set(change.record.key, change.record);
|
|
648
|
+
else {
|
|
649
|
+
const prior = records.get(change.key);
|
|
650
|
+
if (prior?.recordSha256 !== change.priorSha256) {
|
|
651
|
+
throw new OhIntegrityError("Replay tombstone does not match its prior record.");
|
|
652
|
+
}
|
|
653
|
+
records.delete(change.key);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (records.size > maximumRecords)
|
|
657
|
+
throw new RangeError("Operation replay exceeds its record bound.");
|
|
658
|
+
verifyDependencies(records);
|
|
659
|
+
const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1);
|
|
660
|
+
const recordsSha256 = canonicalSha256(refs);
|
|
661
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
662
|
+
changes: operation.changes,
|
|
663
|
+
operationId: operation.operationId,
|
|
664
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
665
|
+
recordsSha256,
|
|
666
|
+
revision: operation.sequence
|
|
667
|
+
});
|
|
668
|
+
if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) {
|
|
669
|
+
throw new OhIntegrityError("Replay does not reproduce an operation head.");
|
|
670
|
+
}
|
|
671
|
+
head = {
|
|
672
|
+
generation: operation.sequence,
|
|
673
|
+
graphRevisionSha256,
|
|
674
|
+
operationSha256: operation.operationSha256,
|
|
675
|
+
recordsSha256,
|
|
676
|
+
sequence: operation.sequence,
|
|
677
|
+
v: 1
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return { head, records: sortedRecords(records.values()), v: 1 };
|
|
681
|
+
}
|
|
682
|
+
function transitionOhSnapshotV1(input) {
|
|
683
|
+
const actorId = safeCode(input.actorId);
|
|
684
|
+
const operationId = safeCode(input.operationId);
|
|
685
|
+
const spaceId = safeCode(input.spaceId);
|
|
686
|
+
const instant = parseCanonicalInstantV1(input.instant);
|
|
687
|
+
const changes = canonicalKnowledgeGraphChangesV1(input.changes);
|
|
688
|
+
if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) {
|
|
689
|
+
throw new TypeError("Invalid graph transition input.");
|
|
690
|
+
}
|
|
691
|
+
const head = parseOhHeadV1(input.snapshot.head);
|
|
692
|
+
if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) {
|
|
693
|
+
throw new OhIntegrityError("The transition snapshot is invalid.");
|
|
694
|
+
}
|
|
695
|
+
const records = new Map;
|
|
696
|
+
for (const value of input.snapshot.records) {
|
|
697
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
698
|
+
if (record === null || records.has(record.key)) {
|
|
699
|
+
throw new OhIntegrityError("The transition snapshot contains an invalid record.");
|
|
700
|
+
}
|
|
701
|
+
records.set(record.key, record);
|
|
702
|
+
}
|
|
703
|
+
verifyDependencies(records);
|
|
704
|
+
const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1));
|
|
705
|
+
if (priorRecordsSha256 !== head.recordsSha256) {
|
|
706
|
+
throw new OhIntegrityError("The transition snapshot does not reproduce its head.");
|
|
707
|
+
}
|
|
708
|
+
for (const change of changes) {
|
|
709
|
+
if (change.kind === "put")
|
|
710
|
+
records.set(change.record.key, change.record);
|
|
711
|
+
else {
|
|
712
|
+
const prior = records.get(change.key);
|
|
713
|
+
if (prior === undefined || prior.recordSha256 !== change.priorSha256) {
|
|
714
|
+
throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`);
|
|
715
|
+
}
|
|
716
|
+
records.delete(change.key);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
|
|
720
|
+
throw new RangeError("Graph transition exceeds its record snapshot limit.");
|
|
721
|
+
}
|
|
722
|
+
verifyDependencies(records);
|
|
723
|
+
const nextRecords = sortedRecords(records.values());
|
|
724
|
+
const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1));
|
|
725
|
+
const graphRevisionSha256 = graphRevisionSha256V1({
|
|
726
|
+
changes,
|
|
727
|
+
operationId,
|
|
728
|
+
parentGraphRevisionSha256: head.graphRevisionSha256,
|
|
729
|
+
recordsSha256,
|
|
730
|
+
revision: head.sequence + 1
|
|
731
|
+
});
|
|
732
|
+
const operation = createOhOperationV1({
|
|
733
|
+
actorId,
|
|
734
|
+
changes,
|
|
735
|
+
contractId: OH_CONTRACT_MANIFEST_V1.contractId,
|
|
736
|
+
graphRevisionSha256,
|
|
737
|
+
instant,
|
|
738
|
+
operationId,
|
|
739
|
+
parentOperationSha256: head.operationSha256,
|
|
740
|
+
recordsSha256,
|
|
741
|
+
sequence: head.sequence + 1,
|
|
742
|
+
spaceId,
|
|
743
|
+
v: 1
|
|
744
|
+
});
|
|
745
|
+
const nextHead = {
|
|
746
|
+
generation: operation.sequence,
|
|
747
|
+
graphRevisionSha256,
|
|
748
|
+
operationSha256: operation.operationSha256,
|
|
749
|
+
recordsSha256,
|
|
750
|
+
sequence: operation.sequence,
|
|
751
|
+
v: 1
|
|
752
|
+
};
|
|
753
|
+
return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } };
|
|
754
|
+
}
|
|
755
|
+
function normalizeRoots(values) {
|
|
756
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) {
|
|
757
|
+
throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`);
|
|
758
|
+
}
|
|
759
|
+
const roots = values.map((value) => safeCode(value, 512));
|
|
760
|
+
if (roots.some((value) => value === null))
|
|
761
|
+
throw new TypeError("Invalid dependency closure root.");
|
|
762
|
+
const sorted = [...roots].sort();
|
|
763
|
+
if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) {
|
|
764
|
+
throw new TypeError("Dependency closure roots must be unique.");
|
|
765
|
+
}
|
|
766
|
+
return sorted;
|
|
767
|
+
}
|
|
768
|
+
function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) {
|
|
769
|
+
const selected = new Map;
|
|
770
|
+
const pending = [...roots];
|
|
771
|
+
let selectedBytes = 0;
|
|
772
|
+
while (pending.length > 0) {
|
|
773
|
+
const key = pending.pop();
|
|
774
|
+
if (selected.has(key))
|
|
775
|
+
continue;
|
|
776
|
+
const record = available.get(key);
|
|
777
|
+
if (record === undefined)
|
|
778
|
+
throw new OhDependencyError(`Dependency closure record ${key} is missing.`);
|
|
779
|
+
selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1;
|
|
780
|
+
if (selectedBytes > maximumBytes)
|
|
781
|
+
throw new RangeError("Dependency closure exceeds its canonical byte bound.");
|
|
782
|
+
selected.set(key, record);
|
|
783
|
+
if (selected.size > maximumRecords)
|
|
784
|
+
throw new RangeError("Dependency closure exceeds its record bound.");
|
|
785
|
+
pending.push(...record.dependencies);
|
|
786
|
+
}
|
|
787
|
+
return sortedRecords(selected.values());
|
|
788
|
+
}
|
|
789
|
+
function createOhDependencyClosureV1(input) {
|
|
790
|
+
const binding = parseOhStoreBindingV1(input.binding);
|
|
791
|
+
const head = parseOhHeadV1(input.snapshot.head);
|
|
792
|
+
const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records;
|
|
793
|
+
if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) {
|
|
794
|
+
throw new TypeError("Invalid dependency closure input.");
|
|
795
|
+
}
|
|
796
|
+
const roots = normalizeRoots(input.roots);
|
|
797
|
+
const available = new Map;
|
|
798
|
+
for (const value of input.snapshot.records) {
|
|
799
|
+
const record = parseKnowledgeGraphRecordV1(value);
|
|
800
|
+
if (record === null || available.has(record.key))
|
|
801
|
+
throw new OhIntegrityError("Snapshot contains an invalid record.");
|
|
802
|
+
available.set(record.key, record);
|
|
803
|
+
}
|
|
804
|
+
const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1));
|
|
805
|
+
if (recordsSha256 !== head.recordsSha256)
|
|
806
|
+
throw new OhIntegrityError("Snapshot records do not reproduce its head.");
|
|
807
|
+
const records = closureRecords(available, roots, maximumRecords);
|
|
808
|
+
const payload = { binding, head, records, roots, v: 1 };
|
|
809
|
+
const closure = { ...payload, closureSha256: canonicalSha256(payload) };
|
|
810
|
+
if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) {
|
|
811
|
+
throw new RangeError("Dependency closure exceeds its canonical byte bound.");
|
|
812
|
+
}
|
|
813
|
+
return Object.freeze(closure);
|
|
814
|
+
}
|
|
815
|
+
function parseOhDependencyClosureV1(value) {
|
|
816
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
817
|
+
"binding",
|
|
818
|
+
"closureSha256",
|
|
819
|
+
"head",
|
|
820
|
+
"records",
|
|
821
|
+
"roots",
|
|
822
|
+
"v"
|
|
823
|
+
]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records)
|
|
824
|
+
return null;
|
|
825
|
+
const binding = parseOhStoreBindingV1(value.binding);
|
|
826
|
+
const head = parseOhHeadV1(value.head);
|
|
827
|
+
const closureSha256 = parseSha256Hex(value.closureSha256);
|
|
828
|
+
if (binding === null || head === null || closureSha256 === null)
|
|
829
|
+
return null;
|
|
830
|
+
const records = new Map;
|
|
831
|
+
for (const item of value.records) {
|
|
832
|
+
const record = parseKnowledgeGraphRecordV1(item);
|
|
833
|
+
if (record === null || records.has(record.key))
|
|
834
|
+
return null;
|
|
835
|
+
records.set(record.key, record);
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
const roots = normalizeRoots(value.roots);
|
|
839
|
+
if (canonicalJson(roots) !== canonicalJson(value.roots))
|
|
840
|
+
return null;
|
|
841
|
+
const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records);
|
|
842
|
+
if (canonicalJson(exact) !== canonicalJson(value.records))
|
|
843
|
+
return null;
|
|
844
|
+
const payload = { binding, head, records: exact, roots, v: 1 };
|
|
845
|
+
const parsed = { ...payload, closureSha256 };
|
|
846
|
+
return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null;
|
|
847
|
+
} catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function verifyOhDependencyClosureV1(value) {
|
|
852
|
+
const closure = parseOhDependencyClosureV1(value);
|
|
853
|
+
return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true };
|
|
854
|
+
}
|
|
855
|
+
function verifyOhDependencyClosureAgainstV1(value, expected) {
|
|
856
|
+
const binding = parseOhStoreBindingV1(expected.binding);
|
|
857
|
+
const head = parseOhHeadV1(expected.head);
|
|
858
|
+
if (binding === null || head === null)
|
|
859
|
+
return { ok: false, reason: "invalid-expectation" };
|
|
860
|
+
const closure = parseOhDependencyClosureV1(value);
|
|
861
|
+
if (closure === null)
|
|
862
|
+
return { ok: false, reason: "invalid-closure" };
|
|
863
|
+
if (closure.binding.bindingSha256 !== binding.bindingSha256)
|
|
864
|
+
return { ok: false, reason: "binding-mismatch" };
|
|
865
|
+
if (canonicalJson(closure.head) !== canonicalJson(head))
|
|
866
|
+
return { ok: false, reason: "head-mismatch" };
|
|
867
|
+
return { closure, ok: true, verification: "expected-authority-and-head" };
|
|
868
|
+
}
|
|
869
|
+
function createOhSpacePurgeReceiptV1(input) {
|
|
870
|
+
const binding = parseOhStoreBindingV1(input.binding);
|
|
871
|
+
const priorHead = parseOhHeadV1(input.priorHead);
|
|
872
|
+
const purgedAt = parseCanonicalInstantV1(input.purgedAt);
|
|
873
|
+
if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) {
|
|
874
|
+
throw new OhProfileError("Only a bound working realm can produce a purge receipt.");
|
|
875
|
+
}
|
|
876
|
+
const payload = {
|
|
877
|
+
bindingSha256: binding.bindingSha256,
|
|
878
|
+
priorHead,
|
|
879
|
+
purgedAt,
|
|
880
|
+
spaceId: binding.spaceId,
|
|
881
|
+
v: 1
|
|
882
|
+
};
|
|
883
|
+
return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) });
|
|
884
|
+
}
|
|
885
|
+
function parseOhSpacePurgeReceiptV1(value) {
|
|
886
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
887
|
+
"bindingSha256",
|
|
888
|
+
"priorHead",
|
|
889
|
+
"purgedAt",
|
|
890
|
+
"receiptSha256",
|
|
891
|
+
"spaceId",
|
|
892
|
+
"v"
|
|
893
|
+
]) || value.v !== 1)
|
|
894
|
+
return null;
|
|
895
|
+
const bindingSha256 = parseSha256Hex(value.bindingSha256);
|
|
896
|
+
const priorHead = parseOhHeadV1(value.priorHead);
|
|
897
|
+
const purgedAt = parseCanonicalInstantV1(value.purgedAt);
|
|
898
|
+
const receiptSha256 = parseSha256Hex(value.receiptSha256);
|
|
899
|
+
const spaceId = safeCode(value.spaceId);
|
|
900
|
+
if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null)
|
|
901
|
+
return null;
|
|
902
|
+
const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 };
|
|
903
|
+
return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
class OhSemanticBundleIngressV1 {
|
|
907
|
+
#codecs;
|
|
908
|
+
#store;
|
|
909
|
+
constructor(store, codecs) {
|
|
910
|
+
this.#store = store;
|
|
911
|
+
this.#codecs = codecs.seal();
|
|
912
|
+
}
|
|
913
|
+
async commit(value) {
|
|
914
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, [
|
|
915
|
+
"actorId",
|
|
916
|
+
"expectedHead",
|
|
917
|
+
"instant",
|
|
918
|
+
"operationId",
|
|
919
|
+
"puts",
|
|
920
|
+
"tombstones",
|
|
921
|
+
"v"
|
|
922
|
+
]) || 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) {
|
|
923
|
+
throw new TypeError("Invalid semantic bundle.");
|
|
924
|
+
}
|
|
925
|
+
const actorId = safeCode(value.actorId);
|
|
926
|
+
const operationId = safeCode(value.operationId);
|
|
927
|
+
const expected = value.expectedHead;
|
|
928
|
+
const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant);
|
|
929
|
+
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)
|
|
930
|
+
throw new TypeError("Invalid semantic bundle identity.");
|
|
931
|
+
const changes = [];
|
|
932
|
+
for (const item of value.puts) {
|
|
933
|
+
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)) {
|
|
934
|
+
throw new TypeError("Invalid semantic bundle put.");
|
|
935
|
+
}
|
|
936
|
+
const parsed = this.#codecs.parseRequired(item.kind, item.value);
|
|
937
|
+
if (parsed === null)
|
|
938
|
+
throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`);
|
|
939
|
+
const record = createKnowledgeGraphRecordV1({
|
|
940
|
+
dependencies: item.dependencies,
|
|
941
|
+
key: item.key,
|
|
942
|
+
kind: item.kind,
|
|
943
|
+
v: 1,
|
|
944
|
+
value: parsed
|
|
945
|
+
});
|
|
946
|
+
changes.push({ kind: "put", record, v: 1 });
|
|
947
|
+
}
|
|
948
|
+
for (const item of value.tombstones) {
|
|
949
|
+
if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) {
|
|
950
|
+
throw new TypeError("Invalid semantic bundle tombstone.");
|
|
951
|
+
}
|
|
952
|
+
const priorSha256 = parseSha256Hex(item.priorSha256);
|
|
953
|
+
if (typeof item.key !== "string" || priorSha256 === null)
|
|
954
|
+
throw new TypeError("Invalid semantic bundle tombstone.");
|
|
955
|
+
changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 });
|
|
956
|
+
}
|
|
957
|
+
const canonical = canonicalKnowledgeGraphChangesV1(changes);
|
|
958
|
+
return await this.#store.commit({
|
|
959
|
+
actorId,
|
|
960
|
+
changes: canonical,
|
|
961
|
+
expectedHead: {
|
|
962
|
+
generation: expected.generation,
|
|
963
|
+
operationSha256: expected.operationSha256
|
|
964
|
+
},
|
|
965
|
+
...typeof instant === "string" ? { instant } : {},
|
|
966
|
+
operationId
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
export {
|
|
971
|
+
verifyOhDependencyClosureV1,
|
|
972
|
+
verifyOhDependencyClosureAgainstV1,
|
|
973
|
+
transitionOhSnapshotV1,
|
|
974
|
+
replayOhOperationsV1,
|
|
975
|
+
parseOhStoreProfileV1,
|
|
976
|
+
parseOhStoreBindingV1,
|
|
977
|
+
parseOhSpacePurgeReceiptV1,
|
|
978
|
+
parseOhHeadV1,
|
|
979
|
+
parseOhHeadRefV1,
|
|
980
|
+
parseOhDependencyClosureV1,
|
|
981
|
+
emptyOhHeadV1,
|
|
982
|
+
createOhStoreProfileV1,
|
|
983
|
+
createOhStoreBindingV1,
|
|
984
|
+
createOhSpacePurgeReceiptV1,
|
|
985
|
+
createOhDependencyClosureV1,
|
|
986
|
+
OhSemanticBundleIngressV1,
|
|
987
|
+
OhRecordCodecRegistry,
|
|
988
|
+
OhPurgedSpaceError,
|
|
989
|
+
OhProfileError,
|
|
990
|
+
OhIntegrityError,
|
|
991
|
+
OhDependencyError,
|
|
992
|
+
OhConflictError,
|
|
993
|
+
OH_WORKING_STORE_PROFILE_V1,
|
|
994
|
+
OH_DEPENDENCY_CLOSURE_LIMITS_V1,
|
|
995
|
+
OH_CANONICAL_STORE_PROFILE_V1
|
|
996
|
+
};
|