diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e0066f5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "vendor/footnote"] + path = vendor/footnote + url = https://github.com/mieweb/melvil-artipod-footnote + branch = main diff --git a/packages/cloud-local/package.json b/packages/cloud-local/package.json index f529b36..6de6dd4 100644 --- a/packages/cloud-local/package.json +++ b/packages/cloud-local/package.json @@ -24,7 +24,8 @@ "test": "node --test" }, "dependencies": { - "@hono/node-server": "^1.13.0" + "@hono/node-server": "^1.13.0", + "@mieweb/footnote": "link:../../vendor/footnote" }, "optionalDependencies": { "better-sqlite3": "^11.0.0", diff --git a/packages/cloud-local/src/registry.mjs b/packages/cloud-local/src/registry.mjs index 6cb5819..b34bfa0 100644 --- a/packages/cloud-local/src/registry.mjs +++ b/packages/cloud-local/src/registry.mjs @@ -7,6 +7,7 @@ import { createInprocQueue } from './adapters/queue-inproc.mjs'; import { createInprocNamespace } from './adapters/durable-inproc.mjs'; import { createAiBackend } from './adapters/ai-multi.mjs'; import { createUnsupportedBinding } from './adapters/unsupported.mjs'; +import { createLocalIndex } from '@mieweb/footnote/vectorize'; /** * Driver registry — the single extension point for the portability layer. @@ -70,6 +71,17 @@ registerDriver('sqlite-vec', ({ cfg, resolvePath }) => createSqliteVecIndex(resolvePath(cfg.path), { dim: cfg.dim ?? 768 }), ); +// FOOTNOTE (artipod) — the richer Vectorize store: hybrid + FTS + assertion-aware +// retrieval, same CloudVectorIndex contract. See mieweb/melvil-artipod-footnote. +registerDriver('footnote', ({ cfg, resolvePath }) => + createLocalIndex({ + name: cfg.name ?? 'index', + dimensions: cfg.dim ?? 768, + metric: cfg.metric ?? 'cosine', + dbPath: cfg.path ? resolvePath(cfg.path) : undefined, + }), +); + registerDriver('fs', ({ cfg, resolvePath }) => createFsBucket(resolvePath(cfg.path))); registerDriver('memory', () => createMemoryKV()); diff --git a/packages/cloud-local/test/conformance.test.mjs b/packages/cloud-local/test/conformance.test.mjs index 1e8d152..b4dda96 100644 --- a/packages/cloud-local/test/conformance.test.mjs +++ b/packages/cloud-local/test/conformance.test.mjs @@ -21,6 +21,8 @@ import { createSqliteD1, createSqliteVecIndex, createAiBackend, + createCloudEnv, + settleEnv, } from '../src/index.mjs'; function tmp(prefix) { @@ -46,6 +48,15 @@ async function hasSqliteVec() { } } +async function hasFootnote() { + try { + await import('@mieweb/footnote/vectorize'); + return true; + } catch { + return false; + } +} + // --- KV contract --------------------------------------------------------- test('KV (memory): get/put/delete + TTL + list prefix', async () => { const kv = createMemoryKV(); @@ -154,6 +165,48 @@ test('Vectorize (sqlite-vec): upsert/query/filter/getByIds/deleteByIds', async ( } }); +// Same Vectorize contract, but driven through the FOOTNOTE driver exactly as a +// `mieweb.jsonc` binding of `{ driver: 'footnote' }` would be — this covers the +// registerDriver('footnote', …) resolution path in registry.mjs, not just the +// footnote package in isolation. +test('Vectorize (footnote driver): resolves via createCloudEnv + honors contract', async (t) => { + if (!(await hasFootnote())) return t.skip('@mieweb/footnote not linked'); + const dir = tmp('mwc-footnote-'); + try { + const targetConfig = { + bindings: { + VECTOR: { driver: 'footnote', name: 'idx', dim: 4, metric: 'cosine', path: 'idx.sqlite' }, + }, + }; + const { env } = createCloudEnv({ targetConfig, root: dir, target: 'local' }); + await settleEnv(env); // createLocalIndex constructs asynchronously + const idx = env.VECTOR; + assert.ok(idx, 'VECTOR binding resolved to a footnote index'); + + await idx.upsert([ + { id: 'a', values: [1, 0, 0, 0], metadata: { org: 'o1', kind: 'doc' } }, + { id: 'b', values: [0, 1, 0, 0], metadata: { org: 'o1', kind: 'call' } }, + { id: 'c', values: [0.9, 0.1, 0, 0], metadata: { org: 'o2', kind: 'doc' } }, + ]); + + const top = await idx.query([1, 0, 0, 0], { topK: 2, returnMetadata: true }); + assert.equal(top.matches[0].id, 'a'); + assert.ok(top.matches[0].score > top.matches[1].score); + + const filtered = await idx.query([1, 0, 0, 0], { topK: 5, filter: { org: { $eq: 'o1' } } }); + assert.deepEqual(filtered.matches.map((m) => m.id).sort(), ['a', 'b']); + + const inFilter = await idx.query([1, 0, 0, 0], { topK: 5, filter: { kind: { $in: ['call'] } } }); + assert.deepEqual(inFilter.matches.map((m) => m.id), ['b']); + + await idx.deleteByIds(['b']); + const afterDelete = await idx.query([0, 1, 0, 0], { topK: 5 }); + assert.ok(!afterDelete.matches.some((m) => m.id === 'b')); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + // --- AI contract --------------------------------------------------------- test('AI (ollama): run({text}) embeddings + toMarkdown passthrough', async () => { const ai = createAiBackend({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cacc103..80f3504 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@hono/node-server': specifier: ^1.13.0 version: 1.19.14(hono@4.12.23) + '@mieweb/footnote': + specifier: link:../../vendor/footnote + version: link:../../vendor/footnote optionalDependencies: better-sqlite3: specifier: ^11.0.0 @@ -731,6 +734,10 @@ packages: resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1661,7 +1668,7 @@ snapshots: fast-xml-builder@1.2.0: dependencies: - path-expression-matcher: 1.5.0 + path-expression-matcher: 1.6.2 xml-naming: 0.1.0 optional: true @@ -1888,6 +1895,9 @@ snapshots: path-expression-matcher@1.5.0: optional: true + path-expression-matcher@1.6.2: + optional: true + path-key@3.1.1: {} path-type@4.0.0: {} diff --git a/vendor/footnote b/vendor/footnote new file mode 160000 index 0000000..6ab7867 --- /dev/null +++ b/vendor/footnote @@ -0,0 +1 @@ +Subproject commit 6ab78676680d0eba63846c883b14c4ba9f3146e5