@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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +598 -0
  3. package/dist/canonical.d.ts +32 -0
  4. package/dist/canonical.d.ts.map +1 -0
  5. package/dist/cli.d.ts +4 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +3419 -0
  8. package/dist/contract.d.ts +33 -0
  9. package/dist/contract.d.ts.map +1 -0
  10. package/dist/graph.d.ts +67 -0
  11. package/dist/graph.d.ts.map +1 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +1988 -0
  15. package/dist/libsql.d.ts +57 -0
  16. package/dist/libsql.d.ts.map +1 -0
  17. package/dist/libsql.js +2662 -0
  18. package/dist/memory.d.ts +366 -0
  19. package/dist/memory.d.ts.map +1 -0
  20. package/dist/memory.js +3650 -0
  21. package/dist/ontology.d.ts +242 -0
  22. package/dist/ontology.d.ts.map +1 -0
  23. package/dist/operation.d.ts +24 -0
  24. package/dist/operation.d.ts.map +1 -0
  25. package/dist/projection-public.d.ts +59 -0
  26. package/dist/projection-public.d.ts.map +1 -0
  27. package/dist/projection-public.js +1682 -0
  28. package/dist/projection-suss.d.ts +17 -0
  29. package/dist/projection-suss.d.ts.map +1 -0
  30. package/dist/projection-suss.js +1721 -0
  31. package/dist/projection.d.ts +315 -0
  32. package/dist/projection.d.ts.map +1 -0
  33. package/dist/schema.d.ts +45 -0
  34. package/dist/schema.d.ts.map +1 -0
  35. package/dist/sdk.d.ts +51 -0
  36. package/dist/sdk.d.ts.map +1 -0
  37. package/dist/sdk.js +3072 -0
  38. package/dist/search.d.ts +34 -0
  39. package/dist/search.d.ts.map +1 -0
  40. package/dist/semantic.d.ts +83 -0
  41. package/dist/semantic.d.ts.map +1 -0
  42. package/dist/semantic.js +706 -0
  43. package/dist/sqlite/driver.d.ts +6 -0
  44. package/dist/sqlite/driver.d.ts.map +1 -0
  45. package/dist/sqlite/index.d.ts +5 -0
  46. package/dist/sqlite/index.d.ts.map +1 -0
  47. package/dist/sqlite/index.js +2840 -0
  48. package/dist/sqlite/migrations.d.ts +10 -0
  49. package/dist/sqlite/migrations.d.ts.map +1 -0
  50. package/dist/sqlite/port.d.ts +40 -0
  51. package/dist/sqlite/port.d.ts.map +1 -0
  52. package/dist/sqlite/runtime.d.ts +23 -0
  53. package/dist/sqlite/runtime.d.ts.map +1 -0
  54. package/dist/sqlite/store.d.ts +85 -0
  55. package/dist/sqlite/store.d.ts.map +1 -0
  56. package/dist/store.d.ts +236 -0
  57. package/dist/store.d.ts.map +1 -0
  58. package/dist/store.js +996 -0
  59. package/dist/sync.d.ts +57 -0
  60. package/dist/sync.d.ts.map +1 -0
  61. package/dist/sync.js +1328 -0
  62. package/package.json +121 -0
  63. package/skills/oh/SKILL.md +206 -0
  64. package/skills/oh/agents/openai.yaml +4 -0
  65. package/spec/README.md +74 -0
  66. package/spec/manifest.json +41 -0
  67. package/spec/v1/canonical-json.md +59 -0
  68. package/spec/v1/contract.json +28 -0
  69. package/spec/v1/contract.schema.json +58 -0
  70. package/spec/v1/embedding-profile.json +11 -0
  71. package/spec/v1/embedding.md +56 -0
  72. package/spec/v1/graph.md +87 -0
  73. package/spec/v1/memory.md +193 -0
  74. package/spec/v1/migration.md +92 -0
  75. package/spec/v1/ontology.json +55 -0
  76. package/spec/v1/ontology.md +80 -0
  77. package/spec/v1/operation.schema.json +138 -0
  78. package/spec/v1/projection-identity.schema.json +58 -0
  79. package/spec/v1/projection-query.schema.json +60 -0
  80. package/spec/v1/projection-result.schema.json +452 -0
  81. package/spec/v1/projection-rule-pack.schema.json +182 -0
  82. package/spec/v1/projection.md +165 -0
  83. package/spec/v1/record.schema.json +95 -0
  84. package/spec/v1/schema-evolution.md +51 -0
  85. package/spec/v1/schema-revision.schema.json +178 -0
  86. package/spec/v1/storage.md +88 -0
  87. package/spec/v1/store.md +131 -0
  88. package/spec/v1/sync-bundle.schema.json +51 -0
  89. package/spec/v1/sync.md +67 -0
  90. package/src/canonical.test.ts +46 -0
  91. package/src/canonical.ts +203 -0
  92. package/src/cli.test.ts +103 -0
  93. package/src/cli.ts +308 -0
  94. package/src/contract.ts +87 -0
  95. package/src/contracts.test.ts +147 -0
  96. package/src/graph.ts +248 -0
  97. package/src/index.ts +8 -0
  98. package/src/libsql.test.ts +657 -0
  99. package/src/libsql.ts +1687 -0
  100. package/src/memory.test.ts +783 -0
  101. package/src/memory.ts +1684 -0
  102. package/src/ontology.ts +573 -0
  103. package/src/operation.ts +80 -0
  104. package/src/projection-public.ts +53 -0
  105. package/src/projection-suss.ts +129 -0
  106. package/src/projection.test.ts +418 -0
  107. package/src/projection.ts +1457 -0
  108. package/src/schema.ts +156 -0
  109. package/src/sdk.ts +96 -0
  110. package/src/search.ts +66 -0
  111. package/src/semantic.test.ts +480 -0
  112. package/src/semantic.ts +333 -0
  113. package/src/sqlite/driver.ts +47 -0
  114. package/src/sqlite/index.ts +4 -0
  115. package/src/sqlite/migrations.test.ts +44 -0
  116. package/src/sqlite/migrations.ts +178 -0
  117. package/src/sqlite/port.test.ts +127 -0
  118. package/src/sqlite/port.ts +120 -0
  119. package/src/sqlite/runtime.test.ts +68 -0
  120. package/src/sqlite/runtime.ts +53 -0
  121. package/src/sqlite/store.test.ts +295 -0
  122. package/src/sqlite/store.ts +988 -0
  123. package/src/store.test.ts +121 -0
  124. package/src/store.ts +701 -0
  125. package/src/sync.test.ts +117 -0
  126. package/src/sync.ts +227 -0
@@ -0,0 +1,706 @@
1
+ // @bun
2
+ // src/semantic.ts
3
+ import { mkdir, readFile, readdir, rename, unlink, writeFile } from "fs/promises";
4
+ import { join, resolve } from "path";
5
+
6
+ // src/canonical.ts
7
+ import { createHash, randomBytes } from "crypto";
8
+
9
+ class OhValidationError extends Error {
10
+ code;
11
+ path;
12
+ constructor(code, path, message) {
13
+ super(`${path}: ${message}`);
14
+ this.name = "OhValidationError";
15
+ this.code = code;
16
+ this.path = path;
17
+ }
18
+ }
19
+ function isPlainRecord(value) {
20
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21
+ return false;
22
+ const prototype = Object.getPrototypeOf(value);
23
+ return prototype === Object.prototype || prototype === null;
24
+ }
25
+ function hasExactKeys(value, keys) {
26
+ const actual = Object.keys(value);
27
+ return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
28
+ }
29
+ function assertUnicodeScalarString(value, path) {
30
+ for (let index = 0;index < value.length; index += 1) {
31
+ const code = value.charCodeAt(index);
32
+ if (code >= 55296 && code <= 56319) {
33
+ const next = value.charCodeAt(index + 1);
34
+ if (!(next >= 56320 && next <= 57343)) {
35
+ throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
36
+ }
37
+ index += 1;
38
+ } else if (code >= 56320 && code <= 57343) {
39
+ throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate");
40
+ }
41
+ }
42
+ }
43
+ function encodeCanonical(value, path, ancestors) {
44
+ if (value === null || typeof value === "boolean")
45
+ return JSON.stringify(value);
46
+ if (typeof value === "string") {
47
+ assertUnicodeScalarString(value, path);
48
+ return JSON.stringify(value);
49
+ }
50
+ if (typeof value === "number") {
51
+ if (!Number.isFinite(value)) {
52
+ throw new OhValidationError("non-json-number", path, "must be finite");
53
+ }
54
+ if (Object.is(value, -0)) {
55
+ throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical");
56
+ }
57
+ return JSON.stringify(value);
58
+ }
59
+ if (typeof value !== "object" || value === null) {
60
+ throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`);
61
+ }
62
+ if (ancestors.has(value)) {
63
+ throw new OhValidationError("cycle", path, "contains a cycle");
64
+ }
65
+ ancestors.add(value);
66
+ try {
67
+ if (Array.isArray(value)) {
68
+ const encoded = [];
69
+ for (let index = 0;index < value.length; index += 1) {
70
+ if (!Object.hasOwn(value, index)) {
71
+ throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
72
+ }
73
+ encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors));
74
+ }
75
+ const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length));
76
+ if (extraKeys.length > 0) {
77
+ throw new OhValidationError("non-json-property", path, "array has non-index properties");
78
+ }
79
+ return `[${encoded.join(",")}]`;
80
+ }
81
+ if (!isPlainRecord(value)) {
82
+ throw new OhValidationError("non-plain-object", path, "must be a plain object");
83
+ }
84
+ const ownKeys = Reflect.ownKeys(value);
85
+ if (ownKeys.some((key) => typeof key !== "string")) {
86
+ throw new OhValidationError("non-json-property", path, "object has a symbol property");
87
+ }
88
+ const keys = ownKeys;
89
+ for (const key of keys) {
90
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
91
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
92
+ throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
93
+ }
94
+ }
95
+ keys.sort();
96
+ const entries = keys.map((key) => {
97
+ assertUnicodeScalarString(key, `${path}.<key>`);
98
+ return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`;
99
+ });
100
+ return `{${entries.join(",")}}`;
101
+ } finally {
102
+ ancestors.delete(value);
103
+ }
104
+ }
105
+ function canonicalJson(value) {
106
+ return encodeCanonical(value, "$", new Set);
107
+ }
108
+ function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) {
109
+ if (utf8ByteLength(text) > maximumBytes) {
110
+ throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit");
111
+ }
112
+ let value;
113
+ try {
114
+ value = JSON.parse(text);
115
+ } catch {
116
+ throw new OhValidationError("invalid-json", "$", "is not valid JSON");
117
+ }
118
+ if (canonicalJson(value) !== text) {
119
+ throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical");
120
+ }
121
+ return value;
122
+ }
123
+ function utf8ByteLength(value) {
124
+ return Buffer.byteLength(value, "utf8");
125
+ }
126
+ function sha256Hex(value) {
127
+ return createHash("sha256").update(value).digest("hex");
128
+ }
129
+ function canonicalSha256(value) {
130
+ return sha256Hex(canonicalJson(value));
131
+ }
132
+ function parseSha256Hex(value) {
133
+ return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null;
134
+ }
135
+ function parseCanonicalInstantV1(value) {
136
+ 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)) {
137
+ return null;
138
+ }
139
+ const timestamp = Date.parse(value);
140
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null;
141
+ }
142
+ function canonicalNow() {
143
+ return new Date().toISOString();
144
+ }
145
+ function opaqueId(prefix) {
146
+ if (!/^[a-z][a-z0-9_]{1,15}$/u.test(prefix)) {
147
+ throw new OhValidationError("invalid-prefix", "prefix", "must be a short lowercase code");
148
+ }
149
+ return `${prefix}${randomBytes(12).toString("hex")}`;
150
+ }
151
+ function safeCode(value, maximumLength = 128) {
152
+ return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
153
+ }
154
+ function boundedText(value, maximumBytes = 64 * 1024) {
155
+ if (typeof value !== "string" || value.length === 0 || value.normalize("NFC") !== value || utf8ByteLength(value) > maximumBytes)
156
+ return null;
157
+ try {
158
+ assertUnicodeScalarString(value, "$text");
159
+ } catch {
160
+ return null;
161
+ }
162
+ for (const character of value) {
163
+ const code = character.codePointAt(0) ?? 0;
164
+ if (code <= 8 || code >= 11 && code <= 12 || code >= 14 && code <= 31 || code >= 127 && code <= 159)
165
+ return null;
166
+ }
167
+ return value;
168
+ }
169
+ function orderedUnique(values, key) {
170
+ return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value));
171
+ }
172
+ function sortUnique(values, key) {
173
+ const sorted = [...values].sort((left, right) => {
174
+ const leftKey = key(left);
175
+ const rightKey = key(right);
176
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
177
+ });
178
+ if (!orderedUnique(sorted, key)) {
179
+ throw new OhValidationError("duplicate", "$", "contains duplicate canonical values");
180
+ }
181
+ return sorted;
182
+ }
183
+
184
+ // src/graph.ts
185
+ var OH_GRAPH_FORMAT_VERSION_V1 = 1;
186
+ var OH_GRAPH_LIMITS_V1 = Object.freeze({
187
+ changesPerOperation: 8192,
188
+ dependenciesPerRecord: 4096,
189
+ recordBytes: 1024 * 1024,
190
+ recordsPerSnapshot: 65536
191
+ });
192
+ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
193
+ "activity",
194
+ "assertion",
195
+ "context",
196
+ "dependency-manifest",
197
+ "edition",
198
+ "entity",
199
+ "evidence",
200
+ "identity-operation",
201
+ "inquiry",
202
+ "inquiry-event",
203
+ "review-decision",
204
+ "rights-decision",
205
+ "schema",
206
+ "shape",
207
+ "statement",
208
+ "type-membership",
209
+ "view",
210
+ "vocabulary"
211
+ ];
212
+ function recordKey(value) {
213
+ return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
214
+ }
215
+ function createKnowledgeGraphRecordV1(input) {
216
+ if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies))
217
+ throw new TypeError("Invalid graph record input.");
218
+ const key = recordKey(input.key);
219
+ const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
220
+ if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
221
+ throw new TypeError("Invalid graph record identity.");
222
+ const dependencies = input.dependencies.map(recordKey);
223
+ if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
224
+ throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
225
+ }
226
+ const valueJson = canonicalJson(input.value);
227
+ if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) {
228
+ throw new RangeError("Graph record value exceeds its canonical byte limit.");
229
+ }
230
+ const payload = { dependencies, key, kind, v: 1, value: input.value };
231
+ return { ...payload, recordSha256: canonicalSha256(payload) };
232
+ }
233
+ function parseKnowledgeGraphRecordV1(value) {
234
+ if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256"))
235
+ return null;
236
+ const recordSha256 = parseSha256Hex(value.recordSha256);
237
+ const { recordSha256: _digest, ...input } = value;
238
+ try {
239
+ const created = createKnowledgeGraphRecordV1(input);
240
+ return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+ function knowledgeGraphRecordRefV1(record) {
246
+ return {
247
+ dependencies: record.dependencies,
248
+ key: record.key,
249
+ kind: record.kind,
250
+ sha256: record.recordSha256,
251
+ v: 1
252
+ };
253
+ }
254
+ function changeKey(change) {
255
+ return change.kind === "put" ? change.record.key : change.key;
256
+ }
257
+ function canonicalKnowledgeGraphChangesV1(changes) {
258
+ const normalized = [];
259
+ for (const change of changes) {
260
+ if (!isPlainRecord(change) || change.v !== 1)
261
+ throw new TypeError("Invalid graph change.");
262
+ if (change.kind === "put") {
263
+ const record = parseKnowledgeGraphRecordV1(change.record);
264
+ if (record === null)
265
+ throw new TypeError("Invalid graph record in change.");
266
+ normalized.push({ kind: "put", record, v: 1 });
267
+ } else if (change.kind === "tombstone") {
268
+ const key = recordKey(change.key);
269
+ const priorSha256 = parseSha256Hex(change.priorSha256);
270
+ if (key === null || priorSha256 === null)
271
+ throw new TypeError("Invalid graph tombstone.");
272
+ normalized.push({ key, kind: "tombstone", priorSha256, v: 1 });
273
+ } else
274
+ throw new TypeError("Unknown graph change kind.");
275
+ }
276
+ return sortUnique(normalized, changeKey);
277
+ }
278
+ function graphRevisionSha256V1(input) {
279
+ const changes = canonicalKnowledgeGraphChangesV1(input.changes);
280
+ const operationId = safeCode(input.operationId);
281
+ const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256);
282
+ const recordsSha256 = parseSha256Hex(input.recordsSha256);
283
+ const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null;
284
+ 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)) {
285
+ throw new TypeError("Invalid graph revision digest input.");
286
+ }
287
+ return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 });
288
+ }
289
+ function createKnowledgeGraphRevisionV1(input) {
290
+ if (input.parent !== null && parseKnowledgeGraphRevisionV1(input.parent) === null) {
291
+ throw new TypeError("Invalid parent graph revision.");
292
+ }
293
+ const operationId = safeCode(input.operationId);
294
+ const changes = canonicalKnowledgeGraphChangesV1(input.changes);
295
+ if (operationId === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation)
296
+ throw new TypeError("Invalid graph revision.");
297
+ const byKey = new Map((input.parent?.recordRefs ?? []).map((ref) => [ref.key, ref]));
298
+ for (const change of changes) {
299
+ if (change.kind === "put") {
300
+ for (const dependency of change.record.dependencies) {
301
+ if (!byKey.has(dependency) && !changes.some((candidate) => candidate.kind === "put" && candidate.record.key === dependency)) {
302
+ throw new TypeError(`Missing graph dependency: ${dependency}`);
303
+ }
304
+ }
305
+ byKey.set(change.record.key, knowledgeGraphRecordRefV1(change.record));
306
+ } else {
307
+ const prior = byKey.get(change.key);
308
+ if (prior === undefined || prior.sha256 !== change.priorSha256)
309
+ throw new TypeError("Tombstone prior digest does not match.");
310
+ byKey.delete(change.key);
311
+ }
312
+ }
313
+ if (byKey.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) {
314
+ throw new RangeError("Graph revision exceeds its record snapshot limit.");
315
+ }
316
+ for (const ref of byKey.values()) {
317
+ if (ref.dependencies.some((dependency) => !byKey.has(dependency))) {
318
+ throw new TypeError(`Missing graph dependency after revision: ${ref.key}`);
319
+ }
320
+ }
321
+ const recordRefs = [...byKey.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
322
+ const recordsSha256 = canonicalSha256(recordRefs);
323
+ const payload = {
324
+ changes,
325
+ operationId,
326
+ parentGraphRevisionSha256: input.parent?.graphRevisionSha256 ?? null,
327
+ recordRefs,
328
+ recordsSha256,
329
+ revision: (input.parent?.revision ?? 0) + 1,
330
+ v: 1
331
+ };
332
+ return { ...payload, graphRevisionSha256: graphRevisionSha256V1(payload) };
333
+ }
334
+ function parseKnowledgeGraphRevisionV1(value) {
335
+ if (!isPlainRecord(value) || !hasExactKeys(value, [
336
+ "changes",
337
+ "graphRevisionSha256",
338
+ "operationId",
339
+ "parentGraphRevisionSha256",
340
+ "recordRefs",
341
+ "recordsSha256",
342
+ "revision",
343
+ "v"
344
+ ]) || value.v !== 1 || !Array.isArray(value.changes) || !Array.isArray(value.recordRefs) || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot)
345
+ return null;
346
+ const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256);
347
+ const recordsSha256 = parseSha256Hex(value.recordsSha256);
348
+ const parentGraphRevisionSha256 = value.parentGraphRevisionSha256 === null ? null : parseSha256Hex(value.parentGraphRevisionSha256);
349
+ const operationId = safeCode(value.operationId);
350
+ const revision = Number.isSafeInteger(value.revision) && value.revision > 0 ? value.revision : null;
351
+ let changes;
352
+ try {
353
+ changes = canonicalKnowledgeGraphChangesV1(value.changes);
354
+ } catch {
355
+ return null;
356
+ }
357
+ const refs = [];
358
+ for (const item of value.recordRefs) {
359
+ if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "sha256", "v"]) || item.v !== 1 || !Array.isArray(item.dependencies))
360
+ return null;
361
+ const dependencies = item.dependencies.map(recordKey);
362
+ const key = recordKey(item.key);
363
+ const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === item.kind);
364
+ const sha256 = parseSha256Hex(item.sha256);
365
+ 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))
366
+ return null;
367
+ refs.push({ dependencies, key, kind, sha256, v: 1 });
368
+ }
369
+ if (graphRevisionSha256 === null || recordsSha256 === null || operationId === null || revision === null || value.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || !orderedUnique(refs, (ref) => ref.key) || canonicalSha256(refs) !== recordsSha256)
370
+ return null;
371
+ const keys = new Set(refs.map((ref) => ref.key));
372
+ if (refs.some((ref) => ref.dependencies.some((dependency) => !keys.has(dependency))))
373
+ return null;
374
+ const payload = {
375
+ changes,
376
+ operationId,
377
+ parentGraphRevisionSha256,
378
+ recordRefs: refs,
379
+ recordsSha256,
380
+ revision,
381
+ v: 1
382
+ };
383
+ try {
384
+ return graphRevisionSha256V1(payload) === graphRevisionSha256 ? { ...payload, graphRevisionSha256 } : null;
385
+ } catch {
386
+ return null;
387
+ }
388
+ }
389
+ function reduceKnowledgeGraphRevisionsV1(revisions) {
390
+ if (revisions.length === 0 || revisions.length > 65536)
391
+ return null;
392
+ const ordered = [...revisions].sort((left, right) => left.revision - right.revision);
393
+ let parent = null;
394
+ const operationIds = new Set;
395
+ for (const candidate of ordered) {
396
+ const current = parseKnowledgeGraphRevisionV1(candidate);
397
+ if (current === null || current.revision !== (parent?.revision ?? 0) + 1 || current.parentGraphRevisionSha256 !== (parent?.graphRevisionSha256 ?? null) || operationIds.has(current.operationId))
398
+ return null;
399
+ try {
400
+ const rebuilt = createKnowledgeGraphRevisionV1({ changes: current.changes, operationId: current.operationId, parent });
401
+ if (rebuilt.graphRevisionSha256 !== current.graphRevisionSha256)
402
+ return null;
403
+ } catch {
404
+ return null;
405
+ }
406
+ operationIds.add(current.operationId);
407
+ parent = current;
408
+ }
409
+ return parent;
410
+ }
411
+
412
+ // src/semantic.ts
413
+ var OH_EMBEDDING_PROFILE_V1 = Object.freeze({
414
+ dimensions: 768,
415
+ distance: "cosine",
416
+ documentation: "https://ai.google.dev/gemma/docs/embeddinggemma",
417
+ documentFormat: "title: {title} | text: {content}",
418
+ engine: "@tobilu/qmd@2.5.3",
419
+ model: "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf",
420
+ normalization: "l2",
421
+ queryFormat: "task: search result | query: {query}",
422
+ v: 1
423
+ });
424
+ function formatOhEmbeddingQueryV1(query) {
425
+ return `task: search result | query: ${query}`;
426
+ }
427
+ function formatOhEmbeddingDocumentV1(title, content) {
428
+ return `title: ${title} | text: ${content}`;
429
+ }
430
+ function normalizeOhEmbeddingV1(vector) {
431
+ if (vector.length !== OH_EMBEDDING_PROFILE_V1.dimensions || vector.some((component) => !Number.isFinite(component))) {
432
+ throw new TypeError(`Embedding vectors must contain ${OH_EMBEDDING_PROFILE_V1.dimensions} finite values.`);
433
+ }
434
+ const magnitude = Math.sqrt(vector.reduce((sum, component) => sum + component * component, 0));
435
+ if (magnitude === 0)
436
+ throw new TypeError("Embedding vectors must have nonzero magnitude.");
437
+ return vector.map((component) => component / magnitude);
438
+ }
439
+ function cosineSimilarityV1(left, right) {
440
+ const normalizedLeft = normalizeOhEmbeddingV1(left);
441
+ const normalizedRight = normalizeOhEmbeddingV1(right);
442
+ return normalizedLeft.reduce((sum, component, index) => sum + component * normalizedRight[index], 0);
443
+ }
444
+ var qmdModuleSpecifier = "@tobilu/qmd";
445
+ async function defaultQmdStoreFactory(options) {
446
+ let module;
447
+ try {
448
+ module = await import(qmdModuleSpecifier);
449
+ } catch {
450
+ throw new Error("Semantic search needs the optional @tobilu/qmd@2.5.3 package.");
451
+ }
452
+ const createStore = module.createStore;
453
+ if (typeof createStore !== "function")
454
+ throw new Error("The installed QMD package has no compatible createStore export.");
455
+ return await createStore(options);
456
+ }
457
+ function recordDocument(record) {
458
+ return `# ${record.key}
459
+
460
+ kind: ${record.kind}
461
+
462
+ ${canonicalJson(record.value)}
463
+ `;
464
+ }
465
+ var QMD_VECTOR_RESULT_KEYS = [
466
+ "body",
467
+ "bodyLength",
468
+ "chunkPos",
469
+ "collectionName",
470
+ "context",
471
+ "displayPath",
472
+ "docid",
473
+ "filepath",
474
+ "hash",
475
+ "modifiedAt",
476
+ "score",
477
+ "source",
478
+ "title"
479
+ ];
480
+ function semanticManifest(entries) {
481
+ const immutableEntries = {};
482
+ for (const [filename, entry] of Object.entries(entries)) {
483
+ immutableEntries[filename] = Object.freeze({ ...entry });
484
+ }
485
+ return Object.freeze({
486
+ entries: Object.freeze(immutableEntries),
487
+ profileSha256: canonicalSha256(OH_EMBEDDING_PROFILE_V1),
488
+ v: 1
489
+ });
490
+ }
491
+ function parseQmdVectorResult(value) {
492
+ if (!isPlainRecord(value) || !hasExactKeys(value, QMD_VECTOR_RESULT_KEYS))
493
+ return null;
494
+ const pathMatch = typeof value.filepath === "string" ? /^qmd:\/\/oh\/([a-f0-9]{64}\.md)$/u.exec(value.filepath) : null;
495
+ const filename = pathMatch?.[1];
496
+ const hash = parseSha256Hex(value.hash);
497
+ const title = safeCode(value.title, 512);
498
+ if (filename === undefined || value.displayPath !== `oh/${filename}` || value.collectionName !== "oh" || value.source !== "vec" || value.context !== null || value.modifiedAt !== "" || hash === null || value.docid !== hash.slice(0, 6) || title === null || typeof value.body !== "string" || typeof value.bodyLength !== "number" || !Number.isSafeInteger(value.bodyLength) || value.bodyLength < 0 || value.bodyLength > OH_GRAPH_LIMITS_V1.recordBytes + 4096 || value.body.length !== value.bodyLength || hash !== sha256Hex(value.body) || typeof value.chunkPos !== "number" || !Number.isSafeInteger(value.chunkPos) || value.chunkPos < 0 || typeof value.score !== "number" || !Number.isFinite(value.score) || value.score < 0 || value.score > 1) {
499
+ return null;
500
+ }
501
+ return { body: value.body, filename, hash, score: value.score, title };
502
+ }
503
+ function parseQmdVectorResults(value) {
504
+ if (!Array.isArray(value) || value.length > 100) {
505
+ throw new Error("QMD returned an invalid vector result batch.");
506
+ }
507
+ return value.map(parseQmdVectorResult).filter((result) => result !== null);
508
+ }
509
+
510
+ class OhQmdSemanticBackendV1 {
511
+ profile = OH_EMBEDDING_PROFILE_V1;
512
+ #cacheDirectory;
513
+ #databasePath;
514
+ #factory;
515
+ #manifest = semanticManifest({});
516
+ #manifestLoad = null;
517
+ #store = null;
518
+ #storeClose = null;
519
+ #closure = null;
520
+ #indexQueue = Promise.resolve();
521
+ #activeSearches = new Set;
522
+ #closed = false;
523
+ constructor(options) {
524
+ this.#cacheDirectory = resolve(options.cacheDirectory);
525
+ this.#databasePath = resolve(options.databasePath ?? join(this.#cacheDirectory, "qmd.sqlite"));
526
+ this.#factory = options.storeFactory ?? defaultQmdStoreFactory;
527
+ }
528
+ async#open() {
529
+ if (this.#closed)
530
+ throw new Error("The semantic backend is closed.");
531
+ this.#store ??= this.#initializeStore();
532
+ const store = await this.#store;
533
+ if (this.#closed) {
534
+ await this.#closeStore(store);
535
+ throw new Error("The semantic backend is closed.");
536
+ }
537
+ return store;
538
+ }
539
+ async#initializeStore() {
540
+ const documents = join(this.#cacheDirectory, "documents");
541
+ await mkdir(documents, { recursive: true });
542
+ await this.#loadManifest();
543
+ if (this.#closed)
544
+ throw new Error("The semantic backend is closed.");
545
+ const store = await this.#factory({
546
+ dbPath: this.#databasePath,
547
+ config: {
548
+ collections: { oh: { path: documents, pattern: "*.md" } },
549
+ models: { embed: OH_EMBEDDING_PROFILE_V1.model }
550
+ }
551
+ });
552
+ if (this.#closed) {
553
+ await this.#closeStore(store);
554
+ throw new Error("The semantic backend is closed.");
555
+ }
556
+ return store;
557
+ }
558
+ #closeStore(store) {
559
+ this.#storeClose ??= Promise.resolve().then(() => store.close());
560
+ return this.#storeClose;
561
+ }
562
+ #loadManifest() {
563
+ this.#manifestLoad ??= this.#readManifest();
564
+ return this.#manifestLoad;
565
+ }
566
+ async#readManifest() {
567
+ let text;
568
+ try {
569
+ text = await readFile(join(this.#cacheDirectory, "manifest.json"), "utf8");
570
+ } catch (error) {
571
+ if (error.code === "ENOENT")
572
+ return;
573
+ throw error;
574
+ }
575
+ let value;
576
+ try {
577
+ value = JSON.parse(text);
578
+ } catch {
579
+ throw new Error("The semantic manifest is not JSON.");
580
+ }
581
+ if (canonicalJson(value) !== text || !isPlainRecord(value) || !hasExactKeys(value, ["entries", "profileSha256", "v"]) || value.v !== 1 || value.profileSha256 !== canonicalSha256(OH_EMBEDDING_PROFILE_V1) || !isPlainRecord(value.entries) || Object.keys(value.entries).length > 65536)
582
+ throw new Error("The semantic manifest is incompatible or invalid.");
583
+ const entries = {};
584
+ for (const [filename, candidate] of Object.entries(value.entries)) {
585
+ if (!/^[a-f0-9]{64}\.md$/u.test(filename) || !isPlainRecord(candidate) || !hasExactKeys(candidate, ["key", "recordSha256"]))
586
+ throw new Error("The semantic manifest has an invalid entry.");
587
+ const key = safeCode(candidate.key, 512);
588
+ const recordSha256 = parseSha256Hex(candidate.recordSha256);
589
+ if (key === null || recordSha256 === null || filename !== `${sha256Hex(key)}.md`) {
590
+ throw new Error("The semantic manifest entry identity is invalid.");
591
+ }
592
+ entries[filename] = { key, recordSha256 };
593
+ }
594
+ this.#manifest = semanticManifest(entries);
595
+ }
596
+ index(records) {
597
+ if (records.length > 65536) {
598
+ return Promise.reject(new RangeError("A semantic snapshot may contain at most 65,536 records."));
599
+ }
600
+ if (this.#closed)
601
+ return Promise.reject(new Error("The semantic backend is closed."));
602
+ const snapshot = [...records];
603
+ const operation = this.#indexQueue.then(() => this.#indexSnapshot(snapshot));
604
+ this.#indexQueue = operation.then(() => {
605
+ return;
606
+ }, () => {
607
+ return;
608
+ });
609
+ return operation;
610
+ }
611
+ async#indexSnapshot(records) {
612
+ if (this.#closed)
613
+ throw new Error("The semantic backend is closed.");
614
+ const documents = join(this.#cacheDirectory, "documents");
615
+ await mkdir(documents, { recursive: true });
616
+ await this.#loadManifest();
617
+ const entries = {};
618
+ for (const record of records) {
619
+ const filename = `${sha256Hex(record.key)}.md`;
620
+ entries[filename] = { key: record.key, recordSha256: record.recordSha256 };
621
+ const path = join(documents, filename);
622
+ const temporary = `${path}.${process.pid}.tmp`;
623
+ await writeFile(temporary, recordDocument(record), { encoding: "utf8", mode: 384 });
624
+ await rename(temporary, path);
625
+ }
626
+ const retained = new Set(Object.keys(entries));
627
+ for (const filename of await readdir(documents)) {
628
+ if (/^[a-f0-9]{64}\.md$/u.test(filename) && !retained.has(filename))
629
+ await unlink(join(documents, filename));
630
+ }
631
+ const nextManifest = semanticManifest(entries);
632
+ const store = await this.#open();
633
+ await store.update({ collections: ["oh"] });
634
+ await store.embed({ collection: "oh", model: OH_EMBEDDING_PROFILE_V1.model });
635
+ const manifestPath = join(this.#cacheDirectory, "manifest.json");
636
+ const temporaryManifest = `${manifestPath}.${process.pid}.tmp`;
637
+ await writeFile(temporaryManifest, canonicalJson(nextManifest), { encoding: "utf8", mode: 384 });
638
+ await rename(temporaryManifest, manifestPath);
639
+ this.#manifest = nextManifest;
640
+ return { indexed: records.length, v: 1 };
641
+ }
642
+ search(query, limit, authority) {
643
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
644
+ return Promise.reject(new RangeError("Semantic limit must be 1 through 100."));
645
+ }
646
+ if (this.#closed)
647
+ return Promise.reject(new Error("The semantic backend is closed."));
648
+ const operation = this.#searchSnapshot(query, limit, authority);
649
+ this.#activeSearches.add(operation);
650
+ operation.then(() => {
651
+ this.#activeSearches.delete(operation);
652
+ }, () => {
653
+ this.#activeSearches.delete(operation);
654
+ });
655
+ return operation;
656
+ }
657
+ async#searchSnapshot(query, limit, authority) {
658
+ const store = await this.#open();
659
+ const manifest = this.#manifest;
660
+ const results = parseQmdVectorResults(await store.searchVector(query, { collection: "oh", limit: Math.min(100, limit * 3) }));
661
+ const output = [];
662
+ const seen = new Set;
663
+ for (const result of results) {
664
+ const entry = manifest.entries[result.filename];
665
+ if (entry === undefined || result.title !== entry.key || seen.has(entry.key))
666
+ continue;
667
+ const current = authority.get(entry.key);
668
+ if (current === null || current.recordSha256 !== entry.recordSha256)
669
+ continue;
670
+ const expectedDocument = recordDocument(current);
671
+ if (result.body !== expectedDocument || result.hash !== sha256Hex(expectedDocument))
672
+ continue;
673
+ seen.add(entry.key);
674
+ output.push({ key: entry.key, recordSha256: entry.recordSha256, score: result.score, v: 1 });
675
+ if (output.length === limit)
676
+ break;
677
+ }
678
+ return output;
679
+ }
680
+ async#finishClose() {
681
+ await this.#indexQueue;
682
+ await Promise.allSettled([...this.#activeSearches]);
683
+ if (this.#store === null)
684
+ return;
685
+ try {
686
+ const store = await this.#store;
687
+ await this.#closeStore(store);
688
+ } catch {
689
+ if (this.#storeClose !== null)
690
+ await this.#storeClose;
691
+ }
692
+ }
693
+ close() {
694
+ this.#closed = true;
695
+ this.#closure ??= this.#finishClose();
696
+ return this.#closure;
697
+ }
698
+ }
699
+ export {
700
+ normalizeOhEmbeddingV1,
701
+ formatOhEmbeddingQueryV1,
702
+ formatOhEmbeddingDocumentV1,
703
+ cosineSimilarityV1,
704
+ OhQmdSemanticBackendV1,
705
+ OH_EMBEDDING_PROFILE_V1
706
+ };