@aigne/afs-index 1.12.0-beta.5
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.md +26 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/backend/d1-index-backend.cjs +656 -0
- package/dist/backend/d1-index-backend.d.cts +149 -0
- package/dist/backend/d1-index-backend.d.cts.map +1 -0
- package/dist/backend/d1-index-backend.d.mts +149 -0
- package/dist/backend/d1-index-backend.d.mts.map +1 -0
- package/dist/backend/d1-index-backend.mjs +657 -0
- package/dist/backend/d1-index-backend.mjs.map +1 -0
- package/dist/backend/d1-vector-backend.cjs +83 -0
- package/dist/backend/d1-vector-backend.d.cts +21 -0
- package/dist/backend/d1-vector-backend.d.cts.map +1 -0
- package/dist/backend/d1-vector-backend.d.mts +21 -0
- package/dist/backend/d1-vector-backend.d.mts.map +1 -0
- package/dist/backend/d1-vector-backend.mjs +84 -0
- package/dist/backend/d1-vector-backend.mjs.map +1 -0
- package/dist/backend/memory-backend.cjs +304 -0
- package/dist/backend/memory-backend.d.cts +66 -0
- package/dist/backend/memory-backend.d.cts.map +1 -0
- package/dist/backend/memory-backend.d.mts +66 -0
- package/dist/backend/memory-backend.d.mts.map +1 -0
- package/dist/backend/memory-backend.mjs +305 -0
- package/dist/backend/memory-backend.mjs.map +1 -0
- package/dist/backend/prefix-range.cjs +25 -0
- package/dist/backend/prefix-range.mjs +25 -0
- package/dist/backend/prefix-range.mjs.map +1 -0
- package/dist/backend/schema.cjs +187 -0
- package/dist/backend/schema.mjs +188 -0
- package/dist/backend/schema.mjs.map +1 -0
- package/dist/backend/sqlite-backend.cjs +889 -0
- package/dist/backend/sqlite-backend.d.cts +96 -0
- package/dist/backend/sqlite-backend.d.cts.map +1 -0
- package/dist/backend/sqlite-backend.d.mts +96 -0
- package/dist/backend/sqlite-backend.d.mts.map +1 -0
- package/dist/backend/sqlite-backend.mjs +890 -0
- package/dist/backend/sqlite-backend.mjs.map +1 -0
- package/dist/backend/types.d.cts +209 -0
- package/dist/backend/types.d.cts.map +1 -0
- package/dist/backend/types.d.mts +209 -0
- package/dist/backend/types.d.mts.map +1 -0
- package/dist/backend/upstash-vector-backend.cjs +98 -0
- package/dist/backend/upstash-vector-backend.d.cts +21 -0
- package/dist/backend/upstash-vector-backend.d.cts.map +1 -0
- package/dist/backend/upstash-vector-backend.d.mts +21 -0
- package/dist/backend/upstash-vector-backend.d.mts.map +1 -0
- package/dist/backend/upstash-vector-backend.mjs +98 -0
- package/dist/backend/upstash-vector-backend.mjs.map +1 -0
- package/dist/backend/vector-backend.d.cts +29 -0
- package/dist/backend/vector-backend.d.cts.map +1 -0
- package/dist/backend/vector-backend.d.mts +29 -0
- package/dist/backend/vector-backend.d.mts.map +1 -0
- package/dist/dirty-queue.d.cts +41 -0
- package/dist/dirty-queue.d.cts.map +1 -0
- package/dist/dirty-queue.d.mts +41 -0
- package/dist/dirty-queue.d.mts.map +1 -0
- package/dist/embed.cjs +45 -0
- package/dist/embed.d.cts +20 -0
- package/dist/embed.d.cts.map +1 -0
- package/dist/embed.d.mts +20 -0
- package/dist/embed.d.mts.map +1 -0
- package/dist/embed.mjs +46 -0
- package/dist/embed.mjs.map +1 -0
- package/dist/extraction/defaults.cjs +242 -0
- package/dist/extraction/defaults.d.cts +14 -0
- package/dist/extraction/defaults.d.cts.map +1 -0
- package/dist/extraction/defaults.d.mts +14 -0
- package/dist/extraction/defaults.d.mts.map +1 -0
- package/dist/extraction/defaults.mjs +242 -0
- package/dist/extraction/defaults.mjs.map +1 -0
- package/dist/extraction/field-map.cjs +162 -0
- package/dist/extraction/field-map.d.cts +70 -0
- package/dist/extraction/field-map.d.cts.map +1 -0
- package/dist/extraction/field-map.d.mts +70 -0
- package/dist/extraction/field-map.d.mts.map +1 -0
- package/dist/extraction/field-map.mjs +157 -0
- package/dist/extraction/field-map.mjs.map +1 -0
- package/dist/extraction/manager.cjs +74 -0
- package/dist/extraction/manager.d.cts +47 -0
- package/dist/extraction/manager.d.cts.map +1 -0
- package/dist/extraction/manager.d.mts +47 -0
- package/dist/extraction/manager.d.mts.map +1 -0
- package/dist/extraction/manager.mjs +74 -0
- package/dist/extraction/manager.mjs.map +1 -0
- package/dist/extraction/path-anchors.cjs +86 -0
- package/dist/extraction/path-anchors.d.cts +72 -0
- package/dist/extraction/path-anchors.d.cts.map +1 -0
- package/dist/extraction/path-anchors.d.mts +72 -0
- package/dist/extraction/path-anchors.d.mts.map +1 -0
- package/dist/extraction/path-anchors.mjs +86 -0
- package/dist/extraction/path-anchors.mjs.map +1 -0
- package/dist/follower-scheduler.cjs +92 -0
- package/dist/follower-scheduler.d.cts +58 -0
- package/dist/follower-scheduler.d.cts.map +1 -0
- package/dist/follower-scheduler.d.mts +58 -0
- package/dist/follower-scheduler.d.mts.map +1 -0
- package/dist/follower-scheduler.mjs +92 -0
- package/dist/follower-scheduler.mjs.map +1 -0
- package/dist/follower.cjs +577 -0
- package/dist/follower.d.cts +241 -0
- package/dist/follower.d.cts.map +1 -0
- package/dist/follower.d.mts +241 -0
- package/dist/follower.d.mts.map +1 -0
- package/dist/follower.mjs +571 -0
- package/dist/follower.mjs.map +1 -0
- package/dist/index-provider.cjs +1166 -0
- package/dist/index-provider.d.cts +175 -0
- package/dist/index-provider.d.cts.map +1 -0
- package/dist/index-provider.d.mts +175 -0
- package/dist/index-provider.d.mts.map +1 -0
- package/dist/index-provider.mjs +1167 -0
- package/dist/index-provider.mjs.map +1 -0
- package/dist/index.cjs +53 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.mts +22 -0
- package/dist/index.mjs +20 -0
- package/dist/keys/exact.cjs +20 -0
- package/dist/keys/exact.mjs +20 -0
- package/dist/keys/exact.mjs.map +1 -0
- package/dist/keys/location.cjs +13 -0
- package/dist/keys/location.mjs +13 -0
- package/dist/keys/location.mjs.map +1 -0
- package/dist/keys/normalized.cjs +62 -0
- package/dist/keys/normalized.mjs +63 -0
- package/dist/keys/normalized.mjs.map +1 -0
- package/dist/keys/person.cjs +15 -0
- package/dist/keys/person.mjs +15 -0
- package/dist/keys/person.mjs.map +1 -0
- package/dist/keys/registry.cjs +45 -0
- package/dist/keys/registry.d.cts +15 -0
- package/dist/keys/registry.d.cts.map +1 -0
- package/dist/keys/registry.d.mts +15 -0
- package/dist/keys/registry.d.mts.map +1 -0
- package/dist/keys/registry.mjs +46 -0
- package/dist/keys/registry.mjs.map +1 -0
- package/dist/keys/time.cjs +128 -0
- package/dist/keys/time.mjs +128 -0
- package/dist/keys/time.mjs.map +1 -0
- package/dist/keys/topic.cjs +23 -0
- package/dist/keys/topic.mjs +23 -0
- package/dist/keys/topic.mjs.map +1 -0
- package/dist/manifest-index.cjs +29 -0
- package/dist/manifest-index.d.cts +24 -0
- package/dist/manifest-index.d.cts.map +1 -0
- package/dist/manifest-index.d.mts +24 -0
- package/dist/manifest-index.d.mts.map +1 -0
- package/dist/manifest-index.mjs +28 -0
- package/dist/manifest-index.mjs.map +1 -0
- package/dist/offline-rebuild.cjs +48 -0
- package/dist/offline-rebuild.d.cts +57 -0
- package/dist/offline-rebuild.d.cts.map +1 -0
- package/dist/offline-rebuild.d.mts +57 -0
- package/dist/offline-rebuild.d.mts.map +1 -0
- package/dist/offline-rebuild.mjs +49 -0
- package/dist/offline-rebuild.mjs.map +1 -0
- package/dist/query/engine.cjs +433 -0
- package/dist/query/engine.d.cts +34 -0
- package/dist/query/engine.d.cts.map +1 -0
- package/dist/query/engine.d.mts +34 -0
- package/dist/query/engine.d.mts.map +1 -0
- package/dist/query/engine.mjs +433 -0
- package/dist/query/engine.mjs.map +1 -0
- package/dist/query/limits.cjs +12 -0
- package/dist/query/limits.mjs +12 -0
- package/dist/query/limits.mjs.map +1 -0
- package/dist/text/cjk.cjs +30 -0
- package/dist/text/cjk.d.cts +24 -0
- package/dist/text/cjk.d.cts.map +1 -0
- package/dist/text/cjk.d.mts +24 -0
- package/dist/text/cjk.d.mts.map +1 -0
- package/dist/text/cjk.mjs +30 -0
- package/dist/text/cjk.mjs.map +1 -0
- package/dist/text/fts.cjs +244 -0
- package/dist/text/fts.mjs +245 -0
- package/dist/text/fts.mjs.map +1 -0
- package/dist/text/host.cjs +39 -0
- package/dist/text/host.mjs +38 -0
- package/dist/text/host.mjs.map +1 -0
- package/dist/types.cjs +44 -0
- package/dist/types.d.cts +393 -0
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.mts +393 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +42 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-backend.mjs","names":[],"sources":["../../src/backend/sqlite-backend.ts"],"sourcesContent":["/**\n * SQLiteIndexBackend — pluggable SQLite storage backend for the Index Provider.\n *\n * Implements the IndexBackend interface using AFSSQLite through AFS.\n * - CRUD operations use built-in AFS actions (insert, query, update_where, delete_where)\n * - Schema creation uses getDatabase() as a local variable (one-time DDL bootstrap)\n * - FTS / count / anchor-intersection use registered custom actions via afs.exec()\n * - Zero stored db handle — all runtime operations go through AFS\n */\nimport { AFS } from \"@aigne/afs\";\nimport type { SqliteDatabase } from \"@aigne/afs-sqlite\";\nimport { SQLiteAFS } from \"@aigne/afs-sqlite\";\nimport { sql } from \"drizzle-orm\";\nimport { joinURL } from \"ufo\";\nimport { KeyGeneratorRegistry } from \"../keys/registry.js\";\nimport { segmentCJK } from \"../text/cjk.js\";\nimport type {\n Anchor,\n AnchorData,\n AnchorQuery,\n AnchorResult,\n CombinedResult,\n EntryFilter,\n FTSResult,\n IndexedEntry,\n QueryOpts,\n StaleFilter,\n VectorResult,\n} from \"../types.js\";\nimport { entryPathPrefixRange } from \"./prefix-range.js\";\nimport { createSchema } from \"./schema.js\";\nimport type {\n CursorRow,\n CursorScopeRegistration,\n IndexBackend,\n IndexedDocumentWrite,\n RemoveIndexOptions,\n} from \"./types.js\";\n\n// Internal mount path for the SQLite provider\nconst MOUNT_NAME = \"db\";\n\nexport class SQLiteIndexBackend implements IndexBackend {\n private afs!: AFS;\n private sqliteProvider!: SQLiteAFS;\n private readonly dbUrl: string;\n private keyRegistry: KeyGeneratorRegistry;\n private readonly ftsTokenizer: string | undefined;\n // Per-caller tenant scope, stamped on every write and enforced on every\n // read/delete/query — the ONLY thing isolating one caller's rows from\n // another's in the shared `/user/index` file (mirrors D1's `this.scope`).\n // Empty string = single logical tenant (standalone use / tests), which is\n // still a valid, self-consistent scope value.\n private readonly scope: string;\n private embeddingDimension = 0;\n private initialized = false;\n\n constructor(\n dbUrl = \":memory:\",\n keyRegistry?: KeyGeneratorRegistry,\n opts: { ftsTokenizer?: string; scope?: string } = {},\n ) {\n this.dbUrl = dbUrl;\n this.keyRegistry = keyRegistry ?? new KeyGeneratorRegistry();\n this.ftsTokenizer = opts.ftsTokenizer;\n this.scope = opts.scope ?? \"\";\n }\n\n /** See `IndexBackend.setKeyRegistry` — wires an injected backend to the provider's shared registry. */\n setKeyRegistry(registry: KeyGeneratorRegistry): void {\n this.keyRegistry = registry;\n }\n\n private get mountPath(): string {\n return joinURL(\"/modules\", MOUNT_NAME);\n }\n\n private tableAction(table: string, action: string): string {\n return joinURL(this.mountPath, table, \".actions\", action);\n }\n\n /**\n * Idempotent (design.md §4.4 addendum, owner decision 2026-07-05): a\n * caller that needs the backend usable BEFORE any `AFSIndex` action runs\n * (e.g. `initCursor` at follower-registration time — see daemon.ts /\n * cf-session-view.ts) must call this explicitly; `AFSIndex.ensureInitialized()`\n * ALSO calls it unconditionally on the first real action. Without the\n * guard, that second call re-mounted a BRAND NEW `SQLiteAFS`/connection —\n * for a `:memory:` db this silently discarded everything the first\n * connection had written (caught by the Node/CF parity test's in-memory\n * backend); for a file-backed db it \"only\" leaked an extra connection.\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n this.sqliteProvider = new SQLiteAFS({\n url: this.dbUrl,\n name: MOUNT_NAME,\n accessMode: \"readwrite\",\n });\n this.afs = new AFS();\n await this.afs.mount(this.sqliteProvider);\n\n // One-time schema creation — local db handle, not stored\n const db = await this.sqliteProvider.getDatabase();\n await createSchema(db, { ftsTokenizer: this.ftsTokenizer });\n\n // Register all custom actions for runtime operations\n this.registerCustomActions();\n this.initialized = true;\n }\n\n private registerCustomActions(): void {\n // --- Anchor intersection (CTE-based single-query AND logic, optional metaFilter) ---\n this.sqliteProvider.registerAction(\n \"anchor_intersection\",\n async (ctx, params) => {\n const keyGroups = params.key_groups as { values: string[] }[];\n const tenantScope = (params.tenant_scope as string | undefined) ?? \"\";\n const scope = params.scope as string | undefined;\n const pathPrefix = params.path_prefix as string | undefined;\n const minConfidence = params.min_confidence as number | undefined;\n const limit = params.limit as number | undefined;\n const metaFilter = params.meta_filter as Record<string, unknown> | undefined;\n const domains = params.domains as string[] | undefined;\n\n if (!keyGroups || keyGroups.length === 0) return { rows: [] };\n\n const cteNames: string[] = [];\n const cteParts: ReturnType<typeof sql>[] = [];\n\n for (let i = 0; i < keyGroups.length; i++) {\n const cteName = `g${i}`;\n cteNames.push(cteName);\n // Non-index_key predicates, shared by every UNION arm below.\n // Tenant isolation: every anchor-key row is scoped, so each arm\n // filters the caller's own `tenantScope` first (the shared-file\n // boundary). The legacy `scope` param stays a path-prefix shorthand —\n // a DIFFERENT, orthogonal filter.\n const extraConds: ReturnType<typeof sql>[] = [];\n // entry_path prefixes as half-open ranges so the (scope, index_key,\n // entry_path) PK bounds the seek instead of a LIKE residual that reads\n // every row for the index_key (issue #1120; entryPathPrefixRange doc).\n // Kept in step with d1-index-backend.ts.\n for (const prefix of [scope, pathPrefix]) {\n if (prefix) {\n const [lo, hi] = entryPathPrefixRange(prefix);\n extraConds.push(sql`entry_path >= ${lo} AND entry_path < ${hi}`);\n }\n }\n if (minConfidence != null) extraConds.push(sql`confidence >= ${minConfidence}`);\n\n // One equality seek per key value instead of `index_key IN (...)`.\n // idx_anchor_key is WITHOUT ROWID with PK (scope, index_key, ...), so\n // `index_key = ?` seeks the PK; `index_key IN (...)` makes the planner\n // (no ANALYZE stats) fall back to a full-scope scan (issue #1120).\n // Kept byte-for-byte in step with d1-index-backend.ts. UNION ALL is\n // semantics-preserving: a row matches exactly one index_key (no\n // duplicates) and MIN(confidence) folds per entry_path.\n const arms = keyGroups[i]!.values.map(\n (v) =>\n sql`SELECT entry_path, confidence FROM idx_anchor_key WHERE ${sql.join(\n [sql`scope = ${tenantScope}`, sql`index_key = ${v}`, ...extraConds],\n sql` AND `,\n )}`,\n );\n cteParts.push(\n sql`${sql.raw(cteName)} AS (SELECT entry_path, MIN(confidence) as min_conf FROM (${sql.join(\n arms,\n sql` UNION ALL `,\n )}) GROUP BY entry_path)`,\n );\n }\n\n const cteSql = sql`WITH ${sql.join(cteParts, sql`, `)}`;\n\n // Build SELECT with CTE joins. GROUP BY must come after all JOINs (including metaFilter).\n let selectSql: ReturnType<typeof sql>;\n const cteJoinParts: string[] = [];\n let scoreExpr: string;\n\n if (cteNames.length === 1) {\n scoreExpr = `${cteNames[0]!}.min_conf`;\n } else {\n for (let i = 1; i < cteNames.length; i++) {\n cteJoinParts.push(\n `INNER JOIN ${cteNames[i]} ON ${cteNames[0]}.entry_path = ${cteNames[i]}.entry_path`,\n );\n }\n scoreExpr = `MIN(${cteNames.map((n) => `${n}.min_conf`).join(\", \")})`;\n }\n\n // Apply domain isolation (`boundDomains` fallback, issue #936 P4 leak\n // fix) and/or metaFilter via a single shared JOIN with\n // ds_indexed_entry + json_extract conditions. Previously this JOIN\n // only existed for metaFilter — anchor queries had NO domain\n // isolation at all, even when the caller's scope implied one; a\n // multi-tenant daemon sharing one SQLite file (Node's `/user/index`)\n // could return another caller's anchor-matched entries.\n const entryJoinPart: string[] = [];\n let entryWhereSql: ReturnType<typeof sql> | null = null;\n if ((domains && domains.length > 0) || (metaFilter && Object.keys(metaFilter).length > 0)) {\n // Join must be scoped: ds_indexed_entry's PK is (scope, entry_path),\n // not entry_path alone, so on the shared-file multi-tenant daemon\n // (Node's `/user/index`) two different callers can have rows with\n // the same entry_path. An unscoped join here would match BOTH\n // rows and (since the outer GROUP BY entry_path was removed as\n // redundant, #1368) surface a duplicate row per collision instead\n // of silently deduping it — this is the same isolation boundary\n // the domains/metaFilter join was added for (issue #936 P4).\n // Mirrors d1-index-backend.ts's `AND ie.scope = '...'` join guard.\n entryJoinPart.push(\n `INNER JOIN ds_indexed_entry ie ON ${cteNames[0]!}.entry_path = ie.entry_path AND ie.scope = '${sqlEscape(tenantScope)}'`,\n );\n const entryConditions: ReturnType<typeof sql>[] = [];\n if (domains && domains.length > 0) {\n const domainPlaceholders = domains.map((d) => sql`${d}`);\n entryConditions.push(sql`ie.domain IN (${sql.join(domainPlaceholders, sql`, `)})`);\n }\n for (const [key, expected] of Object.entries(metaFilter ?? {})) {\n if (Array.isArray(expected)) {\n if (expected.length === 0) return { rows: [] };\n const placeholders = expected.map((v) => sql`${v}`);\n entryConditions.push(\n sql`json_extract(ie.meta, ${`$.${key}`}) IN (${sql.join(placeholders, sql`, `)})`,\n );\n } else {\n entryConditions.push(sql`json_extract(ie.meta, ${`$.${key}`}) = ${expected}`);\n }\n }\n entryWhereSql = sql.join(entryConditions, sql` AND `);\n }\n\n // Assemble: SELECT ... FROM g0 [JOIN g1 ...] [JOIN ie ...] [WHERE ...]\n // No outer GROUP BY: g0 (and each gN) is already grouped by\n // entry_path, and the INNER JOINs below are 1:1 on entry_path, so the\n // result set is already unique per entry_path (#1368).\n const allJoins = [...cteJoinParts, ...entryJoinPart].join(\" \");\n if (entryWhereSql) {\n selectSql = sql`${cteSql} SELECT ${sql.raw(cteNames[0]!)}.entry_path, ${sql.raw(scoreExpr)} as score FROM ${sql.raw(cteNames[0]!)} ${sql.raw(allJoins)} WHERE ${entryWhereSql}`;\n } else {\n selectSql = sql`${cteSql} SELECT ${sql.raw(cteNames[0]!)}.entry_path, ${sql.raw(scoreExpr)} as score FROM ${sql.raw(cteNames[0]!)} ${sql.raw(allJoins)}`;\n }\n\n // Secondary sort key stabilizes top-k ordering: ties in `score` would\n // otherwise leave row order undefined across runs (#1368).\n const orderSql =\n limit != null\n ? sql`${selectSql} ORDER BY score DESC, ${sql.raw(cteNames[0]!)}.entry_path LIMIT ${limit}`\n : sql`${selectSql} ORDER BY score DESC, ${sql.raw(cteNames[0]!)}.entry_path`;\n\n const rows = await ctx.db.all<{ entry_path: string; score: number }>(orderSql);\n return { rows };\n },\n { description: \"Anchor key intersection with CTE-based AND logic\", tableLevel: true },\n );\n\n // --- FTS write (atomic INSERT OR REPLACE by mapped rowid) ---\n //\n // issue #1103: `scope`/`entry_path` are UNINDEXED on the FTS5 table, so\n // the old `DELETE ... WHERE scope=? AND entry_path=?` full-scanned it\n // (measured 67% of a production D1's read budget). `writeFtsRow` writes by\n // the stable mapped rowid instead (O(1)) and is race-safe under D1's\n // no-transaction model — see the helper block near the bottom of the file.\n this.sqliteProvider.registerAction(\n \"fts_write\",\n async (ctx, params) => {\n const { scope, path, summary } = params as { scope: string; path: string; summary: string };\n await writeFtsRow(ctx.db, scope, path, segmentCJK(summary));\n return { success: true };\n },\n {\n description: \"Write summary to FTS5 index (insert-or-replace by rowid, not full scan)\",\n tableLevel: true,\n },\n );\n\n // --- FTS query (MATCH + rank ordering, optional metaFilter via json_extract JOIN) ---\n this.sqliteProvider.registerAction(\n \"fts_query\",\n async (ctx, params) => {\n const { text, limit, meta_filter, domain, domains, path_prefix, tenant_scope } = params as {\n text: string;\n limit?: number;\n meta_filter?: Record<string, unknown>;\n domain?: string;\n domains?: string[];\n path_prefix?: string;\n tenant_scope?: string;\n };\n const tScope = tenant_scope ?? \"\";\n\n // Per-domain isolation: when caller passes a `domain` (or a\n // `domains` set — the `boundDomains` fallback, issue #936 P4 leak\n // fix), JOIN ds_indexed_entry and filter so cross-tenant data\n // doesn't leak into recall. Without this, a daemon hosting multiple\n // blocklets/callers in one db scores every tenant's docs against\n // every query — FTS bleed.\n //\n // We deliberately don't ALSO filter by `scope` as a path\n // prefix here. Anchor queries treat `scope` as a strict path-\n // LIKE filter (legacy), but FTS callers commonly use\n // `scope: \"/<domain>\"` as a shorthand for the domain — paths\n // inside that domain don't always start with the scope string\n // (the docs-rag e2e indexes `/docs/foo.md` paths under domain\n // `docs-rag`; restricting on both produced false negatives).\n const needsJoin =\n (meta_filter && Object.keys(meta_filter).length > 0) ||\n (typeof domain === \"string\" && domain.length > 0) ||\n (Array.isArray(domains) && domains.length > 0) ||\n (typeof path_prefix === \"string\" && path_prefix.length > 0);\n\n let query: ReturnType<typeof sql>;\n\n if (needsJoin) {\n const conditions: ReturnType<typeof sql>[] = [\n sql`idx_fts MATCH ${text}`,\n sql`idx_fts.scope = ${tScope}`,\n ];\n if (domain) conditions.push(sql`ie.domain = ${domain}`);\n if (domains && domains.length > 0) {\n const domainPlaceholders = domains.map((d) => sql`${d}`);\n conditions.push(sql`ie.domain IN (${sql.join(domainPlaceholders, sql`, `)})`);\n }\n // Explicit entry_path prefix (distinct from the FTS `scope`/`domain`\n // shorthand above): applied to the MATCH-bounded rows over the\n // `(scope, entry_path)` PK, so subtree scoping stays index-friendly\n // and adds no full scan. Enables `/user/<sub>` searches to return the\n // subtree's top-K instead of being crowded out cross-collection.\n if (path_prefix) conditions.push(sql`ie.entry_path LIKE ${`${path_prefix}%`}`);\n for (const [key, expected] of Object.entries(meta_filter ?? {})) {\n if (Array.isArray(expected)) {\n if (expected.length === 0) {\n // Empty array matches nothing\n return { rows: [] };\n }\n const placeholders = expected.map((v) => sql`${v}`);\n conditions.push(\n sql`json_extract(ie.meta, ${`$.${key}`}) IN (${sql.join(placeholders, sql`, `)})`,\n );\n } else {\n conditions.push(sql`json_extract(ie.meta, ${`$.${key}`}) = ${expected}`);\n }\n }\n const where = sql.join(conditions, sql` AND `);\n query =\n limit != null\n ? sql`SELECT idx_fts.entry_path, idx_fts.rank FROM idx_fts INNER JOIN ds_indexed_entry ie ON idx_fts.entry_path = ie.entry_path WHERE ${where} ORDER BY idx_fts.rank LIMIT ${limit}`\n : sql`SELECT idx_fts.entry_path, idx_fts.rank FROM idx_fts INNER JOIN ds_indexed_entry ie ON idx_fts.entry_path = ie.entry_path WHERE ${where} ORDER BY idx_fts.rank`;\n } else {\n // No domain/metaFilter join needed — but tenant `scope` is ALWAYS\n // enforced (the shared-file isolation boundary), so this still filters\n // idx_fts.scope over the MATCH-bounded rows.\n query =\n limit != null\n ? sql`SELECT entry_path, rank FROM idx_fts WHERE idx_fts MATCH ${text} AND scope = ${tScope} ORDER BY rank LIMIT ${limit}`\n : sql`SELECT entry_path, rank FROM idx_fts WHERE idx_fts MATCH ${text} AND scope = ${tScope} ORDER BY rank`;\n }\n\n const rows = await ctx.db.all<{ entry_path: string; rank: number }>(query);\n return { rows };\n },\n {\n description: \"FTS5 search with optional domain/scope/metaFilter isolation\",\n tableLevel: true,\n },\n );\n\n // --- FTS delete (by mapped rowid — see fts_write comment, issue #1103) ---\n this.sqliteProvider.registerAction(\n \"fts_delete\",\n async (ctx, params) => {\n const { scope, path } = params as { scope: string; path: string };\n await deleteFtsRow(ctx.db, scope, path);\n return { success: true };\n },\n {\n description: \"Remove entry from FTS5 index (delete by rowid, not full scan)\",\n tableLevel: true,\n },\n );\n\n // --- FTS read (single entry's stored summary) ---\n //\n // Fetch the stored summary text for a path. CJK summaries are\n // stored space-segmented for FTS matching — callers that need the\n // original text for downstream synthesis should keep the source\n // around or accept the segmentation. Useful for diagnostics and\n // ASCII-mostly corpora where the round-trip is faithful.\n this.sqliteProvider.registerAction(\n \"fts_get\",\n async (ctx, params) => {\n const { scope, path } = params as { scope: string; path: string };\n const rows = await ctx.db.all<{ summary: string }>(\n sql`SELECT summary FROM idx_fts WHERE scope = ${scope} AND entry_path = ${path} LIMIT 1`,\n );\n return { summary: rows[0]?.summary ?? null };\n },\n { description: \"Read the stored FTS summary for a single entry\", tableLevel: true },\n );\n\n // --- Count entries (efficient SQL COUNT with dynamic WHERE) ---\n this.sqliteProvider.registerAction(\n \"count_entries\",\n async (ctx, params) => {\n const filter = params as {\n tenant_scope?: string;\n domain?: string;\n state?: string;\n extractorId?: string;\n scope?: string;\n boundDomains?: string[];\n };\n const conditions: ReturnType<typeof sql>[] = [sql`scope = ${filter.tenant_scope ?? \"\"}`];\n if (filter.domain) conditions.push(sql`domain = ${filter.domain}`);\n if (filter.state) conditions.push(sql`state = ${filter.state}`);\n if (filter.extractorId) conditions.push(sql`extractor_id = ${filter.extractorId}`);\n if (filter.scope) conditions.push(sql`entry_path LIKE ${`${filter.scope}%`}`);\n if (filter.boundDomains && filter.boundDomains.length > 0) {\n const ps = filter.boundDomains.map((d) => sql`${d}`);\n conditions.push(sql`domain IN (${sql.join(ps, sql`, `)})`);\n }\n\n const query = sql`SELECT COUNT(*) as count FROM ds_indexed_entry WHERE ${sql.join(conditions, sql` AND `)}`;\n const rows = await ctx.db.all<{ count: number }>(query);\n return { count: rows[0]?.count ?? 0 };\n },\n { description: \"Count indexed entries with optional filters\", tableLevel: true },\n );\n\n // --- Embedding write (upsert) ---\n //\n // Stores the vector as Float32Array bytes in `embedding_blob`. The\n // legacy `embedding` TEXT column gets an empty string — older\n // databases were created with the column as NOT NULL and we can't\n // safely drop the constraint via ALTER TABLE without rewriting the\n // table, so writes have to satisfy it. New databases (created by\n // the current schema.ts) omit the NOT NULL and would accept NULL\n // here, but writing '' works for both shapes.\n this.sqliteProvider.registerAction(\n \"embedding_write\",\n async (ctx, params) => {\n const { scope, path, embedding } = params as {\n scope: string;\n path: string;\n embedding: number[];\n };\n const blob = embeddingToBlob(embedding);\n // Single atomic upsert, not DELETE-then-INSERT: two non-atomic\n // statements let concurrent embedding_write calls for the same\n // entry_path interleave (the DELETE yields to the event loop before\n // the INSERT) — both DELETEs succeed, then the second INSERT collides\n // on the PRIMARY KEY. See d1-index-backend.ts's writeEmbedding for the\n // same race confirmed on production D1.\n await ctx.db.run(\n sql`INSERT INTO idx_embedding_vector (scope, entry_path, embedding, embedding_blob, dimension) VALUES (${scope}, ${path}, '', ${blob}, ${embedding.length})\n ON CONFLICT(scope, entry_path) DO UPDATE SET\n embedding = excluded.embedding,\n embedding_blob = excluded.embedding_blob,\n dimension = excluded.dimension`,\n );\n return { success: true };\n },\n { description: \"Write embedding vector\", tableLevel: true },\n );\n\n // --- Embedding query (brute-force, returns all for JS-side cosine similarity) ---\n //\n // Joins `ds_indexed_entry` so callers can pass `scope` / `domain` and\n // get only the embeddings that belong to that domain — without this\n // join, two domains sharing the same physical SQLite database (the\n // production daemon hosts every per-blocklet domain in one\n // `~/.afs/agent-index/index.db`) bleed each other's vectors into\n // every recall, scoring across-tenant noise alongside the real\n // candidates.\n this.sqliteProvider.registerAction(\n \"embedding_query\",\n async (ctx, params) => {\n // See `fts_query` for why we filter on `domain`/`domains` only and\n // not on `scope` as a path prefix — same reasoning, same dedup.\n const filter = (params ?? {}) as {\n tenant_scope?: string;\n domain?: string;\n domains?: string[];\n path_prefix?: string;\n };\n const conditions: ReturnType<typeof sql>[] = [\n sql`idx_embedding_vector.scope = ${filter.tenant_scope ?? \"\"}`,\n ];\n if (filter.domain) conditions.push(sql`ds_indexed_entry.domain = ${filter.domain}`);\n if (filter.domains && filter.domains.length > 0) {\n const domainPlaceholders = filter.domains.map((d) => sql`${d}`);\n conditions.push(\n sql`ds_indexed_entry.domain IN (${sql.join(domainPlaceholders, sql`, `)})`,\n );\n }\n // Subtree scope: prune the cosine candidate set BEFORE the O(N) JS\n // similarity pass (see queryVector). Filters the domain-scoped rows over\n // the ds_indexed_entry `(…, entry_path)` index.\n if (filter.path_prefix) {\n conditions.push(sql`ds_indexed_entry.entry_path LIKE ${`${filter.path_prefix}%`}`);\n }\n const where = sql`WHERE ${sql.join(conditions, sql` AND `)}`;\n const rows = await ctx.db.all<{\n entry_path: string;\n embedding: string | null;\n embedding_blob: ArrayBuffer | Uint8Array | Buffer | null;\n }>(\n sql`SELECT idx_embedding_vector.entry_path AS entry_path,\n idx_embedding_vector.embedding AS embedding,\n idx_embedding_vector.embedding_blob AS embedding_blob\n FROM idx_embedding_vector\n JOIN ds_indexed_entry\n ON ds_indexed_entry.scope = idx_embedding_vector.scope\n AND ds_indexed_entry.entry_path = idx_embedding_vector.entry_path\n ${where}`,\n );\n return { rows };\n },\n {\n description: \"Fetch embeddings (filtered by domain/scope) for vector search\",\n tableLevel: true,\n },\n );\n\n // --- Embedding delete ---\n this.sqliteProvider.registerAction(\n \"embedding_delete\",\n async (ctx, params) => {\n const { scope, path } = params as { scope: string; path: string };\n await ctx.db.run(\n sql`DELETE FROM idx_embedding_vector WHERE scope = ${scope} AND entry_path = ${path}`,\n );\n return { success: true };\n },\n { description: \"Remove embedding vector\", tableLevel: true },\n );\n\n // --- Single upsert (no transaction wrapper — safe for concurrent calls) ---\n\n this.sqliteProvider.registerAction(\n \"upsert_entry\",\n async (ctx, params) => {\n const e = params as {\n scope: string;\n entry_path: string;\n domain: string;\n state: string;\n indexed_at: string;\n updated_at: string;\n anchor_count: number;\n source_path?: string | null;\n source_hash?: string | null;\n extractor_id?: string | null;\n meta?: string | null;\n };\n await ctx.db.run(\n sql`INSERT OR REPLACE INTO ds_indexed_entry (scope, entry_path, domain, state, indexed_at, updated_at, anchor_count, source_path, source_hash, extractor_id, meta) VALUES (${e.scope}, ${e.entry_path}, ${e.domain}, ${e.state}, ${e.indexed_at}, ${e.updated_at}, ${e.anchor_count}, ${e.source_path ?? null}, ${e.source_hash ?? null}, ${e.extractor_id ?? null}, ${e.meta ?? null})`,\n );\n return { success: true };\n },\n { description: \"Single entry upsert via INSERT OR REPLACE\", tableLevel: true },\n );\n\n // --- Single replace anchors (no outer transaction — safe for concurrent calls) ---\n\n this.sqliteProvider.registerAction(\n \"replace_anchors\",\n async (ctx, params) => {\n const { scope, path, dataRows, keyRows } = params as {\n scope: string;\n path: string;\n dataRows: {\n entry_path: string;\n anchor_type: string;\n confidence: number;\n raw_anchor: string;\n }[];\n keyRows: {\n index_key: string;\n entry_path: string;\n anchor_type: string;\n confidence: number;\n }[];\n };\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_data WHERE scope = ${scope} AND entry_path = ${path}`,\n );\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_key WHERE scope = ${scope} AND entry_path = ${path}`,\n );\n for (const row of dataRows) {\n await ctx.db.run(\n sql`INSERT INTO idx_anchor_data (scope, entry_path, anchor_type, confidence, raw_anchor) VALUES (${scope}, ${row.entry_path}, ${row.anchor_type}, ${row.confidence}, ${row.raw_anchor})`,\n );\n }\n for (const row of keyRows) {\n await ctx.db.run(\n sql`INSERT OR REPLACE INTO idx_anchor_key (scope, index_key, entry_path, anchor_type, confidence) VALUES (${scope}, ${row.index_key}, ${row.entry_path}, ${row.anchor_type}, ${row.confidence})`,\n );\n }\n return { success: true };\n },\n { description: \"Replace all anchors for a single entry\", tableLevel: true },\n );\n\n this.sqliteProvider.registerAction(\n \"write_indexed_document\",\n async (ctx, params) => {\n const { entry, path, dataRows, keyRows, summary } = params as {\n entry: {\n scope: string;\n entry_path: string;\n domain: string;\n state: string;\n indexed_at: string;\n updated_at: string;\n anchor_count: number;\n source_path?: string | null;\n source_hash?: string | null;\n extractor_id?: string | null;\n meta?: string | null;\n };\n path: string;\n dataRows: {\n entry_path: string;\n anchor_type: string;\n confidence: number;\n raw_anchor: string;\n }[];\n keyRows: {\n index_key: string;\n entry_path: string;\n anchor_type: string;\n confidence: number;\n }[];\n summary?: string | null;\n };\n await ctx.db.run(sql.raw(\"BEGIN TRANSACTION\"));\n try {\n await ctx.db.run(\n sql`INSERT OR REPLACE INTO ds_indexed_entry (scope, entry_path, domain, state, indexed_at, updated_at, anchor_count, source_path, source_hash, extractor_id, meta) VALUES (${entry.scope}, ${entry.entry_path}, ${entry.domain}, ${entry.state}, ${entry.indexed_at}, ${entry.updated_at}, ${entry.anchor_count}, ${entry.source_path ?? null}, ${entry.source_hash ?? null}, ${entry.extractor_id ?? null}, ${entry.meta ?? null})`,\n );\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_data WHERE scope = ${entry.scope} AND entry_path = ${path}`,\n );\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_key WHERE scope = ${entry.scope} AND entry_path = ${path}`,\n );\n for (const row of dataRows) {\n await ctx.db.run(\n sql`INSERT INTO idx_anchor_data (scope, entry_path, anchor_type, confidence, raw_anchor) VALUES (${entry.scope}, ${row.entry_path}, ${row.anchor_type}, ${row.confidence}, ${row.raw_anchor})`,\n );\n }\n for (const row of keyRows) {\n await ctx.db.run(\n sql`INSERT OR REPLACE INTO idx_anchor_key (scope, index_key, entry_path, anchor_type, confidence) VALUES (${entry.scope}, ${row.index_key}, ${row.entry_path}, ${row.anchor_type}, ${row.confidence})`,\n );\n }\n if (summary) {\n await writeFtsRow(ctx.db, entry.scope, path, segmentCJK(summary));\n }\n await ctx.db.run(sql.raw(\"COMMIT\"));\n return { success: true };\n } catch (error) {\n await ctx.db.run(sql.raw(\"ROLLBACK\"));\n throw error;\n }\n },\n {\n description: \"Write entry metadata, anchors, and FTS summary in one transaction\",\n tableLevel: true,\n },\n );\n\n // --- Batch purge (delete entry + all associated data in one transaction) ---\n\n this.sqliteProvider.registerAction(\n \"batch_purge\",\n async (ctx, params) => {\n const { scope, paths, skipVector } = params as {\n scope: string;\n paths: string[];\n skipVector?: boolean;\n };\n await ctx.db.run(sql.raw(\"BEGIN TRANSACTION\"));\n try {\n for (const path of paths) {\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_data WHERE scope = ${scope} AND entry_path = ${path}`,\n );\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_key WHERE scope = ${scope} AND entry_path = ${path}`,\n );\n // issue #1103: delete by mapped rowid, not full-table scan.\n await deleteFtsRow(ctx.db, scope, path);\n // Phase 4 offline reset: skipVector leaves the embedding untouched.\n if (skipVector !== true) {\n await ctx.db.run(\n sql`DELETE FROM idx_embedding_vector WHERE scope = ${scope} AND entry_path = ${path}`,\n );\n }\n await ctx.db.run(\n sql`DELETE FROM ds_indexed_entry WHERE scope = ${scope} AND entry_path = ${path}`,\n );\n }\n await ctx.db.run(sql.raw(\"COMMIT\"));\n return { count: paths.length };\n } catch (error) {\n await ctx.db.run(sql.raw(\"ROLLBACK\"));\n throw error;\n }\n },\n {\n description: \"Purge entries and all associated data in a single transaction\",\n tableLevel: true,\n },\n );\n\n // --- Transaction-batched operations ---\n\n this.sqliteProvider.registerAction(\n \"batch_upsert_entries\",\n async (ctx, params) => {\n const entries = params.entries as {\n scope: string;\n entry_path: string;\n domain: string;\n state: string;\n indexed_at: string;\n updated_at: string;\n anchor_count: number;\n source_path?: string | null;\n source_hash?: string | null;\n extractor_id?: string | null;\n meta?: string | null;\n }[];\n await ctx.db.run(sql.raw(\"BEGIN TRANSACTION\"));\n try {\n for (const e of entries) {\n await ctx.db.run(\n sql`INSERT OR REPLACE INTO ds_indexed_entry (scope, entry_path, domain, state, indexed_at, updated_at, anchor_count, source_path, source_hash, extractor_id, meta) VALUES (${e.scope}, ${e.entry_path}, ${e.domain}, ${e.state}, ${e.indexed_at}, ${e.updated_at}, ${e.anchor_count}, ${e.source_path ?? null}, ${e.source_hash ?? null}, ${e.extractor_id ?? null}, ${e.meta ?? null})`,\n );\n }\n await ctx.db.run(sql.raw(\"COMMIT\"));\n return { count: entries.length };\n } catch (error) {\n await ctx.db.run(sql.raw(\"ROLLBACK\"));\n throw error;\n }\n },\n { description: \"Batch upsert entries in a single transaction\", tableLevel: true },\n );\n\n this.sqliteProvider.registerAction(\n \"batch_replace_anchors\",\n async (ctx, params) => {\n const { scope, items } = params as {\n scope: string;\n items: {\n path: string;\n dataRows: {\n entry_path: string;\n anchor_type: string;\n confidence: number;\n raw_anchor: string;\n }[];\n keyRows: {\n index_key: string;\n entry_path: string;\n anchor_type: string;\n confidence: number;\n }[];\n }[];\n };\n await ctx.db.run(sql.raw(\"BEGIN TRANSACTION\"));\n try {\n for (const item of items) {\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_data WHERE scope = ${scope} AND entry_path = ${item.path}`,\n );\n await ctx.db.run(\n sql`DELETE FROM idx_anchor_key WHERE scope = ${scope} AND entry_path = ${item.path}`,\n );\n for (const row of item.dataRows) {\n await ctx.db.run(\n sql`INSERT INTO idx_anchor_data (scope, entry_path, anchor_type, confidence, raw_anchor) VALUES (${scope}, ${row.entry_path}, ${row.anchor_type}, ${row.confidence}, ${row.raw_anchor})`,\n );\n }\n for (const row of item.keyRows) {\n await ctx.db.run(\n sql`INSERT OR REPLACE INTO idx_anchor_key (scope, index_key, entry_path, anchor_type, confidence) VALUES (${scope}, ${row.index_key}, ${row.entry_path}, ${row.anchor_type}, ${row.confidence})`,\n );\n }\n }\n await ctx.db.run(sql.raw(\"COMMIT\"));\n return { count: items.length };\n } catch (error) {\n await ctx.db.run(sql.raw(\"ROLLBACK\"));\n throw error;\n }\n },\n { description: \"Batch replace anchors in a single transaction\", tableLevel: true },\n );\n\n this.sqliteProvider.registerAction(\n \"batch_write_fts\",\n async (ctx, params) => {\n const { scope, items } = params as {\n scope: string;\n items: { path: string; summary: string }[];\n };\n await ctx.db.run(sql.raw(\"BEGIN TRANSACTION\"));\n try {\n for (const item of items) {\n // issue #1103: atomic insert-or-replace by mapped rowid, not full-table scan.\n await writeFtsRow(ctx.db, scope, item.path, item.summary);\n }\n await ctx.db.run(sql.raw(\"COMMIT\"));\n return { count: items.length };\n } catch (error) {\n await ctx.db.run(sql.raw(\"ROLLBACK\"));\n throw error;\n }\n },\n { description: \"Batch write FTS summaries in a single transaction\", tableLevel: true },\n );\n\n // --- Changelog follower cursor ---\n\n this.sqliteProvider.registerAction(\n \"cursor_read\",\n async (ctx, params) => {\n const { consumer_id } = params as { consumer_id: string };\n const rows = await ctx.db.all<{\n consumer_id: string;\n last_seq: number;\n watermarks: string | null;\n updated_at: number;\n }>(\n sql`SELECT consumer_id, last_seq, watermarks, updated_at FROM idx_index_cursor WHERE consumer_id = ${consumer_id} LIMIT 1`,\n );\n if (rows.length === 0) return { cursor: null };\n const row = rows[0]!;\n return {\n cursor: {\n consumerId: row.consumer_id,\n lastSeq: row.last_seq,\n watermarks: row.watermarks\n ? (JSON.parse(row.watermarks) as Record<string, number>)\n : undefined,\n updatedAt: row.updated_at,\n },\n };\n },\n { description: \"Read changelog follower cursor\", tableLevel: true },\n );\n\n this.sqliteProvider.registerAction(\n \"cursor_write\",\n async (ctx, params) => {\n const { consumer_id, last_seq, watermarks } = params as {\n consumer_id: string;\n last_seq: number;\n watermarks?: Record<string, number> | null;\n };\n const now = Date.now();\n // Monotonic write (issue #935 BLOCK fix #2) — see D1IndexBackend.writeCursor.\n // Post-cutover (index-follower-dirty-path §4.2) `last_seq` is a DEAD column\n // the follower no longer advances; this action is retained for out-of-band\n // cursor seeding + tests, so the monotonic guard still matters there.\n await ctx.db.run(\n sql`INSERT INTO idx_index_cursor (consumer_id, last_seq, watermarks, updated_at, served_at)\n\t VALUES (${consumer_id}, ${last_seq}, ${watermarks ? JSON.stringify(watermarks) : null}, ${now}, ${now})\n ON CONFLICT(consumer_id) DO UPDATE SET\n last_seq = MAX(last_seq, excluded.last_seq),\n watermarks = excluded.watermarks,\n\t\tupdated_at = excluded.updated_at,\n\t\tserved_at = excluded.served_at`,\n );\n return { success: true };\n },\n {\n description: \"Write index follower cursor (monotonic last_seq; dead column post-cutover)\",\n tableLevel: true,\n },\n );\n\n this.sqliteProvider.registerAction(\n \"cursor_mark_served\",\n async (ctx, params) => {\n const { consumer_id } = params as { consumer_id: string };\n await ctx.db.run(\n sql`UPDATE idx_index_cursor SET served_at = ${Date.now()} WHERE consumer_id = ${consumer_id}`,\n );\n return { success: true };\n },\n { description: \"Mark changelog follower cursor as served\", tableLevel: true },\n );\n\n this.sqliteProvider.registerAction(\n \"cursor_init\",\n async (ctx, params) => {\n const { consumer_id, instance_did, domains } = params as {\n consumer_id: string;\n instance_did?: string | null;\n domains?: string | null;\n };\n const now = Date.now();\n await ctx.db.run(\n sql`INSERT OR IGNORE INTO idx_index_cursor (consumer_id, last_seq, watermarks, updated_at, served_at, instance_did, domains)\n\t VALUES (${consumer_id}, 0, NULL, ${now}, ${now}, ${instance_did ?? null}, ${domains ?? null})`,\n );\n return { success: true };\n },\n { description: \"Initialize changelog follower cursor (idempotent)\", tableLevel: true },\n );\n\n this.sqliteProvider.registerAction(\n \"cursor_scope_list\",\n async (ctx, params) => {\n const { family_consumer_id } = params as { family_consumer_id: string };\n const rows = await ctx.db.all<{\n consumer_id: string;\n instance_did: string | null;\n domains: string | null;\n updated_at: number;\n served_at: number | null;\n }>(\n sql`SELECT consumer_id, instance_did, domains, updated_at, served_at\n\t FROM idx_index_cursor\n\t WHERE consumer_id LIKE ${`${family_consumer_id}:%`}`,\n );\n return {\n registrations: rows.map((r) => ({\n scope: cursorScopeFromConsumerId(r.consumer_id, family_consumer_id, r.instance_did),\n consumerId: r.consumer_id,\n instanceDid: r.instance_did,\n domainsJson: r.domains,\n updatedAt: r.updated_at,\n servedAt: r.served_at ?? r.updated_at,\n })),\n };\n },\n { description: \"List changelog follower cursor registrations\", tableLevel: true },\n );\n }\n\n // --- Changelog follower cursor ---\n\n async readCursor(consumerId: string): Promise<CursorRow | null> {\n const result = await this.afs.exec(this.tableAction(\"idx_index_cursor\", \"cursor_read\"), {\n consumer_id: consumerId,\n });\n return (result.data as { cursor: CursorRow | null }).cursor;\n }\n\n async writeCursor(\n consumerId: string,\n lastSeq: number,\n watermarks?: Record<string, number>,\n ): Promise<void> {\n await this.afs.exec(this.tableAction(\"idx_index_cursor\", \"cursor_write\"), {\n consumer_id: consumerId,\n last_seq: lastSeq,\n watermarks: watermarks ?? null,\n });\n }\n\n async markCursorServed(consumerId: string): Promise<void> {\n await this.afs.exec(this.tableAction(\"idx_index_cursor\", \"cursor_mark_served\"), {\n consumer_id: consumerId,\n });\n }\n\n async initCursor(\n consumerId: string,\n registration?: { instanceDid?: string; domainsJson?: string },\n ): Promise<void> {\n await this.afs.exec(this.tableAction(\"idx_index_cursor\", \"cursor_init\"), {\n consumer_id: consumerId,\n instance_did: registration?.instanceDid ?? null,\n domains: registration?.domainsJson ?? null,\n });\n }\n\n async listCursorScopes(consumerId: string): Promise<CursorScopeRegistration[]> {\n const result = await this.afs.exec(this.tableAction(\"idx_index_cursor\", \"cursor_scope_list\"), {\n family_consumer_id: consumerId,\n });\n return (\n result.data as {\n registrations: CursorScopeRegistration[];\n }\n ).registrations;\n }\n\n async close(): Promise<void> {\n this.afs = undefined!;\n this.sqliteProvider = undefined!;\n }\n\n // --- Entry metadata (via AFS CRUD actions) ---\n\n async writeEntry(entry: IndexedEntry): Promise<void> {\n await this.afs.exec(\n this.tableAction(\"ds_indexed_entry\", \"upsert_entry\"),\n this.entryToRow(entry),\n );\n }\n\n async writeIndexedDocument(document: IndexedDocumentWrite): Promise<void> {\n const path = document.entry.entryPath;\n const anchors = document.anchors ?? [];\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"write_indexed_document\"), {\n entry: this.entryToRow(document.entry),\n path,\n dataRows: anchors.map((anchor) => ({\n entry_path: path,\n anchor_type: anchor.type,\n confidence: anchor.confidence,\n raw_anchor: JSON.stringify(anchor),\n })),\n keyRows: anchors.flatMap((anchor) =>\n this.keyRegistry.generateKeys(anchor).map((key) => ({\n index_key: key,\n entry_path: path,\n anchor_type: anchor.type,\n confidence: anchor.confidence,\n })),\n ),\n summary: document.summary ?? null,\n });\n }\n\n private entryToRow(entry: IndexedEntry): Record<string, unknown> {\n const data: Record<string, unknown> = {\n scope: this.scope,\n entry_path: entry.entryPath,\n domain: entry.domain,\n state: entry.state,\n indexed_at: entry.indexedAt,\n updated_at: entry.updatedAt,\n anchor_count: entry.anchorCount,\n };\n if (entry.sourcePath != null) data.source_path = entry.sourcePath;\n if (entry.sourceHash != null) data.source_hash = entry.sourceHash;\n if (entry.extractorId != null) data.extractor_id = entry.extractorId;\n if (entry.meta != null) data.meta = JSON.stringify(entry.meta);\n return data;\n }\n\n async getEntry(path: string): Promise<IndexedEntry | null> {\n const result = await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"query\"), {\n where: { scope: this.scope, entry_path: path },\n limit: 1,\n });\n\n const data = result.data as { rows: EntryRow[] };\n if (!data.rows || data.rows.length === 0) return null;\n return rowToEntry(data.rows[0]!);\n }\n\n async removeEntry(path: string): Promise<void> {\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"delete_where\"), {\n where: { scope: this.scope, entry_path: path },\n });\n }\n\n async removeIndexedDocument(path: string, opts?: RemoveIndexOptions): Promise<void> {\n await this.purgeEntries([path], opts);\n }\n\n async listEntries(filter: EntryFilter): Promise<IndexedEntry[]> {\n const where: Record<string, unknown> = { scope: this.scope };\n\n if (filter.domain) where.domain = filter.domain;\n if (filter.state) where.state = filter.state;\n if (filter.extractorId) where.extractor_id = filter.extractorId;\n // Optional caller-supplied path-prefix (the legacy `scope:\"/sub\"`\n // shorthand) narrows WITHIN the tenant — orthogonal to the tenant `scope`\n // column above, which is the isolation boundary.\n if (filter.scope) where.entry_path = { $like: `${filter.scope}%` };\n if (filter.boundDomains && filter.boundDomains.length > 0) {\n where.domain = { $in: filter.boundDomains };\n }\n\n const result = await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"query\"), { where });\n const data = result.data as { rows: EntryRow[] };\n return (data.rows ?? []).map(rowToEntry);\n }\n\n async markStale(filter: StaleFilter): Promise<number> {\n const entryWhere: Record<string, unknown> = { scope: this.scope, state: \"active\" };\n\n if (filter.extractorId) entryWhere.extractor_id = filter.extractorId;\n if (filter.domain) entryWhere.domain = filter.domain;\n if (filter.scope) entryWhere.entry_path = { $like: `${filter.scope}%` };\n if (filter.boundDomains && filter.boundDomains.length > 0) {\n entryWhere.domain = { $in: filter.boundDomains };\n }\n\n if (filter.anchorType) {\n const anchorResult = await this.afs.exec(this.tableAction(\"idx_anchor_data\", \"query\"), {\n where: { scope: this.scope, anchor_type: filter.anchorType },\n });\n const anchorData = anchorResult.data as { rows: { entry_path: string }[] };\n const paths = [...new Set(anchorData.rows.map((r) => r.entry_path))];\n\n if (paths.length === 0) return 0;\n\n if (typeof entryWhere.entry_path === \"object\") {\n entryWhere.entry_path = {\n ...(entryWhere.entry_path as Record<string, unknown>),\n $in: paths,\n };\n } else {\n entryWhere.entry_path = { $in: paths };\n }\n }\n\n const result = await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"update_where\"), {\n where: entryWhere,\n data: { state: \"stale\" },\n });\n\n return (result.data as { affectedRows: number }).affectedRows;\n }\n\n // --- Count (via custom action) ---\n\n async countEntries(filter: EntryFilter): Promise<number> {\n const result = await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"count_entries\"), {\n tenant_scope: this.scope,\n domain: filter.domain,\n state: filter.state,\n extractorId: filter.extractorId,\n scope: filter.scope,\n boundDomains: filter.boundDomains,\n });\n return (result.data as { count: number }).count;\n }\n\n // --- Anchor operations ---\n\n async writeAnchors(path: string, anchors: Anchor[]): Promise<void> {\n const dataRows = anchors.map((anchor) => ({\n entry_path: path,\n anchor_type: anchor.type,\n confidence: anchor.confidence,\n raw_anchor: JSON.stringify(anchor),\n }));\n const keyRows = anchors.flatMap((anchor) =>\n this.keyRegistry.generateKeys(anchor).map((key) => ({\n index_key: key,\n entry_path: path,\n anchor_type: anchor.type,\n confidence: anchor.confidence,\n })),\n );\n await this.afs.exec(this.tableAction(\"idx_anchor_data\", \"replace_anchors\"), {\n scope: this.scope,\n path,\n dataRows,\n keyRows,\n });\n }\n\n async queryAnchors(query: AnchorQuery): Promise<AnchorResult[]> {\n if (query.keys.length === 0) return [];\n\n // Run each query value through the same `KeyGenerator` that\n // `writeAnchors` uses on the write side. Otherwise the lookup\n // table — which stores `topic:foo` (prefixed, lowercased) — never\n // matches a raw user-provided \"foo\". Each input `values: [...]`\n // expands to the union of generated keys; if the type has no\n // generator (e.g. arbitrary `type: \"tag\"`), the registry falls\n // back to `generateExactKeys` for parity.\n const keyGroups = query.keys.map((kg) => {\n const values = new Set<string>();\n for (const raw of kg.values) {\n const synthetic = { type: kg.type ?? \"exact\", values: [raw], confidence: 1 } as Anchor;\n const generated = this.keyRegistry.generateKeys(synthetic);\n for (const k of generated) values.add(k);\n // Also include the raw value so callers that already pass\n // pre-prefixed keys keep working.\n values.add(raw);\n }\n return { values: [...values] };\n });\n\n const params: Record<string, unknown> = {\n key_groups: keyGroups,\n tenant_scope: this.scope,\n scope: query.scope,\n min_confidence: query.minConfidence,\n limit: query.limit,\n };\n if (query.pathPrefix) params.path_prefix = query.pathPrefix;\n if (query.metaFilter && Object.keys(query.metaFilter).length > 0) {\n params.meta_filter = query.metaFilter;\n }\n if (query.domains && query.domains.length > 0) {\n params.domains = query.domains;\n }\n\n const result = await this.afs.exec(\n this.tableAction(\"idx_anchor_key\", \"anchor_intersection\"),\n params,\n );\n\n const data = result.data as { rows: { entry_path: string; score: number }[] };\n return (data.rows ?? []).map((r) => ({\n entryPath: r.entry_path,\n score: r.score,\n }));\n }\n\n async removeAnchors(path: string): Promise<void> {\n await this.afs.exec(this.tableAction(\"idx_anchor_data\", \"delete_where\"), {\n where: { scope: this.scope, entry_path: path },\n });\n await this.afs.exec(this.tableAction(\"idx_anchor_key\", \"delete_where\"), {\n where: { scope: this.scope, entry_path: path },\n });\n }\n\n async getAnchorData(path: string): Promise<AnchorData[]> {\n const result = await this.afs.exec(this.tableAction(\"idx_anchor_data\", \"query\"), {\n where: { scope: this.scope, entry_path: path },\n });\n const rows = (result.data as { rows: AnchorDataRow[] }).rows;\n\n return (rows ?? []).map((r) => ({\n entryPath: r.entry_path,\n anchorType: r.anchor_type,\n confidence: r.confidence,\n rawAnchor: r.raw_anchor,\n }));\n }\n\n // --- FTS operations (via custom actions) ---\n\n async writeSummary(path: string, summary: string): Promise<void> {\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"fts_write\"), {\n scope: this.scope,\n path,\n summary,\n });\n }\n\n async getSummary(path: string): Promise<string | null> {\n const result = await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"fts_get\"), {\n scope: this.scope,\n path,\n });\n const data = result.data as { summary?: string | null };\n return data.summary ?? null;\n }\n\n async queryFTS(\n text: string,\n limit?: number,\n metaFilter?: Record<string, unknown>,\n opts?: { domain?: string; domains?: string[]; scope?: string; pathPrefix?: string },\n ): Promise<FTSResult[]> {\n try {\n // Sanitize FTS5 special characters to prevent query syntax errors\n const sanitized = sanitizeFTS5(text);\n if (!sanitized) return [];\n\n const params: Record<string, unknown> = {\n text: sanitized,\n limit,\n tenant_scope: this.scope,\n };\n if (metaFilter && Object.keys(metaFilter).length > 0) {\n params.meta_filter = metaFilter;\n }\n if (opts?.domain) params.domain = opts.domain;\n if (opts?.domains && opts.domains.length > 0) params.domains = opts.domains;\n if (opts?.scope) params.scope = opts.scope;\n if (opts?.pathPrefix) params.path_prefix = opts.pathPrefix;\n\n const result = await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"fts_query\"), params);\n const rows = (result.data as { rows: { entry_path: string; rank: number }[] }).rows;\n\n return (rows ?? []).map((r) => ({\n entryPath: r.entry_path,\n // FTS5 rank is negative (lower = better). Normalize to positive score.\n rank: -r.rank,\n }));\n } catch {\n // FTS5 query syntax error — return empty results rather than crashing\n return [];\n }\n }\n\n async removeSummary(path: string): Promise<void> {\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"fts_delete\"), {\n scope: this.scope,\n path,\n });\n }\n\n // --- Embedding operations ---\n\n async writeEmbedding(path: string, embedding: number[]): Promise<void> {\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"embedding_write\"), {\n scope: this.scope,\n path,\n embedding,\n });\n if (this.embeddingDimension === 0 && embedding.length > 0) {\n this.embeddingDimension = embedding.length;\n }\n }\n\n async queryVector(\n embedding: number[],\n limit?: number,\n opts?: { domain?: string; domains?: string[]; scope?: string; pathPrefix?: string },\n ): Promise<VectorResult[]> {\n // The backend action JOINs ds_indexed_entry, so a domain/domains or\n // scope filter here is what keeps a multi-tenant SQLite from leaking\n // cross-domain similarities into recall (see comment on the\n // `embedding_query` action registration). `path_prefix` further narrows the\n // cosine candidate set to a subtree BEFORE the O(N) similarity pass — a\n // scale win, not just an isolation one.\n const result = await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"embedding_query\"), {\n tenant_scope: this.scope,\n ...(opts?.domain ? { domain: opts.domain } : {}),\n ...(opts?.domains && opts.domains.length > 0 ? { domains: opts.domains } : {}),\n ...(opts?.scope ? { scope: opts.scope } : {}),\n ...(opts?.pathPrefix ? { path_prefix: opts.pathPrefix } : {}),\n });\n const rows =\n (\n result.data as {\n rows: {\n entry_path: string;\n embedding: string | null;\n embedding_blob: ArrayBuffer | Uint8Array | Buffer | null;\n }[];\n }\n ).rows ?? [];\n\n // Cache the query vector as Float32 once — the per-row decode is\n // already cheap, but doing the conversion N times is pure waste.\n const queryVec = Float32Array.from(embedding);\n const results: VectorResult[] = rows.map((r) => ({\n entryPath: r.entry_path,\n similarity: cosineSimilarityF32(queryVec, decodeStoredEmbedding(r)),\n }));\n\n results.sort((a, b) => b.similarity - a.similarity);\n if (limit != null) return results.slice(0, limit);\n return results;\n }\n\n async removeEmbedding(path: string): Promise<void> {\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"embedding_delete\"), {\n scope: this.scope,\n path,\n });\n }\n\n embeddingCapability(): { supported: boolean; dimension: number } {\n return { supported: true, dimension: this.embeddingDimension };\n }\n\n // --- Batch operations (transaction-wrapped) ---\n\n async writeEntriesBatch(entries: IndexedEntry[]): Promise<void> {\n if (entries.length === 0) return;\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"batch_upsert_entries\"), {\n entries: entries.map((e) => this.entryToRow(e)),\n });\n }\n\n async writeAnchorsBatch(items: { path: string; anchors: Anchor[] }[]): Promise<void> {\n if (items.length === 0) return;\n const batchItems = items.map((item) => ({\n path: item.path,\n dataRows: item.anchors.map((anchor) => ({\n entry_path: item.path,\n anchor_type: anchor.type,\n confidence: anchor.confidence,\n raw_anchor: JSON.stringify(anchor),\n })),\n keyRows: item.anchors.flatMap((anchor) =>\n this.keyRegistry.generateKeys(anchor).map((key) => ({\n index_key: key,\n entry_path: item.path,\n anchor_type: anchor.type,\n confidence: anchor.confidence,\n })),\n ),\n }));\n await this.afs.exec(this.tableAction(\"idx_anchor_data\", \"batch_replace_anchors\"), {\n scope: this.scope,\n items: batchItems,\n });\n }\n\n async writeSummariesBatch(items: { path: string; summary: string }[]): Promise<void> {\n if (items.length === 0) return;\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"batch_write_fts\"), {\n scope: this.scope,\n items,\n });\n }\n\n async purgeEntries(paths: string[], opts?: RemoveIndexOptions): Promise<number> {\n if (paths.length === 0) return 0;\n await this.afs.exec(this.tableAction(\"ds_indexed_entry\", \"batch_purge\"), {\n scope: this.scope,\n paths,\n // Phase 4 offline reset: leave embeddings intact (design §10.1 constraint B).\n skipVector: opts?.skipVector === true,\n });\n return paths.length;\n }\n\n // --- Combined query ---\n\n async queryCombined(\n keys: { type: string; values: string[] }[],\n text?: string,\n opts?: QueryOpts,\n ): Promise<CombinedResult[]> {\n const anchorResults =\n keys.length > 0\n ? await this.queryAnchors({\n keys,\n scope: opts?.scope,\n minConfidence: opts?.minConfidence,\n limit: opts?.limit,\n })\n : [];\n\n const anchorPaths = new Set(anchorResults.map((r) => r.entryPath));\n\n const ftsResults = text ? await this.queryFTS(text, opts?.limit) : [];\n\n const results: CombinedResult[] = anchorResults.map((r) => {\n const fts = ftsResults.find((f) => f.entryPath === r.entryPath);\n return {\n entryPath: r.entryPath,\n anchorScore: r.score,\n ftsRank: fts?.rank,\n score: r.score + (fts?.rank ?? 0),\n };\n });\n\n for (const f of ftsResults) {\n if (!anchorPaths.has(f.entryPath)) {\n results.push({\n entryPath: f.entryPath,\n ftsRank: f.rank,\n score: f.rank,\n });\n }\n }\n\n results.sort((a, b) => b.score - a.score);\n if (opts?.limit != null) return results.slice(0, opts.limit);\n return results;\n }\n}\n\n// --- idx_fts rowid mapping helpers (issue #1103) ---\n//\n// `scope`/`entry_path` are UNINDEXED on the FTS5 `idx_fts` table, so deleting\n// by them forced a full-table scan (measured 67% of a production D1's 7-day\n// read budget). `idx_fts_rowid` owns a stable AUTOINCREMENT `fts_rowid` per\n// (scope, entry_path); we write/delete `idx_fts` BY THAT ROWID, which FTS5\n// resolves in O(1).\n//\n// Race-safety matters because D1 auto-commits every statement — there is no\n// enclosing transaction here (see d1-index-backend.ts). The write is two\n// atomic statements:\n// 1. `ensureFtsRowid` — create-once mapping row (ON CONFLICT DO NOTHING), so\n// concurrent writers for the same (scope, entry_path) resolve to the SAME\n// rowid; it is never reassigned.\n// 2. `INSERT OR REPLACE INTO idx_fts (rowid, …)` — last-writer-wins on that\n// rowid, so two racing writers converge to exactly ONE row: no duplicate,\n// no PRIMARY KEY conflict.\n// This deliberately avoids the DELETE + plain-INSERT + `last_insert_rowid()`\n// sequence, which is NOT atomic on D1: two racing writers would each INSERT a\n// fresh auto-rowid row → permanent duplicate idx_fts rows (the same class of\n// race already fixed for idx_embedding_vector via ON CONFLICT upsert).\n\n/** Get-or-create the stable idx_fts rowid for (scope, path) via the mapping table. */\nasync function ensureFtsRowid(db: SqliteDatabase, scope: string, path: string): Promise<number> {\n await db.run(\n sql`INSERT INTO idx_fts_rowid (scope, entry_path) VALUES (${scope}, ${path})\n ON CONFLICT(scope, entry_path) DO NOTHING`,\n );\n const rows = await db.all<{ fts_rowid: number }>(\n sql`SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path} LIMIT 1`,\n );\n return rows[0]!.fts_rowid;\n}\n\n/** Atomically insert-or-replace an entry's FTS row at its stable mapped rowid. */\nasync function writeFtsRow(\n db: SqliteDatabase,\n scope: string,\n path: string,\n summary: string,\n): Promise<void> {\n const rowid = await ensureFtsRowid(db, scope, path);\n await db.run(\n sql`INSERT OR REPLACE INTO idx_fts (rowid, scope, entry_path, summary) VALUES (${rowid}, ${scope}, ${path}, ${summary})`,\n );\n}\n\n/** Delete an entry's idx_fts row (by mapped rowid, O(1)) and its mapping row. */\nasync function deleteFtsRow(db: SqliteDatabase, scope: string, path: string): Promise<void> {\n await db.run(\n sql`DELETE FROM idx_fts WHERE rowid = (SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path})`,\n );\n await db.run(sql`DELETE FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path}`);\n}\n\n// --- Internal row types ---\n\ninterface EntryRow {\n entry_path: string;\n source_path: string | null;\n source_hash: string | null;\n domain: string;\n state: string;\n extractor_id: string | null;\n indexed_at: string;\n updated_at: string;\n anchor_count: number;\n meta: string | null;\n}\n\ninterface AnchorDataRow {\n entry_path: string;\n anchor_type: string;\n confidence: number;\n raw_anchor: string;\n}\n\nfunction sqlEscape(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n\nfunction rowToEntry(row: EntryRow): IndexedEntry {\n return {\n entryPath: row.entry_path,\n sourcePath: row.source_path ?? undefined,\n sourceHash: row.source_hash ?? undefined,\n domain: row.domain,\n state: row.state as IndexedEntry[\"state\"],\n extractorId: row.extractor_id ?? undefined,\n indexedAt: row.indexed_at,\n updatedAt: row.updated_at,\n anchorCount: row.anchor_count,\n meta: row.meta ? JSON.parse(row.meta) : undefined,\n };\n}\n\nfunction cursorScopeFromConsumerId(\n consumerId: string,\n familyConsumerId: string,\n instanceDid: string | null,\n): string {\n const prefix = `${familyConsumerId}:`;\n if (!consumerId.startsWith(prefix)) return \"\";\n const tail = consumerId.slice(prefix.length);\n if (instanceDid) {\n const suffix = `:${instanceDid}`;\n if (tail.endsWith(suffix)) return tail.slice(0, -suffix.length);\n }\n const firstSeparator = tail.indexOf(\":\");\n return firstSeparator === -1 ? tail : tail.slice(0, firstSeparator);\n}\n\n/**\n * Sanitize text for FTS5 MATCH queries.\n * Removes special FTS5 characters to prevent query syntax errors.\n *\n * NOTE: does NOT strip AND/OR/NOT. The query text reaching this backend is\n * always pre-built by `transformFtsQuery` (the single owner of FTS query\n * construction, #933), which strips *bare* boolean keywords from the raw\n * input and then re-emits `OR` only as an intentional infix operator for\n * memory's or-recall mode (#1002). Stripping it again here silently collapsed\n * or-mode back to AND — the #1002 follow-up bug that dropped memory recall for\n * plural/partial-word queries (e.g. \"architecture OR use\" → \"architecture use\",\n * which no longer matched \"…uses architecture…\").\n */\nfunction sanitizeFTS5(text: string): string {\n return text\n .replace(/[\"*^()\\-+{}:]/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Float32-typed cosine similarity. Used by `queryVector` once we've\n * decoded the stored blob into a Float32Array view — typed-array indexed\n * access is ~2× the speed of `number[]` access in V8 because the JIT\n * doesn't need to deopt-check for hole/undefined slots.\n */\nfunction cosineSimilarityF32(a: Float32Array, b: Float32Array): number {\n const len = Math.min(a.length, b.length);\n let dot = 0;\n let magA = 0;\n let magB = 0;\n for (let i = 0; i < len; i++) {\n const ai = a[i]!;\n const bi = b[i]!;\n dot += ai * bi;\n magA += ai * ai;\n magB += bi * bi;\n }\n const denom = Math.sqrt(magA) * Math.sqrt(magB);\n return denom === 0 ? 0 : dot / denom;\n}\n\n/**\n * Encode an embedding (number[]) to little-endian Float32 bytes for\n * BLOB storage. Returns a Node `Buffer` — that's the only typed-array\n * shape that drizzle's libsql adapter binds through correctly when\n * the underlying client is file-backed. Both `Uint8Array` and raw\n * `ArrayBuffer` get serialized via `toString()` somewhere in the\n * binding pipeline (yielding \"0,32,71,...\" or \"[object ArrayBuffer]\"\n * respectively), which corrupts the BLOB. The in-memory libsql client\n * doesn't exhibit this, which is why unit tests passed — file-backed\n * is the production path. `Buffer` is a `Uint8Array` subclass with the\n * correct `toString()` and is honored as a BLOB by both clients.\n *\n * `Buffer` is Node-only. CF Workers / browser builds of this provider\n * would need a different code path; we don't ship the index there\n * today.\n */\nfunction embeddingToBlob(embedding: number[]): Buffer {\n const f32 = Float32Array.from(embedding);\n return Buffer.from(f32.buffer, f32.byteOffset, f32.byteLength);\n}\n\n/**\n * Decode a stored embedding row into Float32Array. Prefers the BLOB\n * column; falls back to JSON-parsing the legacy TEXT column for rows\n * indexed before the schema migration. Different SQLite drivers hand\n * back different containers — libsql returns a raw `ArrayBuffer`,\n * better-sqlite3 returns a `Buffer`, others a `Uint8Array`. Normalize.\n */\nfunction decodeStoredEmbedding(row: {\n embedding: string | null;\n embedding_blob: ArrayBuffer | Uint8Array | Buffer | null;\n}): Float32Array {\n const blob = row.embedding_blob;\n if (blob != null) {\n if (blob instanceof ArrayBuffer) {\n return new Float32Array(blob.slice(0));\n }\n // Buffer is a Uint8Array subclass; both expose buffer/byteOffset/byteLength.\n // Copy because the underlying ArrayBuffer may be reused by the SQLite\n // driver after this row is consumed.\n const view = new Float32Array(\n blob.buffer,\n blob.byteOffset,\n blob.byteLength / Float32Array.BYTES_PER_ELEMENT,\n );\n return new Float32Array(view);\n }\n if (row.embedding != null && row.embedding !== \"\") {\n return Float32Array.from(JSON.parse(row.embedding) as number[]);\n }\n return new Float32Array(0);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Anchor, AnchorData, AnchorQuery, AnchorResult, CombinedResult, EntryFilter, FTSResult, IndexedEntry, QueryOpts, StaleFilter, VectorResult } from "../types.cjs";
|
|
2
|
+
import { KeyGeneratorRegistry } from "../keys/registry.cjs";
|
|
3
|
+
|
|
4
|
+
//#region src/backend/types.d.ts
|
|
5
|
+
interface CursorRow {
|
|
6
|
+
consumerId: string;
|
|
7
|
+
lastSeq: number;
|
|
8
|
+
watermarks?: Record<string, number>;
|
|
9
|
+
updatedAt: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One `idx_index_cursor` row as returned by {@link IndexBackend.listCursorScopes} —
|
|
13
|
+
* the durable discovery source for the CF index-follower cron (issue #935
|
|
14
|
+
* BLOCK fix #1: the cron must not rely solely on the isolate-lived
|
|
15
|
+
* `userFollowersByScope` map, which is empty on a cold isolate).
|
|
16
|
+
*
|
|
17
|
+
* `instanceDid` + `domainsJson` are captured once at registration time
|
|
18
|
+
* (`initCursor`'s `registration` param) specifically so a cold isolate can
|
|
19
|
+
* rebuild a follower's `spaceAfs` from JUST this row — no per-caller
|
|
20
|
+
* `callerDid` available to re-derive a `CloudflareDIDSpace`, and (with de-id
|
|
21
|
+
* / `scopeHmacSecret` on) `scope` is a one-way hash that can never be
|
|
22
|
+
* reversed back into a `userDid` anyway. Both are `null` for cursors written
|
|
23
|
+
* before this field existed, or by a caller that doesn't supply them (Node).
|
|
24
|
+
*/
|
|
25
|
+
interface CursorScopeRegistration {
|
|
26
|
+
scope: string;
|
|
27
|
+
consumerId: string;
|
|
28
|
+
instanceDid: string | null;
|
|
29
|
+
/** JSON-serialized `BlockletIndexDomain[]` snapshot from registration time. */
|
|
30
|
+
domainsJson: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Cursor row content/progress update time. This should not be used as the
|
|
33
|
+
* scheduler fairness clock when a backend exposes `servedAt`.
|
|
34
|
+
*/
|
|
35
|
+
updatedAt: number;
|
|
36
|
+
/**
|
|
37
|
+
* Last time the scheduler selected this cursor for service. CF D1 uses this
|
|
38
|
+
* to rotate no-op/error/aborted scopes without pretending their cursor
|
|
39
|
+
* progress changed.
|
|
40
|
+
*/
|
|
41
|
+
servedAt?: number;
|
|
42
|
+
}
|
|
43
|
+
interface IndexedDocumentWrite {
|
|
44
|
+
entry: IndexedEntry;
|
|
45
|
+
anchors?: Anchor[];
|
|
46
|
+
summary?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Options for the index-clear methods (index-follower-dirty-path Phase 4).
|
|
50
|
+
* `skipVector: true` deletes the relational / FTS / anchor rows but leaves ALL
|
|
51
|
+
* vector data UNTOUCHED — the D1-native `idx_embedding_vector` table AND an
|
|
52
|
+
* injected external backend (Upstash). The offline reset-and-rebuild reuses the
|
|
53
|
+
* follower's own remove path to clear the three cheap-to-rebuild layers while
|
|
54
|
+
* preserving the expensive embeddings (design §10.1 constraint B: reset+rebuild
|
|
55
|
+
* relational/FTS/anchor only, never re-embed). Default (unset) → full delete
|
|
56
|
+
* including vectors (the live delete/remove behavior — unchanged).
|
|
57
|
+
*/
|
|
58
|
+
interface RemoveIndexOptions {
|
|
59
|
+
skipVector?: boolean;
|
|
60
|
+
}
|
|
61
|
+
interface IndexBackend {
|
|
62
|
+
/** Initialize backend (create tables, etc.) */
|
|
63
|
+
initialize(): Promise<void>;
|
|
64
|
+
/** Close backend (release resources) */
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Replace this backend's internal `KeyGeneratorRegistry` with the AFSIndex
|
|
68
|
+
* provider's shared registry. Every backend default-constructs its own
|
|
69
|
+
* registry so it works standalone (unit tests, direct instantiation) — but
|
|
70
|
+
* when a caller injects a backend via `AFSIndexOptions.backend` (e.g. CF's
|
|
71
|
+
* `new D1IndexBackend(indexDb, scope)` in `cf-session-view.ts`), that
|
|
72
|
+
* injected instance never sees the provider's `discoverProvider()` /
|
|
73
|
+
* `register-domain` custom anchor-type registrations, because those calls
|
|
74
|
+
* mutate the *provider's* registry, not the backend's. Without this hook,
|
|
75
|
+
* custom anchor types on an injected backend silently fall back to
|
|
76
|
+
* `generateExactKeys` (JSON.stringify, field-order/case-sensitive) instead
|
|
77
|
+
* of their declared generator. `AFSIndex.ensureInitialized()` calls this
|
|
78
|
+
* unconditionally once the backend is assigned (self-constructed or
|
|
79
|
+
* injected). Optional because standalone backend use never needs it.
|
|
80
|
+
*/
|
|
81
|
+
setKeyRegistry?(registry: KeyGeneratorRegistry): void;
|
|
82
|
+
writeEntry(entry: IndexedEntry): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Atomically write the derived read-model rows for one source document when a
|
|
85
|
+
* backend can do so efficiently. D1 implements this with one native
|
|
86
|
+
* `D1Database.batch()` so the follower's hot path spends one D1 round trip
|
|
87
|
+
* for entry metadata + anchors + FTS rows. Backends without a native batch
|
|
88
|
+
* keep using the granular methods above.
|
|
89
|
+
*/
|
|
90
|
+
writeIndexedDocument?(document: IndexedDocumentWrite): Promise<void>;
|
|
91
|
+
getEntry(path: string): Promise<IndexedEntry | null>;
|
|
92
|
+
removeEntry(path: string): Promise<void>;
|
|
93
|
+
/** Backend-optimized counterpart for delete/remove changelog entries.
|
|
94
|
+
* `opts.skipVector` (Phase 4) leaves embeddings intact (offline reset). */
|
|
95
|
+
removeIndexedDocument?(path: string, opts?: RemoveIndexOptions): Promise<void>;
|
|
96
|
+
listEntries(filter: EntryFilter): Promise<IndexedEntry[]>;
|
|
97
|
+
markStale(filter: StaleFilter): Promise<number>;
|
|
98
|
+
writeAnchors(path: string, anchors: Anchor[]): Promise<void>;
|
|
99
|
+
queryAnchors(query: AnchorQuery): Promise<AnchorResult[]>;
|
|
100
|
+
removeAnchors(path: string): Promise<void>;
|
|
101
|
+
getAnchorData(path: string): Promise<AnchorData[]>;
|
|
102
|
+
countEntries(filter: EntryFilter): Promise<number>;
|
|
103
|
+
writeSummary(path: string, summary: string): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Read back the stored FTS summary for a single entry. Returns null
|
|
106
|
+
* when the entry doesn't exist or has no summary. Used by callers
|
|
107
|
+
* that need the indexed text body for downstream synthesis (e.g. a
|
|
108
|
+
* RAG agent composing an answer from retrieved docs).
|
|
109
|
+
*/
|
|
110
|
+
getSummary?(path: string): Promise<string | null>;
|
|
111
|
+
/**
|
|
112
|
+
* FTS over indexed summaries. `opts.domain` (or `opts.scope` path-prefix)
|
|
113
|
+
* isolates per-tenant data so a multi-domain SQLite doesn't leak FTS hits
|
|
114
|
+
* across blocklets — same isolation that `queryAnchors` and `queryVector`
|
|
115
|
+
* already enforce. `opts.domains` is the `boundDomains` fallback set
|
|
116
|
+
* (`domain IN (...)`) — see `AFSIndexOptions.boundDomains`; distinct from
|
|
117
|
+
* `opts.domain` (a single explicit value derived from `opts.scope`) but
|
|
118
|
+
* mutually exclusive in practice (`QueryEngine` never sets both).
|
|
119
|
+
*/
|
|
120
|
+
queryFTS(text: string, limit?: number, metaFilter?: Record<string, unknown>, opts?: {
|
|
121
|
+
domain?: string;
|
|
122
|
+
domains?: string[];
|
|
123
|
+
scope?: string;
|
|
124
|
+
pathPrefix?: string;
|
|
125
|
+
}): Promise<FTSResult[]>;
|
|
126
|
+
removeSummary(path: string): Promise<void>;
|
|
127
|
+
queryCombined?(keys: {
|
|
128
|
+
type: string;
|
|
129
|
+
values: string[];
|
|
130
|
+
}[], text?: string, opts?: QueryOpts): Promise<CombinedResult[]>;
|
|
131
|
+
readCursor?(consumerId: string): Promise<CursorRow | null>;
|
|
132
|
+
/**
|
|
133
|
+
* Write-through the follower's progress. Implementations MUST make
|
|
134
|
+
* `lastSeq` monotonic (never regress on a concurrent/out-of-order write —
|
|
135
|
+
* issue #935 BLOCK fix #2): `cron.runBatch()` and query-triggered
|
|
136
|
+
* `runCatchUp()` can race on the SAME cursor row, and the naive
|
|
137
|
+
* last-writer-wins `INSERT OR REPLACE` let a smaller `lastSeq` clobber a
|
|
138
|
+
* larger one already persisted. `watermarks`/`updatedAt` still update
|
|
139
|
+
* unconditionally on every call (only `lastSeq` is guarded).
|
|
140
|
+
*/
|
|
141
|
+
writeCursor?(consumerId: string, lastSeq: number, watermarks?: Record<string, number>): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Mark a cursor as selected by the scheduler, leaving progress fields
|
|
144
|
+
* (`lastSeq`, `watermarks`, `updatedAt`) untouched. CF cron uses this to
|
|
145
|
+
* rotate no-op/rebuild-failed scopes out of the front of the queue under very
|
|
146
|
+
* small per-tick budgets.
|
|
147
|
+
*/
|
|
148
|
+
markCursorServed?(consumerId: string): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Idempotent (INSERT-if-absent) cursor row creation. `registration` is
|
|
151
|
+
* captured ONLY on the row's initial creation (a later call for the same
|
|
152
|
+
* `(scope, consumerId)` is a no-op, matching the existing "first
|
|
153
|
+
* registration wins" semantics) — see {@link CursorScopeRegistration}.
|
|
154
|
+
*/
|
|
155
|
+
initCursor?(consumerId: string, registration?: {
|
|
156
|
+
instanceDid?: string;
|
|
157
|
+
domainsJson?: string;
|
|
158
|
+
}): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* List every scope with a persisted cursor for the `consumerId` FAMILY —
|
|
161
|
+
* i.e. every row whose `consumer_id` starts with `"{consumerId}:"` (see
|
|
162
|
+
* `deriveFollowerConsumerId` in follower.ts; real cursor rows are always
|
|
163
|
+
* scope-qualified this way as of the #936 P4 isolation rework, owner
|
|
164
|
+
* decision 2026-07-05) — see {@link CursorScopeRegistration}. D1-only: the
|
|
165
|
+
* D1 backend reads its explicit `scope` column; the SQLite backend derives
|
|
166
|
+
* the same logical scope from the scope-qualified `consumer_id`. Keeping
|
|
167
|
+
* both backends on this contract prevents runtime-specific scheduler logic
|
|
168
|
+
* from drifting.
|
|
169
|
+
*/
|
|
170
|
+
listCursorScopes?(consumerId: string): Promise<CursorScopeRegistration[]>;
|
|
171
|
+
writeEntriesBatch?(entries: IndexedEntry[]): Promise<void>;
|
|
172
|
+
writeAnchorsBatch?(items: {
|
|
173
|
+
path: string;
|
|
174
|
+
anchors: Anchor[];
|
|
175
|
+
}[]): Promise<void>;
|
|
176
|
+
writeSummariesBatch?(items: {
|
|
177
|
+
path: string;
|
|
178
|
+
summary: string;
|
|
179
|
+
}[]): Promise<void>;
|
|
180
|
+
purgeEntries?(paths: string[], opts?: RemoveIndexOptions): Promise<number>;
|
|
181
|
+
/**
|
|
182
|
+
* `domain` (the entry's composite `{name}:{scope}`) is forwarded to the
|
|
183
|
+
* vector backend's `upsert` so query-time domain filtering can isolate
|
|
184
|
+
* cross-blocklet (see `VectorBackend.upsert`). Optional for backward compat
|
|
185
|
+
* and single-domain callers; must be supplied wherever `queryVector` is
|
|
186
|
+
* called with `domains`/boundDomains (every /user query).
|
|
187
|
+
*/
|
|
188
|
+
writeEmbedding?(path: string, embedding: number[], domain?: string): Promise<void>;
|
|
189
|
+
/**
|
|
190
|
+
* Vector search with optional domain/scope filter. The default daemon
|
|
191
|
+
* hosts every per-blocklet domain in one SQLite database, so an
|
|
192
|
+
* unfiltered scan returns vectors from every tenant. Backends should
|
|
193
|
+
* filter via `domain` (preferred) or `scope` (path prefix) when set.
|
|
194
|
+
*/
|
|
195
|
+
queryVector?(embedding: number[], limit?: number, opts?: {
|
|
196
|
+
domain?: string;
|
|
197
|
+
domains?: string[];
|
|
198
|
+
scope?: string;
|
|
199
|
+
pathPrefix?: string;
|
|
200
|
+
}): Promise<VectorResult[]>;
|
|
201
|
+
removeEmbedding?(path: string): Promise<void>;
|
|
202
|
+
embeddingCapability?(): {
|
|
203
|
+
supported: boolean;
|
|
204
|
+
dimension: number;
|
|
205
|
+
} | null;
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
export { CursorRow, CursorScopeRegistration, IndexBackend, IndexedDocumentWrite, RemoveIndexOptions };
|
|
209
|
+
//# sourceMappingURL=types.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.cts","names":[],"sources":["../../src/backend/types.ts"],"mappings":"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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Anchor, AnchorData, AnchorQuery, AnchorResult, CombinedResult, EntryFilter, FTSResult, IndexedEntry, QueryOpts, StaleFilter, VectorResult } from "../types.mjs";
|
|
2
|
+
import { KeyGeneratorRegistry } from "../keys/registry.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/backend/types.d.ts
|
|
5
|
+
interface CursorRow {
|
|
6
|
+
consumerId: string;
|
|
7
|
+
lastSeq: number;
|
|
8
|
+
watermarks?: Record<string, number>;
|
|
9
|
+
updatedAt: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One `idx_index_cursor` row as returned by {@link IndexBackend.listCursorScopes} —
|
|
13
|
+
* the durable discovery source for the CF index-follower cron (issue #935
|
|
14
|
+
* BLOCK fix #1: the cron must not rely solely on the isolate-lived
|
|
15
|
+
* `userFollowersByScope` map, which is empty on a cold isolate).
|
|
16
|
+
*
|
|
17
|
+
* `instanceDid` + `domainsJson` are captured once at registration time
|
|
18
|
+
* (`initCursor`'s `registration` param) specifically so a cold isolate can
|
|
19
|
+
* rebuild a follower's `spaceAfs` from JUST this row — no per-caller
|
|
20
|
+
* `callerDid` available to re-derive a `CloudflareDIDSpace`, and (with de-id
|
|
21
|
+
* / `scopeHmacSecret` on) `scope` is a one-way hash that can never be
|
|
22
|
+
* reversed back into a `userDid` anyway. Both are `null` for cursors written
|
|
23
|
+
* before this field existed, or by a caller that doesn't supply them (Node).
|
|
24
|
+
*/
|
|
25
|
+
interface CursorScopeRegistration {
|
|
26
|
+
scope: string;
|
|
27
|
+
consumerId: string;
|
|
28
|
+
instanceDid: string | null;
|
|
29
|
+
/** JSON-serialized `BlockletIndexDomain[]` snapshot from registration time. */
|
|
30
|
+
domainsJson: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Cursor row content/progress update time. This should not be used as the
|
|
33
|
+
* scheduler fairness clock when a backend exposes `servedAt`.
|
|
34
|
+
*/
|
|
35
|
+
updatedAt: number;
|
|
36
|
+
/**
|
|
37
|
+
* Last time the scheduler selected this cursor for service. CF D1 uses this
|
|
38
|
+
* to rotate no-op/error/aborted scopes without pretending their cursor
|
|
39
|
+
* progress changed.
|
|
40
|
+
*/
|
|
41
|
+
servedAt?: number;
|
|
42
|
+
}
|
|
43
|
+
interface IndexedDocumentWrite {
|
|
44
|
+
entry: IndexedEntry;
|
|
45
|
+
anchors?: Anchor[];
|
|
46
|
+
summary?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Options for the index-clear methods (index-follower-dirty-path Phase 4).
|
|
50
|
+
* `skipVector: true` deletes the relational / FTS / anchor rows but leaves ALL
|
|
51
|
+
* vector data UNTOUCHED — the D1-native `idx_embedding_vector` table AND an
|
|
52
|
+
* injected external backend (Upstash). The offline reset-and-rebuild reuses the
|
|
53
|
+
* follower's own remove path to clear the three cheap-to-rebuild layers while
|
|
54
|
+
* preserving the expensive embeddings (design §10.1 constraint B: reset+rebuild
|
|
55
|
+
* relational/FTS/anchor only, never re-embed). Default (unset) → full delete
|
|
56
|
+
* including vectors (the live delete/remove behavior — unchanged).
|
|
57
|
+
*/
|
|
58
|
+
interface RemoveIndexOptions {
|
|
59
|
+
skipVector?: boolean;
|
|
60
|
+
}
|
|
61
|
+
interface IndexBackend {
|
|
62
|
+
/** Initialize backend (create tables, etc.) */
|
|
63
|
+
initialize(): Promise<void>;
|
|
64
|
+
/** Close backend (release resources) */
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Replace this backend's internal `KeyGeneratorRegistry` with the AFSIndex
|
|
68
|
+
* provider's shared registry. Every backend default-constructs its own
|
|
69
|
+
* registry so it works standalone (unit tests, direct instantiation) — but
|
|
70
|
+
* when a caller injects a backend via `AFSIndexOptions.backend` (e.g. CF's
|
|
71
|
+
* `new D1IndexBackend(indexDb, scope)` in `cf-session-view.ts`), that
|
|
72
|
+
* injected instance never sees the provider's `discoverProvider()` /
|
|
73
|
+
* `register-domain` custom anchor-type registrations, because those calls
|
|
74
|
+
* mutate the *provider's* registry, not the backend's. Without this hook,
|
|
75
|
+
* custom anchor types on an injected backend silently fall back to
|
|
76
|
+
* `generateExactKeys` (JSON.stringify, field-order/case-sensitive) instead
|
|
77
|
+
* of their declared generator. `AFSIndex.ensureInitialized()` calls this
|
|
78
|
+
* unconditionally once the backend is assigned (self-constructed or
|
|
79
|
+
* injected). Optional because standalone backend use never needs it.
|
|
80
|
+
*/
|
|
81
|
+
setKeyRegistry?(registry: KeyGeneratorRegistry): void;
|
|
82
|
+
writeEntry(entry: IndexedEntry): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Atomically write the derived read-model rows for one source document when a
|
|
85
|
+
* backend can do so efficiently. D1 implements this with one native
|
|
86
|
+
* `D1Database.batch()` so the follower's hot path spends one D1 round trip
|
|
87
|
+
* for entry metadata + anchors + FTS rows. Backends without a native batch
|
|
88
|
+
* keep using the granular methods above.
|
|
89
|
+
*/
|
|
90
|
+
writeIndexedDocument?(document: IndexedDocumentWrite): Promise<void>;
|
|
91
|
+
getEntry(path: string): Promise<IndexedEntry | null>;
|
|
92
|
+
removeEntry(path: string): Promise<void>;
|
|
93
|
+
/** Backend-optimized counterpart for delete/remove changelog entries.
|
|
94
|
+
* `opts.skipVector` (Phase 4) leaves embeddings intact (offline reset). */
|
|
95
|
+
removeIndexedDocument?(path: string, opts?: RemoveIndexOptions): Promise<void>;
|
|
96
|
+
listEntries(filter: EntryFilter): Promise<IndexedEntry[]>;
|
|
97
|
+
markStale(filter: StaleFilter): Promise<number>;
|
|
98
|
+
writeAnchors(path: string, anchors: Anchor[]): Promise<void>;
|
|
99
|
+
queryAnchors(query: AnchorQuery): Promise<AnchorResult[]>;
|
|
100
|
+
removeAnchors(path: string): Promise<void>;
|
|
101
|
+
getAnchorData(path: string): Promise<AnchorData[]>;
|
|
102
|
+
countEntries(filter: EntryFilter): Promise<number>;
|
|
103
|
+
writeSummary(path: string, summary: string): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Read back the stored FTS summary for a single entry. Returns null
|
|
106
|
+
* when the entry doesn't exist or has no summary. Used by callers
|
|
107
|
+
* that need the indexed text body for downstream synthesis (e.g. a
|
|
108
|
+
* RAG agent composing an answer from retrieved docs).
|
|
109
|
+
*/
|
|
110
|
+
getSummary?(path: string): Promise<string | null>;
|
|
111
|
+
/**
|
|
112
|
+
* FTS over indexed summaries. `opts.domain` (or `opts.scope` path-prefix)
|
|
113
|
+
* isolates per-tenant data so a multi-domain SQLite doesn't leak FTS hits
|
|
114
|
+
* across blocklets — same isolation that `queryAnchors` and `queryVector`
|
|
115
|
+
* already enforce. `opts.domains` is the `boundDomains` fallback set
|
|
116
|
+
* (`domain IN (...)`) — see `AFSIndexOptions.boundDomains`; distinct from
|
|
117
|
+
* `opts.domain` (a single explicit value derived from `opts.scope`) but
|
|
118
|
+
* mutually exclusive in practice (`QueryEngine` never sets both).
|
|
119
|
+
*/
|
|
120
|
+
queryFTS(text: string, limit?: number, metaFilter?: Record<string, unknown>, opts?: {
|
|
121
|
+
domain?: string;
|
|
122
|
+
domains?: string[];
|
|
123
|
+
scope?: string;
|
|
124
|
+
pathPrefix?: string;
|
|
125
|
+
}): Promise<FTSResult[]>;
|
|
126
|
+
removeSummary(path: string): Promise<void>;
|
|
127
|
+
queryCombined?(keys: {
|
|
128
|
+
type: string;
|
|
129
|
+
values: string[];
|
|
130
|
+
}[], text?: string, opts?: QueryOpts): Promise<CombinedResult[]>;
|
|
131
|
+
readCursor?(consumerId: string): Promise<CursorRow | null>;
|
|
132
|
+
/**
|
|
133
|
+
* Write-through the follower's progress. Implementations MUST make
|
|
134
|
+
* `lastSeq` monotonic (never regress on a concurrent/out-of-order write —
|
|
135
|
+
* issue #935 BLOCK fix #2): `cron.runBatch()` and query-triggered
|
|
136
|
+
* `runCatchUp()` can race on the SAME cursor row, and the naive
|
|
137
|
+
* last-writer-wins `INSERT OR REPLACE` let a smaller `lastSeq` clobber a
|
|
138
|
+
* larger one already persisted. `watermarks`/`updatedAt` still update
|
|
139
|
+
* unconditionally on every call (only `lastSeq` is guarded).
|
|
140
|
+
*/
|
|
141
|
+
writeCursor?(consumerId: string, lastSeq: number, watermarks?: Record<string, number>): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Mark a cursor as selected by the scheduler, leaving progress fields
|
|
144
|
+
* (`lastSeq`, `watermarks`, `updatedAt`) untouched. CF cron uses this to
|
|
145
|
+
* rotate no-op/rebuild-failed scopes out of the front of the queue under very
|
|
146
|
+
* small per-tick budgets.
|
|
147
|
+
*/
|
|
148
|
+
markCursorServed?(consumerId: string): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Idempotent (INSERT-if-absent) cursor row creation. `registration` is
|
|
151
|
+
* captured ONLY on the row's initial creation (a later call for the same
|
|
152
|
+
* `(scope, consumerId)` is a no-op, matching the existing "first
|
|
153
|
+
* registration wins" semantics) — see {@link CursorScopeRegistration}.
|
|
154
|
+
*/
|
|
155
|
+
initCursor?(consumerId: string, registration?: {
|
|
156
|
+
instanceDid?: string;
|
|
157
|
+
domainsJson?: string;
|
|
158
|
+
}): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* List every scope with a persisted cursor for the `consumerId` FAMILY —
|
|
161
|
+
* i.e. every row whose `consumer_id` starts with `"{consumerId}:"` (see
|
|
162
|
+
* `deriveFollowerConsumerId` in follower.ts; real cursor rows are always
|
|
163
|
+
* scope-qualified this way as of the #936 P4 isolation rework, owner
|
|
164
|
+
* decision 2026-07-05) — see {@link CursorScopeRegistration}. D1-only: the
|
|
165
|
+
* D1 backend reads its explicit `scope` column; the SQLite backend derives
|
|
166
|
+
* the same logical scope from the scope-qualified `consumer_id`. Keeping
|
|
167
|
+
* both backends on this contract prevents runtime-specific scheduler logic
|
|
168
|
+
* from drifting.
|
|
169
|
+
*/
|
|
170
|
+
listCursorScopes?(consumerId: string): Promise<CursorScopeRegistration[]>;
|
|
171
|
+
writeEntriesBatch?(entries: IndexedEntry[]): Promise<void>;
|
|
172
|
+
writeAnchorsBatch?(items: {
|
|
173
|
+
path: string;
|
|
174
|
+
anchors: Anchor[];
|
|
175
|
+
}[]): Promise<void>;
|
|
176
|
+
writeSummariesBatch?(items: {
|
|
177
|
+
path: string;
|
|
178
|
+
summary: string;
|
|
179
|
+
}[]): Promise<void>;
|
|
180
|
+
purgeEntries?(paths: string[], opts?: RemoveIndexOptions): Promise<number>;
|
|
181
|
+
/**
|
|
182
|
+
* `domain` (the entry's composite `{name}:{scope}`) is forwarded to the
|
|
183
|
+
* vector backend's `upsert` so query-time domain filtering can isolate
|
|
184
|
+
* cross-blocklet (see `VectorBackend.upsert`). Optional for backward compat
|
|
185
|
+
* and single-domain callers; must be supplied wherever `queryVector` is
|
|
186
|
+
* called with `domains`/boundDomains (every /user query).
|
|
187
|
+
*/
|
|
188
|
+
writeEmbedding?(path: string, embedding: number[], domain?: string): Promise<void>;
|
|
189
|
+
/**
|
|
190
|
+
* Vector search with optional domain/scope filter. The default daemon
|
|
191
|
+
* hosts every per-blocklet domain in one SQLite database, so an
|
|
192
|
+
* unfiltered scan returns vectors from every tenant. Backends should
|
|
193
|
+
* filter via `domain` (preferred) or `scope` (path prefix) when set.
|
|
194
|
+
*/
|
|
195
|
+
queryVector?(embedding: number[], limit?: number, opts?: {
|
|
196
|
+
domain?: string;
|
|
197
|
+
domains?: string[];
|
|
198
|
+
scope?: string;
|
|
199
|
+
pathPrefix?: string;
|
|
200
|
+
}): Promise<VectorResult[]>;
|
|
201
|
+
removeEmbedding?(path: string): Promise<void>;
|
|
202
|
+
embeddingCapability?(): {
|
|
203
|
+
supported: boolean;
|
|
204
|
+
dimension: number;
|
|
205
|
+
} | null;
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
export { CursorRow, CursorScopeRegistration, IndexBackend, IndexedDocumentWrite, RemoveIndexOptions };
|
|
209
|
+
//# sourceMappingURL=types.d.mts.map
|