@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,480 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import { canonicalJson, canonicalSha256, sha256Hex } from "./canonical";
7
+ import { createKnowledgeGraphRecordV1 } from "./graph";
8
+ import { searchOhV1 } from "./search";
9
+ import { OH_EMBEDDING_PROFILE_V1, OhQmdSemanticBackendV1, cosineSimilarityV1,
10
+ formatOhEmbeddingDocumentV1, formatOhEmbeddingQueryV1, normalizeOhEmbeddingV1,
11
+ type QmdStoreFactoryV1 } from "./semantic";
12
+ import { OhSqliteStore } from "./sqlite/store";
13
+
14
+ const roots: string[] = [];
15
+ afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); });
16
+
17
+ const record = (name: string) => createKnowledgeGraphRecordV1({ dependencies: [], key: "entity:ada",
18
+ kind: "entity", v: 1, value: { name } });
19
+
20
+ function commit(store: OhSqliteStore, value: ReturnType<typeof record>, operationId: string): void {
21
+ store.commit({ actorId: "agent.test", changes: [{ kind: "put", record: value, v: 1 }],
22
+ expectedHead: store.head(), operationId });
23
+ }
24
+
25
+ function testRecordDocument(value: ReturnType<typeof record>): string {
26
+ return `# ${value.key}\n\nkind: ${value.kind}\n\n${canonicalJson(value.value)}\n`;
27
+ }
28
+
29
+ function qmdVectorResult(value: ReturnType<typeof record>,
30
+ overrides: Readonly<Record<string, unknown>> = {}): Readonly<Record<string, unknown>> {
31
+ const filename = `${sha256Hex(value.key)}.md`;
32
+ const body = testRecordDocument(value);
33
+ const hash = sha256Hex(body);
34
+ return {
35
+ body,
36
+ bodyLength: body.length,
37
+ chunkPos: 0,
38
+ collectionName: "oh",
39
+ context: null,
40
+ displayPath: `oh/${filename}`,
41
+ docid: hash.slice(0, 6),
42
+ filepath: `qmd://oh/${filename}`,
43
+ hash,
44
+ modifiedAt: "",
45
+ score: 0.9,
46
+ source: "vec",
47
+ title: value.key,
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ function deferred<T>(): Readonly<{ promise: Promise<T>; resolve: (value: T) => void }> {
53
+ let resolvePromise: ((value: T) => void) | undefined;
54
+ const promise = new Promise<T>((resolve) => { resolvePromise = resolve; });
55
+ return { promise, resolve: (value) => resolvePromise?.(value) };
56
+ }
57
+
58
+ async function writeManifest(root: string, value: ReturnType<typeof record>): Promise<void> {
59
+ const filename = `${sha256Hex(value.key)}.md`;
60
+ await writeFile(join(root, "manifest.json"), canonicalJson({
61
+ entries: { [filename]: { key: value.key, recordSha256: value.recordSha256 } },
62
+ profileSha256: canonicalSha256(OH_EMBEDDING_PROFILE_V1),
63
+ v: 1,
64
+ }), "utf8");
65
+ }
66
+
67
+ describe("local EmbeddingGemma profile", () => {
68
+ test("matches the pinned KB/QMD model and prompt contract", () => {
69
+ expect(OH_EMBEDDING_PROFILE_V1).toMatchObject({
70
+ dimensions: 768,
71
+ engine: "@tobilu/qmd@2.5.3",
72
+ model: "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf",
73
+ normalization: "l2",
74
+ });
75
+ expect(formatOhEmbeddingQueryV1("agent research")).toBe("task: search result | query: agent research");
76
+ expect(formatOhEmbeddingDocumentV1("Ada", "research notes")).toBe("title: Ada | text: research notes");
77
+ });
78
+
79
+ test("validates dimensions and cosine normalization", () => {
80
+ const vector = Array.from({ length: 768 }, (_, index) => index === 0 ? 3 : index === 1 ? 4 : 0);
81
+ const normalized = normalizeOhEmbeddingV1(vector);
82
+ expect(normalized[0]).toBeCloseTo(0.6);
83
+ expect(normalized[1]).toBeCloseTo(0.8);
84
+ expect(cosineSimilarityV1(vector, vector)).toBeCloseTo(1);
85
+ expect(() => normalizeOhEmbeddingV1([1, 2])).toThrow("768");
86
+ });
87
+ });
88
+
89
+ describe("QMD derived index confinement", () => {
90
+ test("closes idempotently after optional backend initialization fails", async () => {
91
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-failed-open-test-"));
92
+ roots.push(root);
93
+ const backend = new OhQmdSemanticBackendV1({
94
+ cacheDirectory: root,
95
+ storeFactory: async () => { throw new Error("optional backend unavailable"); },
96
+ });
97
+ await expect(backend.index([])).rejects.toThrow("optional backend unavailable");
98
+ await expect(backend.close()).resolves.toBeUndefined();
99
+ await expect(backend.close()).resolves.toBeUndefined();
100
+ });
101
+
102
+ test("waits for a racing first open and closes a store that resolves after closure", async () => {
103
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-close-race-test-"));
104
+ roots.push(root);
105
+ const entered = deferred<void>();
106
+ const release = deferred<void>();
107
+ let closeCalls = 0;
108
+ const backend = new OhQmdSemanticBackendV1({
109
+ cacheDirectory: root,
110
+ storeFactory: async () => {
111
+ entered.resolve();
112
+ await release.promise;
113
+ return {
114
+ close: async () => { closeCalls += 1; },
115
+ embed: async () => ({}),
116
+ searchVector: async () => [],
117
+ update: async () => ({}),
118
+ };
119
+ },
120
+ });
121
+ const authority = new OhSqliteStore({ path: ":memory:" });
122
+ const search = backend.search("racing open", 1, authority);
123
+ await entered.promise;
124
+ let closeSettled = false;
125
+ const close = backend.close().finally(() => { closeSettled = true; });
126
+ await Promise.resolve();
127
+ expect(closeSettled).toBe(false);
128
+ release.resolve();
129
+ await expect(search).rejects.toThrow("closed");
130
+ await expect(close).resolves.toBeUndefined();
131
+ await expect(backend.close()).resolves.toBeUndefined();
132
+ expect(closeCalls).toBe(1);
133
+ authority.close();
134
+ });
135
+
136
+ test("shares and preserves a store close failure", async () => {
137
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-close-error-test-"));
138
+ roots.push(root);
139
+ let closeCalls = 0;
140
+ const backend = new OhQmdSemanticBackendV1({
141
+ cacheDirectory: root,
142
+ storeFactory: async () => ({
143
+ close: () => {
144
+ closeCalls += 1;
145
+ throw new Error("synthetic close failure");
146
+ },
147
+ embed: async () => ({}),
148
+ searchVector: async () => [],
149
+ update: async () => ({}),
150
+ }),
151
+ });
152
+ await backend.index([]);
153
+ await expect(backend.close()).rejects.toThrow("synthetic close failure");
154
+ await expect(backend.close()).rejects.toThrow("synthetic close failure");
155
+ expect(closeCalls).toBe(1);
156
+ });
157
+
158
+ test("waits for an admitted search before closing its store", async () => {
159
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-active-search-close-test-"));
160
+ roots.push(root);
161
+ const original = record("Ada Lovelace");
162
+ const authority = new OhSqliteStore({ path: ":memory:" });
163
+ commit(authority, original, "op_active_search");
164
+ const entered = deferred<void>();
165
+ const release = deferred<void>();
166
+ let blockSearch = false;
167
+ let closeCalls = 0;
168
+ const backend = new OhQmdSemanticBackendV1({
169
+ cacheDirectory: root,
170
+ storeFactory: async () => ({
171
+ close: async () => { closeCalls += 1; },
172
+ embed: async () => ({}),
173
+ searchVector: async () => {
174
+ if (blockSearch) {
175
+ entered.resolve();
176
+ await release.promise;
177
+ }
178
+ return [qmdVectorResult(original)];
179
+ },
180
+ update: async () => ({}),
181
+ }),
182
+ });
183
+ await backend.index([original]);
184
+ blockSearch = true;
185
+ const search = backend.search("active", 1, authority);
186
+ await entered.promise;
187
+ let closeSettled = false;
188
+ const close = backend.close().finally(() => { closeSettled = true; });
189
+ await expect(backend.search("late", 1, authority)).rejects.toThrow("closed");
190
+ await Promise.resolve();
191
+ expect(closeSettled).toBe(false);
192
+ expect(closeCalls).toBe(0);
193
+ release.resolve();
194
+ expect((await search)[0]?.recordSha256).toBe(original.recordSha256);
195
+ await close;
196
+ expect(closeCalls).toBe(1);
197
+ authority.close();
198
+ });
199
+
200
+ test("shares one complete manifest and store initialization across concurrent first searches", async () => {
201
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-concurrent-load-test-"));
202
+ roots.push(root);
203
+ const authority = new OhSqliteStore({ path: ":memory:" });
204
+ const original = record("Ada Lovelace");
205
+ commit(authority, original, "op_concurrent");
206
+ await writeManifest(root, original);
207
+ const entered = deferred<void>();
208
+ const release = deferred<void>();
209
+ let factoryCalls = 0;
210
+ const backend = new OhQmdSemanticBackendV1({
211
+ cacheDirectory: root,
212
+ storeFactory: async () => {
213
+ factoryCalls += 1;
214
+ entered.resolve();
215
+ await release.promise;
216
+ return {
217
+ close: async () => {},
218
+ embed: async () => ({}),
219
+ searchVector: async () => [qmdVectorResult(original)],
220
+ update: async () => ({}),
221
+ };
222
+ },
223
+ });
224
+ const searches = [
225
+ backend.search("first", 1, authority),
226
+ backend.search("second", 1, authority),
227
+ backend.search("third", 1, authority),
228
+ ];
229
+ await entered.promise;
230
+ expect(factoryCalls).toBe(1);
231
+ release.resolve();
232
+ const results = await Promise.all(searches);
233
+ expect(results.every((items) => items[0]?.recordSha256 === original.recordSha256)).toBe(true);
234
+ await backend.close();
235
+ authority.close();
236
+ });
237
+
238
+ test("memoizes malformed manifest rejection across concurrent and repeated attempts", async () => {
239
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-invalid-manifest-test-"));
240
+ roots.push(root);
241
+ await writeFile(join(root, "manifest.json"), "{}", "utf8");
242
+ let factoryCalls = 0;
243
+ const backend = new OhQmdSemanticBackendV1({
244
+ cacheDirectory: root,
245
+ storeFactory: async () => {
246
+ factoryCalls += 1;
247
+ throw new Error("factory must not run");
248
+ },
249
+ });
250
+ const authority = new OhSqliteStore({ path: ":memory:" });
251
+ const first = await Promise.allSettled([
252
+ backend.search("first", 1, authority),
253
+ backend.search("second", 1, authority),
254
+ ]);
255
+ expect(first.map((result) => result.status)).toEqual(["rejected", "rejected"]);
256
+ expect(first.every((result) => result.status === "rejected"
257
+ && result.reason instanceof Error && result.reason.message.includes("manifest"))).toBe(true);
258
+ await writeManifest(root, record("corrected only on disk"));
259
+ await expect(backend.search("retry", 1, authority)).rejects.toThrow("manifest");
260
+ expect(factoryCalls).toBe(0);
261
+ await backend.close();
262
+ authority.close();
263
+ });
264
+
265
+ test("keeps the prior manifest published when reindex embedding fails", async () => {
266
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-failed-reindex-test-"));
267
+ roots.push(root);
268
+ const original = record("Ada Lovelace");
269
+ const updated = record("Augusta Ada King");
270
+ const authority = new OhSqliteStore({ path: ":memory:" });
271
+ commit(authority, original, "op_failed_reindex");
272
+ let embedCalls = 0;
273
+ let searchResult = qmdVectorResult(original);
274
+ const backend = new OhQmdSemanticBackendV1({
275
+ cacheDirectory: root,
276
+ storeFactory: async () => ({
277
+ close: async () => {},
278
+ embed: async () => {
279
+ embedCalls += 1;
280
+ if (embedCalls === 2) throw new Error("synthetic embed failure");
281
+ },
282
+ searchVector: async () => [searchResult],
283
+ update: async () => ({}),
284
+ }),
285
+ });
286
+ await backend.index([original]);
287
+ const published = await readFile(join(root, "manifest.json"), "utf8");
288
+ searchResult = qmdVectorResult(updated);
289
+ await expect(backend.index([updated])).rejects.toThrow("synthetic embed failure");
290
+ expect(await readFile(join(root, "manifest.json"), "utf8")).toBe(published);
291
+ expect(await backend.search("stale score", 1, authority)).toEqual([]);
292
+ searchResult = qmdVectorResult(original);
293
+ expect((await backend.search("prior snapshot", 1, authority))[0]?.recordSha256)
294
+ .toBe(original.recordSha256);
295
+ await backend.close();
296
+ authority.close();
297
+ });
298
+
299
+ test("uses one manifest snapshot across a search and concurrent successful reindex", async () => {
300
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-search-reindex-race-test-"));
301
+ roots.push(root);
302
+ const original = record("Ada Lovelace");
303
+ const updated = record("Augusta Ada King");
304
+ const authority = new OhSqliteStore({ path: ":memory:" });
305
+ commit(authority, original, "op_race_original");
306
+ const entered = deferred<void>();
307
+ const release = deferred<void>();
308
+ let blockSearch = false;
309
+ let searchResult = qmdVectorResult(original);
310
+ const backend = new OhQmdSemanticBackendV1({
311
+ cacheDirectory: root,
312
+ storeFactory: async () => ({
313
+ close: async () => {},
314
+ embed: async () => ({}),
315
+ searchVector: async () => {
316
+ const captured = searchResult;
317
+ if (blockSearch) {
318
+ entered.resolve();
319
+ await release.promise;
320
+ blockSearch = false;
321
+ }
322
+ return [captured];
323
+ },
324
+ update: async () => ({}),
325
+ }),
326
+ });
327
+ await backend.index([original]);
328
+ blockSearch = true;
329
+ const oldSearch = backend.search("old snapshot", 1, authority);
330
+ await entered.promise;
331
+ await backend.index([updated]);
332
+ release.resolve();
333
+ expect((await oldSearch)[0]?.recordSha256).toBe(original.recordSha256);
334
+
335
+ commit(authority, updated, "op_race_updated");
336
+ searchResult = qmdVectorResult(original);
337
+ expect(await backend.search("stale body", 1, authority)).toEqual([]);
338
+ searchResult = qmdVectorResult(updated);
339
+ expect((await backend.search("current body", 1, authority))[0]?.recordSha256)
340
+ .toBe(updated.recordSha256);
341
+ await backend.close();
342
+ authority.close();
343
+ });
344
+
345
+ test("serializes concurrent index publications", async () => {
346
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-serialized-index-test-"));
347
+ roots.push(root);
348
+ const first = record("First edition");
349
+ const second = record("Second edition");
350
+ const firstUpdate = deferred<void>();
351
+ const releaseFirst = deferred<void>();
352
+ let updateCalls = 0;
353
+ const backend = new OhQmdSemanticBackendV1({
354
+ cacheDirectory: root,
355
+ storeFactory: async () => ({
356
+ close: async () => {},
357
+ embed: async () => ({}),
358
+ searchVector: async () => [],
359
+ update: async () => {
360
+ updateCalls += 1;
361
+ if (updateCalls === 1) {
362
+ firstUpdate.resolve();
363
+ await releaseFirst.promise;
364
+ }
365
+ },
366
+ }),
367
+ });
368
+ const firstIndex = backend.index([first]);
369
+ await firstUpdate.promise;
370
+ const secondIndex = backend.index([second]);
371
+ await Promise.resolve();
372
+ expect(updateCalls).toBe(1);
373
+ releaseFirst.resolve();
374
+ await Promise.all([firstIndex, secondIndex]);
375
+ expect(updateCalls).toBe(2);
376
+ const manifest = JSON.parse(await readFile(join(root, "manifest.json"), "utf8")) as {
377
+ entries: Record<string, { recordSha256: string }>;
378
+ };
379
+ expect(manifest.entries[`${sha256Hex(second.key)}.md`]?.recordSha256).toBe(second.recordSha256);
380
+ await backend.close();
381
+ });
382
+
383
+ test("rejoins hits to the exact authoritative record digest", async () => {
384
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-test-"));
385
+ roots.push(root);
386
+ const key = "entity:ada";
387
+ const original = record("Ada Lovelace");
388
+ let closed = false;
389
+ const openings: Parameters<QmdStoreFactoryV1>[0][] = [];
390
+ const factory: QmdStoreFactoryV1 = async (options) => {
391
+ openings.push(options);
392
+ return {
393
+ close: async () => { closed = true; },
394
+ embed: async () => ({}),
395
+ searchVector: async () => [qmdVectorResult(original)],
396
+ update: async () => ({}),
397
+ };
398
+ };
399
+ const authority = new OhSqliteStore({ path: ":memory:" });
400
+ commit(authority, original, "op_first");
401
+ const backend = new OhQmdSemanticBackendV1({ cacheDirectory: root, storeFactory: factory });
402
+ expect(await backend.index(authority.snapshotRecords())).toEqual({ indexed: 1, v: 1 });
403
+ expect(openings).toHaveLength(1);
404
+ const config = openings[0]?.config as { collections?: { oh?: { path?: unknown } } };
405
+ const collectionPath = config.collections?.oh?.path;
406
+ expect(typeof collectionPath).toBe("string");
407
+ expect(isAbsolute(collectionPath as string)).toBe(true);
408
+ expect(collectionPath).toBe(join(root, "documents"));
409
+ expect(await backend.search("mathematician", 10, authority)).toEqual([
410
+ { key, recordSha256: original.recordSha256, score: 0.9, v: 1 },
411
+ ]);
412
+ await backend.close();
413
+ expect(closed).toBe(true);
414
+ closed = false;
415
+ const reopened = new OhQmdSemanticBackendV1({ cacheDirectory: root, storeFactory: factory });
416
+ expect(await reopened.search("mathematician", 10, authority)).toEqual([
417
+ { key, recordSha256: original.recordSha256, score: 0.9, v: 1 },
418
+ ]);
419
+ commit(authority, record("Augusta Ada King"), "op_second");
420
+ expect(await reopened.search("mathematician", 10, authority)).toEqual([]);
421
+ await reopened.close();
422
+ expect(closed).toBe(true);
423
+ authority.close();
424
+ });
425
+
426
+ test("resolves relative QMD paths once and rejects results outside its collection URI", async () => {
427
+ const root = await mkdtemp(join(tmpdir(), "oh-semantic-relative-test-"));
428
+ roots.push(root);
429
+ const relativeCache = relative(process.cwd(), join(root, "cache"));
430
+ const original = record("Ada Lovelace");
431
+ const openings: Parameters<QmdStoreFactoryV1>[0][] = [];
432
+ const factory: QmdStoreFactoryV1 = async (options) => {
433
+ openings.push(options);
434
+ return {
435
+ close: async () => {},
436
+ embed: async () => ({}),
437
+ searchVector: async () => [
438
+ qmdVectorResult(original, { filepath: `qmd://other/${sha256Hex("entity:ada")}.md`, score: 1 }),
439
+ qmdVectorResult(original, { score: -0.1 }),
440
+ qmdVectorResult(original, { score: 1.1 }),
441
+ { file: `qmd://oh/${sha256Hex("entity:ada")}.md`, score: 0.7 },
442
+ qmdVectorResult(original, { extension: "rejected", score: 0.75 }),
443
+ qmdVectorResult(original, { displayPath: "oh/other.md", score: 0.76 }),
444
+ qmdVectorResult(original, { score: 0.8 }),
445
+ ],
446
+ update: async () => ({}),
447
+ };
448
+ };
449
+ const authority = new OhSqliteStore({ path: ":memory:" });
450
+ commit(authority, original, "op_first");
451
+ const backend = new OhQmdSemanticBackendV1({ cacheDirectory: relativeCache,
452
+ databasePath: join(relativeCache, "custom.sqlite"), storeFactory: factory });
453
+ await backend.index(authority.snapshotRecords());
454
+ expect(openings).toHaveLength(1);
455
+ expect(openings[0]?.dbPath).toBe(resolve(relativeCache, "custom.sqlite"));
456
+ const config = openings[0]?.config as { collections?: { oh?: { path?: unknown } } };
457
+ expect(config.collections?.oh?.path).toBe(resolve(relativeCache, "documents"));
458
+ expect(await backend.search("mathematician", 10, authority)).toEqual([
459
+ { key: "entity:ada", recordSha256: original.recordSha256, score: 0.8, v: 1 },
460
+ ]);
461
+ await backend.close();
462
+ authority.close();
463
+ });
464
+
465
+ test("degrades hybrid search to model-free keyword evidence", async () => {
466
+ const authority = new OhSqliteStore({ path: ":memory:" });
467
+ commit(authority, record("Ada Lovelace analytical engine"), "op_first");
468
+ const response = await searchOhV1({ store: authority, query: "analytical engine", mode: "hybrid",
469
+ backend: {
470
+ profile: OH_EMBEDDING_PROFILE_V1,
471
+ close: async () => {},
472
+ index: async () => ({ indexed: 0, v: 1 }),
473
+ search: async () => { throw new Error("model unavailable"); },
474
+ } });
475
+ expect(response.results[0]?.record.key).toBe("entity:ada");
476
+ expect(response.results[0]?.evidence.map((item) => item.lane)).toEqual(["keyword"]);
477
+ expect(response.diagnostics).toEqual([{ code: "semantic-unavailable", message: "model unavailable", v: 1 }]);
478
+ authority.close();
479
+ });
480
+ });