@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.
Files changed (186) hide show
  1. package/LICENSE.md +26 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  4. package/dist/backend/d1-index-backend.cjs +656 -0
  5. package/dist/backend/d1-index-backend.d.cts +149 -0
  6. package/dist/backend/d1-index-backend.d.cts.map +1 -0
  7. package/dist/backend/d1-index-backend.d.mts +149 -0
  8. package/dist/backend/d1-index-backend.d.mts.map +1 -0
  9. package/dist/backend/d1-index-backend.mjs +657 -0
  10. package/dist/backend/d1-index-backend.mjs.map +1 -0
  11. package/dist/backend/d1-vector-backend.cjs +83 -0
  12. package/dist/backend/d1-vector-backend.d.cts +21 -0
  13. package/dist/backend/d1-vector-backend.d.cts.map +1 -0
  14. package/dist/backend/d1-vector-backend.d.mts +21 -0
  15. package/dist/backend/d1-vector-backend.d.mts.map +1 -0
  16. package/dist/backend/d1-vector-backend.mjs +84 -0
  17. package/dist/backend/d1-vector-backend.mjs.map +1 -0
  18. package/dist/backend/memory-backend.cjs +304 -0
  19. package/dist/backend/memory-backend.d.cts +66 -0
  20. package/dist/backend/memory-backend.d.cts.map +1 -0
  21. package/dist/backend/memory-backend.d.mts +66 -0
  22. package/dist/backend/memory-backend.d.mts.map +1 -0
  23. package/dist/backend/memory-backend.mjs +305 -0
  24. package/dist/backend/memory-backend.mjs.map +1 -0
  25. package/dist/backend/prefix-range.cjs +25 -0
  26. package/dist/backend/prefix-range.mjs +25 -0
  27. package/dist/backend/prefix-range.mjs.map +1 -0
  28. package/dist/backend/schema.cjs +187 -0
  29. package/dist/backend/schema.mjs +188 -0
  30. package/dist/backend/schema.mjs.map +1 -0
  31. package/dist/backend/sqlite-backend.cjs +889 -0
  32. package/dist/backend/sqlite-backend.d.cts +96 -0
  33. package/dist/backend/sqlite-backend.d.cts.map +1 -0
  34. package/dist/backend/sqlite-backend.d.mts +96 -0
  35. package/dist/backend/sqlite-backend.d.mts.map +1 -0
  36. package/dist/backend/sqlite-backend.mjs +890 -0
  37. package/dist/backend/sqlite-backend.mjs.map +1 -0
  38. package/dist/backend/types.d.cts +209 -0
  39. package/dist/backend/types.d.cts.map +1 -0
  40. package/dist/backend/types.d.mts +209 -0
  41. package/dist/backend/types.d.mts.map +1 -0
  42. package/dist/backend/upstash-vector-backend.cjs +98 -0
  43. package/dist/backend/upstash-vector-backend.d.cts +21 -0
  44. package/dist/backend/upstash-vector-backend.d.cts.map +1 -0
  45. package/dist/backend/upstash-vector-backend.d.mts +21 -0
  46. package/dist/backend/upstash-vector-backend.d.mts.map +1 -0
  47. package/dist/backend/upstash-vector-backend.mjs +98 -0
  48. package/dist/backend/upstash-vector-backend.mjs.map +1 -0
  49. package/dist/backend/vector-backend.d.cts +29 -0
  50. package/dist/backend/vector-backend.d.cts.map +1 -0
  51. package/dist/backend/vector-backend.d.mts +29 -0
  52. package/dist/backend/vector-backend.d.mts.map +1 -0
  53. package/dist/dirty-queue.d.cts +41 -0
  54. package/dist/dirty-queue.d.cts.map +1 -0
  55. package/dist/dirty-queue.d.mts +41 -0
  56. package/dist/dirty-queue.d.mts.map +1 -0
  57. package/dist/embed.cjs +45 -0
  58. package/dist/embed.d.cts +20 -0
  59. package/dist/embed.d.cts.map +1 -0
  60. package/dist/embed.d.mts +20 -0
  61. package/dist/embed.d.mts.map +1 -0
  62. package/dist/embed.mjs +46 -0
  63. package/dist/embed.mjs.map +1 -0
  64. package/dist/extraction/defaults.cjs +242 -0
  65. package/dist/extraction/defaults.d.cts +14 -0
  66. package/dist/extraction/defaults.d.cts.map +1 -0
  67. package/dist/extraction/defaults.d.mts +14 -0
  68. package/dist/extraction/defaults.d.mts.map +1 -0
  69. package/dist/extraction/defaults.mjs +242 -0
  70. package/dist/extraction/defaults.mjs.map +1 -0
  71. package/dist/extraction/field-map.cjs +162 -0
  72. package/dist/extraction/field-map.d.cts +70 -0
  73. package/dist/extraction/field-map.d.cts.map +1 -0
  74. package/dist/extraction/field-map.d.mts +70 -0
  75. package/dist/extraction/field-map.d.mts.map +1 -0
  76. package/dist/extraction/field-map.mjs +157 -0
  77. package/dist/extraction/field-map.mjs.map +1 -0
  78. package/dist/extraction/manager.cjs +74 -0
  79. package/dist/extraction/manager.d.cts +47 -0
  80. package/dist/extraction/manager.d.cts.map +1 -0
  81. package/dist/extraction/manager.d.mts +47 -0
  82. package/dist/extraction/manager.d.mts.map +1 -0
  83. package/dist/extraction/manager.mjs +74 -0
  84. package/dist/extraction/manager.mjs.map +1 -0
  85. package/dist/extraction/path-anchors.cjs +86 -0
  86. package/dist/extraction/path-anchors.d.cts +72 -0
  87. package/dist/extraction/path-anchors.d.cts.map +1 -0
  88. package/dist/extraction/path-anchors.d.mts +72 -0
  89. package/dist/extraction/path-anchors.d.mts.map +1 -0
  90. package/dist/extraction/path-anchors.mjs +86 -0
  91. package/dist/extraction/path-anchors.mjs.map +1 -0
  92. package/dist/follower-scheduler.cjs +92 -0
  93. package/dist/follower-scheduler.d.cts +58 -0
  94. package/dist/follower-scheduler.d.cts.map +1 -0
  95. package/dist/follower-scheduler.d.mts +58 -0
  96. package/dist/follower-scheduler.d.mts.map +1 -0
  97. package/dist/follower-scheduler.mjs +92 -0
  98. package/dist/follower-scheduler.mjs.map +1 -0
  99. package/dist/follower.cjs +577 -0
  100. package/dist/follower.d.cts +241 -0
  101. package/dist/follower.d.cts.map +1 -0
  102. package/dist/follower.d.mts +241 -0
  103. package/dist/follower.d.mts.map +1 -0
  104. package/dist/follower.mjs +571 -0
  105. package/dist/follower.mjs.map +1 -0
  106. package/dist/index-provider.cjs +1166 -0
  107. package/dist/index-provider.d.cts +175 -0
  108. package/dist/index-provider.d.cts.map +1 -0
  109. package/dist/index-provider.d.mts +175 -0
  110. package/dist/index-provider.d.mts.map +1 -0
  111. package/dist/index-provider.mjs +1167 -0
  112. package/dist/index-provider.mjs.map +1 -0
  113. package/dist/index.cjs +53 -0
  114. package/dist/index.d.cts +22 -0
  115. package/dist/index.d.mts +22 -0
  116. package/dist/index.mjs +20 -0
  117. package/dist/keys/exact.cjs +20 -0
  118. package/dist/keys/exact.mjs +20 -0
  119. package/dist/keys/exact.mjs.map +1 -0
  120. package/dist/keys/location.cjs +13 -0
  121. package/dist/keys/location.mjs +13 -0
  122. package/dist/keys/location.mjs.map +1 -0
  123. package/dist/keys/normalized.cjs +62 -0
  124. package/dist/keys/normalized.mjs +63 -0
  125. package/dist/keys/normalized.mjs.map +1 -0
  126. package/dist/keys/person.cjs +15 -0
  127. package/dist/keys/person.mjs +15 -0
  128. package/dist/keys/person.mjs.map +1 -0
  129. package/dist/keys/registry.cjs +45 -0
  130. package/dist/keys/registry.d.cts +15 -0
  131. package/dist/keys/registry.d.cts.map +1 -0
  132. package/dist/keys/registry.d.mts +15 -0
  133. package/dist/keys/registry.d.mts.map +1 -0
  134. package/dist/keys/registry.mjs +46 -0
  135. package/dist/keys/registry.mjs.map +1 -0
  136. package/dist/keys/time.cjs +128 -0
  137. package/dist/keys/time.mjs +128 -0
  138. package/dist/keys/time.mjs.map +1 -0
  139. package/dist/keys/topic.cjs +23 -0
  140. package/dist/keys/topic.mjs +23 -0
  141. package/dist/keys/topic.mjs.map +1 -0
  142. package/dist/manifest-index.cjs +29 -0
  143. package/dist/manifest-index.d.cts +24 -0
  144. package/dist/manifest-index.d.cts.map +1 -0
  145. package/dist/manifest-index.d.mts +24 -0
  146. package/dist/manifest-index.d.mts.map +1 -0
  147. package/dist/manifest-index.mjs +28 -0
  148. package/dist/manifest-index.mjs.map +1 -0
  149. package/dist/offline-rebuild.cjs +48 -0
  150. package/dist/offline-rebuild.d.cts +57 -0
  151. package/dist/offline-rebuild.d.cts.map +1 -0
  152. package/dist/offline-rebuild.d.mts +57 -0
  153. package/dist/offline-rebuild.d.mts.map +1 -0
  154. package/dist/offline-rebuild.mjs +49 -0
  155. package/dist/offline-rebuild.mjs.map +1 -0
  156. package/dist/query/engine.cjs +433 -0
  157. package/dist/query/engine.d.cts +34 -0
  158. package/dist/query/engine.d.cts.map +1 -0
  159. package/dist/query/engine.d.mts +34 -0
  160. package/dist/query/engine.d.mts.map +1 -0
  161. package/dist/query/engine.mjs +433 -0
  162. package/dist/query/engine.mjs.map +1 -0
  163. package/dist/query/limits.cjs +12 -0
  164. package/dist/query/limits.mjs +12 -0
  165. package/dist/query/limits.mjs.map +1 -0
  166. package/dist/text/cjk.cjs +30 -0
  167. package/dist/text/cjk.d.cts +24 -0
  168. package/dist/text/cjk.d.cts.map +1 -0
  169. package/dist/text/cjk.d.mts +24 -0
  170. package/dist/text/cjk.d.mts.map +1 -0
  171. package/dist/text/cjk.mjs +30 -0
  172. package/dist/text/cjk.mjs.map +1 -0
  173. package/dist/text/fts.cjs +244 -0
  174. package/dist/text/fts.mjs +245 -0
  175. package/dist/text/fts.mjs.map +1 -0
  176. package/dist/text/host.cjs +39 -0
  177. package/dist/text/host.mjs +38 -0
  178. package/dist/text/host.mjs.map +1 -0
  179. package/dist/types.cjs +44 -0
  180. package/dist/types.d.cts +393 -0
  181. package/dist/types.d.cts.map +1 -0
  182. package/dist/types.d.mts +393 -0
  183. package/dist/types.d.mts.map +1 -0
  184. package/dist/types.mjs +42 -0
  185. package/dist/types.mjs.map +1 -0
  186. package/package.json +59 -0
@@ -0,0 +1,890 @@
1
+ import { KeyGeneratorRegistry } from "../keys/registry.mjs";
2
+ import { segmentCJK } from "../text/cjk.mjs";
3
+ import { entryPathPrefixRange } from "./prefix-range.mjs";
4
+ import { createSchema } from "./schema.mjs";
5
+ import { SQLiteAFS } from "@aigne/afs-sqlite";
6
+ import { sql } from "drizzle-orm";
7
+ import { AFS } from "@aigne/afs";
8
+ import { joinURL } from "ufo";
9
+
10
+ //#region src/backend/sqlite-backend.ts
11
+ /**
12
+ * SQLiteIndexBackend — pluggable SQLite storage backend for the Index Provider.
13
+ *
14
+ * Implements the IndexBackend interface using AFSSQLite through AFS.
15
+ * - CRUD operations use built-in AFS actions (insert, query, update_where, delete_where)
16
+ * - Schema creation uses getDatabase() as a local variable (one-time DDL bootstrap)
17
+ * - FTS / count / anchor-intersection use registered custom actions via afs.exec()
18
+ * - Zero stored db handle — all runtime operations go through AFS
19
+ */
20
+ const MOUNT_NAME = "db";
21
+ var SQLiteIndexBackend = class {
22
+ afs;
23
+ sqliteProvider;
24
+ dbUrl;
25
+ keyRegistry;
26
+ ftsTokenizer;
27
+ scope;
28
+ embeddingDimension = 0;
29
+ initialized = false;
30
+ constructor(dbUrl = ":memory:", keyRegistry, opts = {}) {
31
+ this.dbUrl = dbUrl;
32
+ this.keyRegistry = keyRegistry ?? new KeyGeneratorRegistry();
33
+ this.ftsTokenizer = opts.ftsTokenizer;
34
+ this.scope = opts.scope ?? "";
35
+ }
36
+ /** See `IndexBackend.setKeyRegistry` — wires an injected backend to the provider's shared registry. */
37
+ setKeyRegistry(registry) {
38
+ this.keyRegistry = registry;
39
+ }
40
+ get mountPath() {
41
+ return joinURL("/modules", MOUNT_NAME);
42
+ }
43
+ tableAction(table, action) {
44
+ return joinURL(this.mountPath, table, ".actions", action);
45
+ }
46
+ /**
47
+ * Idempotent (design.md §4.4 addendum, owner decision 2026-07-05): a
48
+ * caller that needs the backend usable BEFORE any `AFSIndex` action runs
49
+ * (e.g. `initCursor` at follower-registration time — see daemon.ts /
50
+ * cf-session-view.ts) must call this explicitly; `AFSIndex.ensureInitialized()`
51
+ * ALSO calls it unconditionally on the first real action. Without the
52
+ * guard, that second call re-mounted a BRAND NEW `SQLiteAFS`/connection —
53
+ * for a `:memory:` db this silently discarded everything the first
54
+ * connection had written (caught by the Node/CF parity test's in-memory
55
+ * backend); for a file-backed db it "only" leaked an extra connection.
56
+ */
57
+ async initialize() {
58
+ if (this.initialized) return;
59
+ this.sqliteProvider = new SQLiteAFS({
60
+ url: this.dbUrl,
61
+ name: MOUNT_NAME,
62
+ accessMode: "readwrite"
63
+ });
64
+ this.afs = new AFS();
65
+ await this.afs.mount(this.sqliteProvider);
66
+ await createSchema(await this.sqliteProvider.getDatabase(), { ftsTokenizer: this.ftsTokenizer });
67
+ this.registerCustomActions();
68
+ this.initialized = true;
69
+ }
70
+ registerCustomActions() {
71
+ this.sqliteProvider.registerAction("anchor_intersection", async (ctx, params) => {
72
+ const keyGroups = params.key_groups;
73
+ const tenantScope = params.tenant_scope ?? "";
74
+ const scope = params.scope;
75
+ const pathPrefix = params.path_prefix;
76
+ const minConfidence = params.min_confidence;
77
+ const limit = params.limit;
78
+ const metaFilter = params.meta_filter;
79
+ const domains = params.domains;
80
+ if (!keyGroups || keyGroups.length === 0) return { rows: [] };
81
+ const cteNames = [];
82
+ const cteParts = [];
83
+ for (let i = 0; i < keyGroups.length; i++) {
84
+ const cteName = `g${i}`;
85
+ cteNames.push(cteName);
86
+ const extraConds = [];
87
+ for (const prefix of [scope, pathPrefix]) if (prefix) {
88
+ const [lo, hi] = entryPathPrefixRange(prefix);
89
+ extraConds.push(sql`entry_path >= ${lo} AND entry_path < ${hi}`);
90
+ }
91
+ if (minConfidence != null) extraConds.push(sql`confidence >= ${minConfidence}`);
92
+ const arms = keyGroups[i].values.map((v) => sql`SELECT entry_path, confidence FROM idx_anchor_key WHERE ${sql.join([
93
+ sql`scope = ${tenantScope}`,
94
+ sql`index_key = ${v}`,
95
+ ...extraConds
96
+ ], sql` AND `)}`);
97
+ cteParts.push(sql`${sql.raw(cteName)} AS (SELECT entry_path, MIN(confidence) as min_conf FROM (${sql.join(arms, sql` UNION ALL `)}) GROUP BY entry_path)`);
98
+ }
99
+ const cteSql = sql`WITH ${sql.join(cteParts, sql`, `)}`;
100
+ let selectSql;
101
+ const cteJoinParts = [];
102
+ let scoreExpr;
103
+ if (cteNames.length === 1) scoreExpr = `${cteNames[0]}.min_conf`;
104
+ else {
105
+ for (let i = 1; i < cteNames.length; i++) cteJoinParts.push(`INNER JOIN ${cteNames[i]} ON ${cteNames[0]}.entry_path = ${cteNames[i]}.entry_path`);
106
+ scoreExpr = `MIN(${cteNames.map((n) => `${n}.min_conf`).join(", ")})`;
107
+ }
108
+ const entryJoinPart = [];
109
+ let entryWhereSql = null;
110
+ if (domains && domains.length > 0 || metaFilter && Object.keys(metaFilter).length > 0) {
111
+ entryJoinPart.push(`INNER JOIN ds_indexed_entry ie ON ${cteNames[0]}.entry_path = ie.entry_path AND ie.scope = '${sqlEscape(tenantScope)}'`);
112
+ const entryConditions = [];
113
+ if (domains && domains.length > 0) {
114
+ const domainPlaceholders = domains.map((d) => sql`${d}`);
115
+ entryConditions.push(sql`ie.domain IN (${sql.join(domainPlaceholders, sql`, `)})`);
116
+ }
117
+ for (const [key, expected] of Object.entries(metaFilter ?? {})) if (Array.isArray(expected)) {
118
+ if (expected.length === 0) return { rows: [] };
119
+ const placeholders = expected.map((v) => sql`${v}`);
120
+ entryConditions.push(sql`json_extract(ie.meta, ${`$.${key}`}) IN (${sql.join(placeholders, sql`, `)})`);
121
+ } else entryConditions.push(sql`json_extract(ie.meta, ${`$.${key}`}) = ${expected}`);
122
+ entryWhereSql = sql.join(entryConditions, sql` AND `);
123
+ }
124
+ const allJoins = [...cteJoinParts, ...entryJoinPart].join(" ");
125
+ if (entryWhereSql) 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}`;
126
+ else selectSql = sql`${cteSql} SELECT ${sql.raw(cteNames[0])}.entry_path, ${sql.raw(scoreExpr)} as score FROM ${sql.raw(cteNames[0])} ${sql.raw(allJoins)}`;
127
+ const orderSql = limit != null ? sql`${selectSql} ORDER BY score DESC, ${sql.raw(cteNames[0])}.entry_path LIMIT ${limit}` : sql`${selectSql} ORDER BY score DESC, ${sql.raw(cteNames[0])}.entry_path`;
128
+ return { rows: await ctx.db.all(orderSql) };
129
+ }, {
130
+ description: "Anchor key intersection with CTE-based AND logic",
131
+ tableLevel: true
132
+ });
133
+ this.sqliteProvider.registerAction("fts_write", async (ctx, params) => {
134
+ const { scope, path, summary } = params;
135
+ await writeFtsRow(ctx.db, scope, path, segmentCJK(summary));
136
+ return { success: true };
137
+ }, {
138
+ description: "Write summary to FTS5 index (insert-or-replace by rowid, not full scan)",
139
+ tableLevel: true
140
+ });
141
+ this.sqliteProvider.registerAction("fts_query", async (ctx, params) => {
142
+ const { text, limit, meta_filter, domain, domains, path_prefix, tenant_scope } = params;
143
+ const tScope = tenant_scope ?? "";
144
+ const needsJoin = meta_filter && Object.keys(meta_filter).length > 0 || typeof domain === "string" && domain.length > 0 || Array.isArray(domains) && domains.length > 0 || typeof path_prefix === "string" && path_prefix.length > 0;
145
+ let query;
146
+ if (needsJoin) {
147
+ const conditions = [sql`idx_fts MATCH ${text}`, sql`idx_fts.scope = ${tScope}`];
148
+ if (domain) conditions.push(sql`ie.domain = ${domain}`);
149
+ if (domains && domains.length > 0) {
150
+ const domainPlaceholders = domains.map((d) => sql`${d}`);
151
+ conditions.push(sql`ie.domain IN (${sql.join(domainPlaceholders, sql`, `)})`);
152
+ }
153
+ if (path_prefix) conditions.push(sql`ie.entry_path LIKE ${`${path_prefix}%`}`);
154
+ for (const [key, expected] of Object.entries(meta_filter ?? {})) if (Array.isArray(expected)) {
155
+ if (expected.length === 0) return { rows: [] };
156
+ const placeholders = expected.map((v) => sql`${v}`);
157
+ conditions.push(sql`json_extract(ie.meta, ${`$.${key}`}) IN (${sql.join(placeholders, sql`, `)})`);
158
+ } else conditions.push(sql`json_extract(ie.meta, ${`$.${key}`}) = ${expected}`);
159
+ const where = sql.join(conditions, sql` AND `);
160
+ query = limit != null ? 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}` : 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`;
161
+ } else query = limit != null ? sql`SELECT entry_path, rank FROM idx_fts WHERE idx_fts MATCH ${text} AND scope = ${tScope} ORDER BY rank LIMIT ${limit}` : sql`SELECT entry_path, rank FROM idx_fts WHERE idx_fts MATCH ${text} AND scope = ${tScope} ORDER BY rank`;
162
+ return { rows: await ctx.db.all(query) };
163
+ }, {
164
+ description: "FTS5 search with optional domain/scope/metaFilter isolation",
165
+ tableLevel: true
166
+ });
167
+ this.sqliteProvider.registerAction("fts_delete", async (ctx, params) => {
168
+ const { scope, path } = params;
169
+ await deleteFtsRow(ctx.db, scope, path);
170
+ return { success: true };
171
+ }, {
172
+ description: "Remove entry from FTS5 index (delete by rowid, not full scan)",
173
+ tableLevel: true
174
+ });
175
+ this.sqliteProvider.registerAction("fts_get", async (ctx, params) => {
176
+ const { scope, path } = params;
177
+ return { summary: (await ctx.db.all(sql`SELECT summary FROM idx_fts WHERE scope = ${scope} AND entry_path = ${path} LIMIT 1`))[0]?.summary ?? null };
178
+ }, {
179
+ description: "Read the stored FTS summary for a single entry",
180
+ tableLevel: true
181
+ });
182
+ this.sqliteProvider.registerAction("count_entries", async (ctx, params) => {
183
+ const filter = params;
184
+ const conditions = [sql`scope = ${filter.tenant_scope ?? ""}`];
185
+ if (filter.domain) conditions.push(sql`domain = ${filter.domain}`);
186
+ if (filter.state) conditions.push(sql`state = ${filter.state}`);
187
+ if (filter.extractorId) conditions.push(sql`extractor_id = ${filter.extractorId}`);
188
+ if (filter.scope) conditions.push(sql`entry_path LIKE ${`${filter.scope}%`}`);
189
+ if (filter.boundDomains && filter.boundDomains.length > 0) {
190
+ const ps = filter.boundDomains.map((d) => sql`${d}`);
191
+ conditions.push(sql`domain IN (${sql.join(ps, sql`, `)})`);
192
+ }
193
+ const query = sql`SELECT COUNT(*) as count FROM ds_indexed_entry WHERE ${sql.join(conditions, sql` AND `)}`;
194
+ return { count: (await ctx.db.all(query))[0]?.count ?? 0 };
195
+ }, {
196
+ description: "Count indexed entries with optional filters",
197
+ tableLevel: true
198
+ });
199
+ this.sqliteProvider.registerAction("embedding_write", async (ctx, params) => {
200
+ const { scope, path, embedding } = params;
201
+ const blob = embeddingToBlob(embedding);
202
+ await ctx.db.run(sql`INSERT INTO idx_embedding_vector (scope, entry_path, embedding, embedding_blob, dimension) VALUES (${scope}, ${path}, '', ${blob}, ${embedding.length})
203
+ ON CONFLICT(scope, entry_path) DO UPDATE SET
204
+ embedding = excluded.embedding,
205
+ embedding_blob = excluded.embedding_blob,
206
+ dimension = excluded.dimension`);
207
+ return { success: true };
208
+ }, {
209
+ description: "Write embedding vector",
210
+ tableLevel: true
211
+ });
212
+ this.sqliteProvider.registerAction("embedding_query", async (ctx, params) => {
213
+ const filter = params ?? {};
214
+ const conditions = [sql`idx_embedding_vector.scope = ${filter.tenant_scope ?? ""}`];
215
+ if (filter.domain) conditions.push(sql`ds_indexed_entry.domain = ${filter.domain}`);
216
+ if (filter.domains && filter.domains.length > 0) {
217
+ const domainPlaceholders = filter.domains.map((d) => sql`${d}`);
218
+ conditions.push(sql`ds_indexed_entry.domain IN (${sql.join(domainPlaceholders, sql`, `)})`);
219
+ }
220
+ if (filter.path_prefix) conditions.push(sql`ds_indexed_entry.entry_path LIKE ${`${filter.path_prefix}%`}`);
221
+ const where = sql`WHERE ${sql.join(conditions, sql` AND `)}`;
222
+ return { rows: await ctx.db.all(sql`SELECT idx_embedding_vector.entry_path AS entry_path,
223
+ idx_embedding_vector.embedding AS embedding,
224
+ idx_embedding_vector.embedding_blob AS embedding_blob
225
+ FROM idx_embedding_vector
226
+ JOIN ds_indexed_entry
227
+ ON ds_indexed_entry.scope = idx_embedding_vector.scope
228
+ AND ds_indexed_entry.entry_path = idx_embedding_vector.entry_path
229
+ ${where}`) };
230
+ }, {
231
+ description: "Fetch embeddings (filtered by domain/scope) for vector search",
232
+ tableLevel: true
233
+ });
234
+ this.sqliteProvider.registerAction("embedding_delete", async (ctx, params) => {
235
+ const { scope, path } = params;
236
+ await ctx.db.run(sql`DELETE FROM idx_embedding_vector WHERE scope = ${scope} AND entry_path = ${path}`);
237
+ return { success: true };
238
+ }, {
239
+ description: "Remove embedding vector",
240
+ tableLevel: true
241
+ });
242
+ this.sqliteProvider.registerAction("upsert_entry", async (ctx, params) => {
243
+ const e = params;
244
+ await ctx.db.run(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})`);
245
+ return { success: true };
246
+ }, {
247
+ description: "Single entry upsert via INSERT OR REPLACE",
248
+ tableLevel: true
249
+ });
250
+ this.sqliteProvider.registerAction("replace_anchors", async (ctx, params) => {
251
+ const { scope, path, dataRows, keyRows } = params;
252
+ await ctx.db.run(sql`DELETE FROM idx_anchor_data WHERE scope = ${scope} AND entry_path = ${path}`);
253
+ await ctx.db.run(sql`DELETE FROM idx_anchor_key WHERE scope = ${scope} AND entry_path = ${path}`);
254
+ for (const row of dataRows) await ctx.db.run(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})`);
255
+ for (const row of keyRows) await ctx.db.run(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})`);
256
+ return { success: true };
257
+ }, {
258
+ description: "Replace all anchors for a single entry",
259
+ tableLevel: true
260
+ });
261
+ this.sqliteProvider.registerAction("write_indexed_document", async (ctx, params) => {
262
+ const { entry, path, dataRows, keyRows, summary } = params;
263
+ await ctx.db.run(sql.raw("BEGIN TRANSACTION"));
264
+ try {
265
+ await ctx.db.run(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})`);
266
+ await ctx.db.run(sql`DELETE FROM idx_anchor_data WHERE scope = ${entry.scope} AND entry_path = ${path}`);
267
+ await ctx.db.run(sql`DELETE FROM idx_anchor_key WHERE scope = ${entry.scope} AND entry_path = ${path}`);
268
+ for (const row of dataRows) await ctx.db.run(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})`);
269
+ for (const row of keyRows) await ctx.db.run(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})`);
270
+ if (summary) await writeFtsRow(ctx.db, entry.scope, path, segmentCJK(summary));
271
+ await ctx.db.run(sql.raw("COMMIT"));
272
+ return { success: true };
273
+ } catch (error) {
274
+ await ctx.db.run(sql.raw("ROLLBACK"));
275
+ throw error;
276
+ }
277
+ }, {
278
+ description: "Write entry metadata, anchors, and FTS summary in one transaction",
279
+ tableLevel: true
280
+ });
281
+ this.sqliteProvider.registerAction("batch_purge", async (ctx, params) => {
282
+ const { scope, paths, skipVector } = params;
283
+ await ctx.db.run(sql.raw("BEGIN TRANSACTION"));
284
+ try {
285
+ for (const path of paths) {
286
+ await ctx.db.run(sql`DELETE FROM idx_anchor_data WHERE scope = ${scope} AND entry_path = ${path}`);
287
+ await ctx.db.run(sql`DELETE FROM idx_anchor_key WHERE scope = ${scope} AND entry_path = ${path}`);
288
+ await deleteFtsRow(ctx.db, scope, path);
289
+ if (skipVector !== true) await ctx.db.run(sql`DELETE FROM idx_embedding_vector WHERE scope = ${scope} AND entry_path = ${path}`);
290
+ await ctx.db.run(sql`DELETE FROM ds_indexed_entry WHERE scope = ${scope} AND entry_path = ${path}`);
291
+ }
292
+ await ctx.db.run(sql.raw("COMMIT"));
293
+ return { count: paths.length };
294
+ } catch (error) {
295
+ await ctx.db.run(sql.raw("ROLLBACK"));
296
+ throw error;
297
+ }
298
+ }, {
299
+ description: "Purge entries and all associated data in a single transaction",
300
+ tableLevel: true
301
+ });
302
+ this.sqliteProvider.registerAction("batch_upsert_entries", async (ctx, params) => {
303
+ const entries = params.entries;
304
+ await ctx.db.run(sql.raw("BEGIN TRANSACTION"));
305
+ try {
306
+ for (const e of entries) await ctx.db.run(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})`);
307
+ await ctx.db.run(sql.raw("COMMIT"));
308
+ return { count: entries.length };
309
+ } catch (error) {
310
+ await ctx.db.run(sql.raw("ROLLBACK"));
311
+ throw error;
312
+ }
313
+ }, {
314
+ description: "Batch upsert entries in a single transaction",
315
+ tableLevel: true
316
+ });
317
+ this.sqliteProvider.registerAction("batch_replace_anchors", async (ctx, params) => {
318
+ const { scope, items } = params;
319
+ await ctx.db.run(sql.raw("BEGIN TRANSACTION"));
320
+ try {
321
+ for (const item of items) {
322
+ await ctx.db.run(sql`DELETE FROM idx_anchor_data WHERE scope = ${scope} AND entry_path = ${item.path}`);
323
+ await ctx.db.run(sql`DELETE FROM idx_anchor_key WHERE scope = ${scope} AND entry_path = ${item.path}`);
324
+ for (const row of item.dataRows) await ctx.db.run(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})`);
325
+ for (const row of item.keyRows) await ctx.db.run(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})`);
326
+ }
327
+ await ctx.db.run(sql.raw("COMMIT"));
328
+ return { count: items.length };
329
+ } catch (error) {
330
+ await ctx.db.run(sql.raw("ROLLBACK"));
331
+ throw error;
332
+ }
333
+ }, {
334
+ description: "Batch replace anchors in a single transaction",
335
+ tableLevel: true
336
+ });
337
+ this.sqliteProvider.registerAction("batch_write_fts", async (ctx, params) => {
338
+ const { scope, items } = params;
339
+ await ctx.db.run(sql.raw("BEGIN TRANSACTION"));
340
+ try {
341
+ for (const item of items) await writeFtsRow(ctx.db, scope, item.path, item.summary);
342
+ await ctx.db.run(sql.raw("COMMIT"));
343
+ return { count: items.length };
344
+ } catch (error) {
345
+ await ctx.db.run(sql.raw("ROLLBACK"));
346
+ throw error;
347
+ }
348
+ }, {
349
+ description: "Batch write FTS summaries in a single transaction",
350
+ tableLevel: true
351
+ });
352
+ this.sqliteProvider.registerAction("cursor_read", async (ctx, params) => {
353
+ const { consumer_id } = params;
354
+ const rows = await ctx.db.all(sql`SELECT consumer_id, last_seq, watermarks, updated_at FROM idx_index_cursor WHERE consumer_id = ${consumer_id} LIMIT 1`);
355
+ if (rows.length === 0) return { cursor: null };
356
+ const row = rows[0];
357
+ return { cursor: {
358
+ consumerId: row.consumer_id,
359
+ lastSeq: row.last_seq,
360
+ watermarks: row.watermarks ? JSON.parse(row.watermarks) : void 0,
361
+ updatedAt: row.updated_at
362
+ } };
363
+ }, {
364
+ description: "Read changelog follower cursor",
365
+ tableLevel: true
366
+ });
367
+ this.sqliteProvider.registerAction("cursor_write", async (ctx, params) => {
368
+ const { consumer_id, last_seq, watermarks } = params;
369
+ const now = Date.now();
370
+ await ctx.db.run(sql`INSERT INTO idx_index_cursor (consumer_id, last_seq, watermarks, updated_at, served_at)
371
+ VALUES (${consumer_id}, ${last_seq}, ${watermarks ? JSON.stringify(watermarks) : null}, ${now}, ${now})
372
+ ON CONFLICT(consumer_id) DO UPDATE SET
373
+ last_seq = MAX(last_seq, excluded.last_seq),
374
+ watermarks = excluded.watermarks,
375
+ updated_at = excluded.updated_at,
376
+ served_at = excluded.served_at`);
377
+ return { success: true };
378
+ }, {
379
+ description: "Write index follower cursor (monotonic last_seq; dead column post-cutover)",
380
+ tableLevel: true
381
+ });
382
+ this.sqliteProvider.registerAction("cursor_mark_served", async (ctx, params) => {
383
+ const { consumer_id } = params;
384
+ await ctx.db.run(sql`UPDATE idx_index_cursor SET served_at = ${Date.now()} WHERE consumer_id = ${consumer_id}`);
385
+ return { success: true };
386
+ }, {
387
+ description: "Mark changelog follower cursor as served",
388
+ tableLevel: true
389
+ });
390
+ this.sqliteProvider.registerAction("cursor_init", async (ctx, params) => {
391
+ const { consumer_id, instance_did, domains } = params;
392
+ const now = Date.now();
393
+ await ctx.db.run(sql`INSERT OR IGNORE INTO idx_index_cursor (consumer_id, last_seq, watermarks, updated_at, served_at, instance_did, domains)
394
+ VALUES (${consumer_id}, 0, NULL, ${now}, ${now}, ${instance_did ?? null}, ${domains ?? null})`);
395
+ return { success: true };
396
+ }, {
397
+ description: "Initialize changelog follower cursor (idempotent)",
398
+ tableLevel: true
399
+ });
400
+ this.sqliteProvider.registerAction("cursor_scope_list", async (ctx, params) => {
401
+ const { family_consumer_id } = params;
402
+ return { registrations: (await ctx.db.all(sql`SELECT consumer_id, instance_did, domains, updated_at, served_at
403
+ FROM idx_index_cursor
404
+ WHERE consumer_id LIKE ${`${family_consumer_id}:%`}`)).map((r) => ({
405
+ scope: cursorScopeFromConsumerId(r.consumer_id, family_consumer_id, r.instance_did),
406
+ consumerId: r.consumer_id,
407
+ instanceDid: r.instance_did,
408
+ domainsJson: r.domains,
409
+ updatedAt: r.updated_at,
410
+ servedAt: r.served_at ?? r.updated_at
411
+ })) };
412
+ }, {
413
+ description: "List changelog follower cursor registrations",
414
+ tableLevel: true
415
+ });
416
+ }
417
+ async readCursor(consumerId) {
418
+ return (await this.afs.exec(this.tableAction("idx_index_cursor", "cursor_read"), { consumer_id: consumerId })).data.cursor;
419
+ }
420
+ async writeCursor(consumerId, lastSeq, watermarks) {
421
+ await this.afs.exec(this.tableAction("idx_index_cursor", "cursor_write"), {
422
+ consumer_id: consumerId,
423
+ last_seq: lastSeq,
424
+ watermarks: watermarks ?? null
425
+ });
426
+ }
427
+ async markCursorServed(consumerId) {
428
+ await this.afs.exec(this.tableAction("idx_index_cursor", "cursor_mark_served"), { consumer_id: consumerId });
429
+ }
430
+ async initCursor(consumerId, registration) {
431
+ await this.afs.exec(this.tableAction("idx_index_cursor", "cursor_init"), {
432
+ consumer_id: consumerId,
433
+ instance_did: registration?.instanceDid ?? null,
434
+ domains: registration?.domainsJson ?? null
435
+ });
436
+ }
437
+ async listCursorScopes(consumerId) {
438
+ return (await this.afs.exec(this.tableAction("idx_index_cursor", "cursor_scope_list"), { family_consumer_id: consumerId })).data.registrations;
439
+ }
440
+ async close() {
441
+ this.afs = void 0;
442
+ this.sqliteProvider = void 0;
443
+ }
444
+ async writeEntry(entry) {
445
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "upsert_entry"), this.entryToRow(entry));
446
+ }
447
+ async writeIndexedDocument(document) {
448
+ const path = document.entry.entryPath;
449
+ const anchors = document.anchors ?? [];
450
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "write_indexed_document"), {
451
+ entry: this.entryToRow(document.entry),
452
+ path,
453
+ dataRows: anchors.map((anchor) => ({
454
+ entry_path: path,
455
+ anchor_type: anchor.type,
456
+ confidence: anchor.confidence,
457
+ raw_anchor: JSON.stringify(anchor)
458
+ })),
459
+ keyRows: anchors.flatMap((anchor) => this.keyRegistry.generateKeys(anchor).map((key) => ({
460
+ index_key: key,
461
+ entry_path: path,
462
+ anchor_type: anchor.type,
463
+ confidence: anchor.confidence
464
+ }))),
465
+ summary: document.summary ?? null
466
+ });
467
+ }
468
+ entryToRow(entry) {
469
+ const data = {
470
+ scope: this.scope,
471
+ entry_path: entry.entryPath,
472
+ domain: entry.domain,
473
+ state: entry.state,
474
+ indexed_at: entry.indexedAt,
475
+ updated_at: entry.updatedAt,
476
+ anchor_count: entry.anchorCount
477
+ };
478
+ if (entry.sourcePath != null) data.source_path = entry.sourcePath;
479
+ if (entry.sourceHash != null) data.source_hash = entry.sourceHash;
480
+ if (entry.extractorId != null) data.extractor_id = entry.extractorId;
481
+ if (entry.meta != null) data.meta = JSON.stringify(entry.meta);
482
+ return data;
483
+ }
484
+ async getEntry(path) {
485
+ const data = (await this.afs.exec(this.tableAction("ds_indexed_entry", "query"), {
486
+ where: {
487
+ scope: this.scope,
488
+ entry_path: path
489
+ },
490
+ limit: 1
491
+ })).data;
492
+ if (!data.rows || data.rows.length === 0) return null;
493
+ return rowToEntry(data.rows[0]);
494
+ }
495
+ async removeEntry(path) {
496
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "delete_where"), { where: {
497
+ scope: this.scope,
498
+ entry_path: path
499
+ } });
500
+ }
501
+ async removeIndexedDocument(path, opts) {
502
+ await this.purgeEntries([path], opts);
503
+ }
504
+ async listEntries(filter) {
505
+ const where = { scope: this.scope };
506
+ if (filter.domain) where.domain = filter.domain;
507
+ if (filter.state) where.state = filter.state;
508
+ if (filter.extractorId) where.extractor_id = filter.extractorId;
509
+ if (filter.scope) where.entry_path = { $like: `${filter.scope}%` };
510
+ if (filter.boundDomains && filter.boundDomains.length > 0) where.domain = { $in: filter.boundDomains };
511
+ return ((await this.afs.exec(this.tableAction("ds_indexed_entry", "query"), { where })).data.rows ?? []).map(rowToEntry);
512
+ }
513
+ async markStale(filter) {
514
+ const entryWhere = {
515
+ scope: this.scope,
516
+ state: "active"
517
+ };
518
+ if (filter.extractorId) entryWhere.extractor_id = filter.extractorId;
519
+ if (filter.domain) entryWhere.domain = filter.domain;
520
+ if (filter.scope) entryWhere.entry_path = { $like: `${filter.scope}%` };
521
+ if (filter.boundDomains && filter.boundDomains.length > 0) entryWhere.domain = { $in: filter.boundDomains };
522
+ if (filter.anchorType) {
523
+ const anchorData = (await this.afs.exec(this.tableAction("idx_anchor_data", "query"), { where: {
524
+ scope: this.scope,
525
+ anchor_type: filter.anchorType
526
+ } })).data;
527
+ const paths = [...new Set(anchorData.rows.map((r) => r.entry_path))];
528
+ if (paths.length === 0) return 0;
529
+ if (typeof entryWhere.entry_path === "object") entryWhere.entry_path = {
530
+ ...entryWhere.entry_path,
531
+ $in: paths
532
+ };
533
+ else entryWhere.entry_path = { $in: paths };
534
+ }
535
+ return (await this.afs.exec(this.tableAction("ds_indexed_entry", "update_where"), {
536
+ where: entryWhere,
537
+ data: { state: "stale" }
538
+ })).data.affectedRows;
539
+ }
540
+ async countEntries(filter) {
541
+ return (await this.afs.exec(this.tableAction("ds_indexed_entry", "count_entries"), {
542
+ tenant_scope: this.scope,
543
+ domain: filter.domain,
544
+ state: filter.state,
545
+ extractorId: filter.extractorId,
546
+ scope: filter.scope,
547
+ boundDomains: filter.boundDomains
548
+ })).data.count;
549
+ }
550
+ async writeAnchors(path, anchors) {
551
+ const dataRows = anchors.map((anchor) => ({
552
+ entry_path: path,
553
+ anchor_type: anchor.type,
554
+ confidence: anchor.confidence,
555
+ raw_anchor: JSON.stringify(anchor)
556
+ }));
557
+ const keyRows = anchors.flatMap((anchor) => this.keyRegistry.generateKeys(anchor).map((key) => ({
558
+ index_key: key,
559
+ entry_path: path,
560
+ anchor_type: anchor.type,
561
+ confidence: anchor.confidence
562
+ })));
563
+ await this.afs.exec(this.tableAction("idx_anchor_data", "replace_anchors"), {
564
+ scope: this.scope,
565
+ path,
566
+ dataRows,
567
+ keyRows
568
+ });
569
+ }
570
+ async queryAnchors(query) {
571
+ if (query.keys.length === 0) return [];
572
+ const params = {
573
+ key_groups: query.keys.map((kg) => {
574
+ const values = /* @__PURE__ */ new Set();
575
+ for (const raw of kg.values) {
576
+ const synthetic = {
577
+ type: kg.type ?? "exact",
578
+ values: [raw],
579
+ confidence: 1
580
+ };
581
+ const generated = this.keyRegistry.generateKeys(synthetic);
582
+ for (const k of generated) values.add(k);
583
+ values.add(raw);
584
+ }
585
+ return { values: [...values] };
586
+ }),
587
+ tenant_scope: this.scope,
588
+ scope: query.scope,
589
+ min_confidence: query.minConfidence,
590
+ limit: query.limit
591
+ };
592
+ if (query.pathPrefix) params.path_prefix = query.pathPrefix;
593
+ if (query.metaFilter && Object.keys(query.metaFilter).length > 0) params.meta_filter = query.metaFilter;
594
+ if (query.domains && query.domains.length > 0) params.domains = query.domains;
595
+ return ((await this.afs.exec(this.tableAction("idx_anchor_key", "anchor_intersection"), params)).data.rows ?? []).map((r) => ({
596
+ entryPath: r.entry_path,
597
+ score: r.score
598
+ }));
599
+ }
600
+ async removeAnchors(path) {
601
+ await this.afs.exec(this.tableAction("idx_anchor_data", "delete_where"), { where: {
602
+ scope: this.scope,
603
+ entry_path: path
604
+ } });
605
+ await this.afs.exec(this.tableAction("idx_anchor_key", "delete_where"), { where: {
606
+ scope: this.scope,
607
+ entry_path: path
608
+ } });
609
+ }
610
+ async getAnchorData(path) {
611
+ return ((await this.afs.exec(this.tableAction("idx_anchor_data", "query"), { where: {
612
+ scope: this.scope,
613
+ entry_path: path
614
+ } })).data.rows ?? []).map((r) => ({
615
+ entryPath: r.entry_path,
616
+ anchorType: r.anchor_type,
617
+ confidence: r.confidence,
618
+ rawAnchor: r.raw_anchor
619
+ }));
620
+ }
621
+ async writeSummary(path, summary) {
622
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "fts_write"), {
623
+ scope: this.scope,
624
+ path,
625
+ summary
626
+ });
627
+ }
628
+ async getSummary(path) {
629
+ return (await this.afs.exec(this.tableAction("ds_indexed_entry", "fts_get"), {
630
+ scope: this.scope,
631
+ path
632
+ })).data.summary ?? null;
633
+ }
634
+ async queryFTS(text, limit, metaFilter, opts) {
635
+ try {
636
+ const sanitized = sanitizeFTS5(text);
637
+ if (!sanitized) return [];
638
+ const params = {
639
+ text: sanitized,
640
+ limit,
641
+ tenant_scope: this.scope
642
+ };
643
+ if (metaFilter && Object.keys(metaFilter).length > 0) params.meta_filter = metaFilter;
644
+ if (opts?.domain) params.domain = opts.domain;
645
+ if (opts?.domains && opts.domains.length > 0) params.domains = opts.domains;
646
+ if (opts?.scope) params.scope = opts.scope;
647
+ if (opts?.pathPrefix) params.path_prefix = opts.pathPrefix;
648
+ return ((await this.afs.exec(this.tableAction("ds_indexed_entry", "fts_query"), params)).data.rows ?? []).map((r) => ({
649
+ entryPath: r.entry_path,
650
+ rank: -r.rank
651
+ }));
652
+ } catch {
653
+ return [];
654
+ }
655
+ }
656
+ async removeSummary(path) {
657
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "fts_delete"), {
658
+ scope: this.scope,
659
+ path
660
+ });
661
+ }
662
+ async writeEmbedding(path, embedding) {
663
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "embedding_write"), {
664
+ scope: this.scope,
665
+ path,
666
+ embedding
667
+ });
668
+ if (this.embeddingDimension === 0 && embedding.length > 0) this.embeddingDimension = embedding.length;
669
+ }
670
+ async queryVector(embedding, limit, opts) {
671
+ const rows = (await this.afs.exec(this.tableAction("ds_indexed_entry", "embedding_query"), {
672
+ tenant_scope: this.scope,
673
+ ...opts?.domain ? { domain: opts.domain } : {},
674
+ ...opts?.domains && opts.domains.length > 0 ? { domains: opts.domains } : {},
675
+ ...opts?.scope ? { scope: opts.scope } : {},
676
+ ...opts?.pathPrefix ? { path_prefix: opts.pathPrefix } : {}
677
+ })).data.rows ?? [];
678
+ const queryVec = Float32Array.from(embedding);
679
+ const results = rows.map((r) => ({
680
+ entryPath: r.entry_path,
681
+ similarity: cosineSimilarityF32(queryVec, decodeStoredEmbedding(r))
682
+ }));
683
+ results.sort((a, b) => b.similarity - a.similarity);
684
+ if (limit != null) return results.slice(0, limit);
685
+ return results;
686
+ }
687
+ async removeEmbedding(path) {
688
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "embedding_delete"), {
689
+ scope: this.scope,
690
+ path
691
+ });
692
+ }
693
+ embeddingCapability() {
694
+ return {
695
+ supported: true,
696
+ dimension: this.embeddingDimension
697
+ };
698
+ }
699
+ async writeEntriesBatch(entries) {
700
+ if (entries.length === 0) return;
701
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "batch_upsert_entries"), { entries: entries.map((e) => this.entryToRow(e)) });
702
+ }
703
+ async writeAnchorsBatch(items) {
704
+ if (items.length === 0) return;
705
+ const batchItems = items.map((item) => ({
706
+ path: item.path,
707
+ dataRows: item.anchors.map((anchor) => ({
708
+ entry_path: item.path,
709
+ anchor_type: anchor.type,
710
+ confidence: anchor.confidence,
711
+ raw_anchor: JSON.stringify(anchor)
712
+ })),
713
+ keyRows: item.anchors.flatMap((anchor) => this.keyRegistry.generateKeys(anchor).map((key) => ({
714
+ index_key: key,
715
+ entry_path: item.path,
716
+ anchor_type: anchor.type,
717
+ confidence: anchor.confidence
718
+ })))
719
+ }));
720
+ await this.afs.exec(this.tableAction("idx_anchor_data", "batch_replace_anchors"), {
721
+ scope: this.scope,
722
+ items: batchItems
723
+ });
724
+ }
725
+ async writeSummariesBatch(items) {
726
+ if (items.length === 0) return;
727
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "batch_write_fts"), {
728
+ scope: this.scope,
729
+ items
730
+ });
731
+ }
732
+ async purgeEntries(paths, opts) {
733
+ if (paths.length === 0) return 0;
734
+ await this.afs.exec(this.tableAction("ds_indexed_entry", "batch_purge"), {
735
+ scope: this.scope,
736
+ paths,
737
+ skipVector: opts?.skipVector === true
738
+ });
739
+ return paths.length;
740
+ }
741
+ async queryCombined(keys, text, opts) {
742
+ const anchorResults = keys.length > 0 ? await this.queryAnchors({
743
+ keys,
744
+ scope: opts?.scope,
745
+ minConfidence: opts?.minConfidence,
746
+ limit: opts?.limit
747
+ }) : [];
748
+ const anchorPaths = new Set(anchorResults.map((r) => r.entryPath));
749
+ const ftsResults = text ? await this.queryFTS(text, opts?.limit) : [];
750
+ const results = anchorResults.map((r) => {
751
+ const fts = ftsResults.find((f) => f.entryPath === r.entryPath);
752
+ return {
753
+ entryPath: r.entryPath,
754
+ anchorScore: r.score,
755
+ ftsRank: fts?.rank,
756
+ score: r.score + (fts?.rank ?? 0)
757
+ };
758
+ });
759
+ for (const f of ftsResults) if (!anchorPaths.has(f.entryPath)) results.push({
760
+ entryPath: f.entryPath,
761
+ ftsRank: f.rank,
762
+ score: f.rank
763
+ });
764
+ results.sort((a, b) => b.score - a.score);
765
+ if (opts?.limit != null) return results.slice(0, opts.limit);
766
+ return results;
767
+ }
768
+ };
769
+ /** Get-or-create the stable idx_fts rowid for (scope, path) via the mapping table. */
770
+ async function ensureFtsRowid(db, scope, path) {
771
+ await db.run(sql`INSERT INTO idx_fts_rowid (scope, entry_path) VALUES (${scope}, ${path})
772
+ ON CONFLICT(scope, entry_path) DO NOTHING`);
773
+ return (await db.all(sql`SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path} LIMIT 1`))[0].fts_rowid;
774
+ }
775
+ /** Atomically insert-or-replace an entry's FTS row at its stable mapped rowid. */
776
+ async function writeFtsRow(db, scope, path, summary) {
777
+ const rowid = await ensureFtsRowid(db, scope, path);
778
+ await db.run(sql`INSERT OR REPLACE INTO idx_fts (rowid, scope, entry_path, summary) VALUES (${rowid}, ${scope}, ${path}, ${summary})`);
779
+ }
780
+ /** Delete an entry's idx_fts row (by mapped rowid, O(1)) and its mapping row. */
781
+ async function deleteFtsRow(db, scope, path) {
782
+ await db.run(sql`DELETE FROM idx_fts WHERE rowid = (SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path})`);
783
+ await db.run(sql`DELETE FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path}`);
784
+ }
785
+ function sqlEscape(value) {
786
+ return value.replace(/'/g, "''");
787
+ }
788
+ function rowToEntry(row) {
789
+ return {
790
+ entryPath: row.entry_path,
791
+ sourcePath: row.source_path ?? void 0,
792
+ sourceHash: row.source_hash ?? void 0,
793
+ domain: row.domain,
794
+ state: row.state,
795
+ extractorId: row.extractor_id ?? void 0,
796
+ indexedAt: row.indexed_at,
797
+ updatedAt: row.updated_at,
798
+ anchorCount: row.anchor_count,
799
+ meta: row.meta ? JSON.parse(row.meta) : void 0
800
+ };
801
+ }
802
+ function cursorScopeFromConsumerId(consumerId, familyConsumerId, instanceDid) {
803
+ const prefix = `${familyConsumerId}:`;
804
+ if (!consumerId.startsWith(prefix)) return "";
805
+ const tail = consumerId.slice(prefix.length);
806
+ if (instanceDid) {
807
+ const suffix = `:${instanceDid}`;
808
+ if (tail.endsWith(suffix)) return tail.slice(0, -suffix.length);
809
+ }
810
+ const firstSeparator = tail.indexOf(":");
811
+ return firstSeparator === -1 ? tail : tail.slice(0, firstSeparator);
812
+ }
813
+ /**
814
+ * Sanitize text for FTS5 MATCH queries.
815
+ * Removes special FTS5 characters to prevent query syntax errors.
816
+ *
817
+ * NOTE: does NOT strip AND/OR/NOT. The query text reaching this backend is
818
+ * always pre-built by `transformFtsQuery` (the single owner of FTS query
819
+ * construction, #933), which strips *bare* boolean keywords from the raw
820
+ * input and then re-emits `OR` only as an intentional infix operator for
821
+ * memory's or-recall mode (#1002). Stripping it again here silently collapsed
822
+ * or-mode back to AND — the #1002 follow-up bug that dropped memory recall for
823
+ * plural/partial-word queries (e.g. "architecture OR use" → "architecture use",
824
+ * which no longer matched "…uses architecture…").
825
+ */
826
+ function sanitizeFTS5(text) {
827
+ return text.replace(/["*^()\-+{}:]/g, " ").replace(/\s+/g, " ").trim();
828
+ }
829
+ /**
830
+ * Float32-typed cosine similarity. Used by `queryVector` once we've
831
+ * decoded the stored blob into a Float32Array view — typed-array indexed
832
+ * access is ~2× the speed of `number[]` access in V8 because the JIT
833
+ * doesn't need to deopt-check for hole/undefined slots.
834
+ */
835
+ function cosineSimilarityF32(a, b) {
836
+ const len = Math.min(a.length, b.length);
837
+ let dot = 0;
838
+ let magA = 0;
839
+ let magB = 0;
840
+ for (let i = 0; i < len; i++) {
841
+ const ai = a[i];
842
+ const bi = b[i];
843
+ dot += ai * bi;
844
+ magA += ai * ai;
845
+ magB += bi * bi;
846
+ }
847
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
848
+ return denom === 0 ? 0 : dot / denom;
849
+ }
850
+ /**
851
+ * Encode an embedding (number[]) to little-endian Float32 bytes for
852
+ * BLOB storage. Returns a Node `Buffer` — that's the only typed-array
853
+ * shape that drizzle's libsql adapter binds through correctly when
854
+ * the underlying client is file-backed. Both `Uint8Array` and raw
855
+ * `ArrayBuffer` get serialized via `toString()` somewhere in the
856
+ * binding pipeline (yielding "0,32,71,..." or "[object ArrayBuffer]"
857
+ * respectively), which corrupts the BLOB. The in-memory libsql client
858
+ * doesn't exhibit this, which is why unit tests passed — file-backed
859
+ * is the production path. `Buffer` is a `Uint8Array` subclass with the
860
+ * correct `toString()` and is honored as a BLOB by both clients.
861
+ *
862
+ * `Buffer` is Node-only. CF Workers / browser builds of this provider
863
+ * would need a different code path; we don't ship the index there
864
+ * today.
865
+ */
866
+ function embeddingToBlob(embedding) {
867
+ const f32 = Float32Array.from(embedding);
868
+ return Buffer.from(f32.buffer, f32.byteOffset, f32.byteLength);
869
+ }
870
+ /**
871
+ * Decode a stored embedding row into Float32Array. Prefers the BLOB
872
+ * column; falls back to JSON-parsing the legacy TEXT column for rows
873
+ * indexed before the schema migration. Different SQLite drivers hand
874
+ * back different containers — libsql returns a raw `ArrayBuffer`,
875
+ * better-sqlite3 returns a `Buffer`, others a `Uint8Array`. Normalize.
876
+ */
877
+ function decodeStoredEmbedding(row) {
878
+ const blob = row.embedding_blob;
879
+ if (blob != null) {
880
+ if (blob instanceof ArrayBuffer) return new Float32Array(blob.slice(0));
881
+ const view = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / Float32Array.BYTES_PER_ELEMENT);
882
+ return new Float32Array(view);
883
+ }
884
+ if (row.embedding != null && row.embedding !== "") return Float32Array.from(JSON.parse(row.embedding));
885
+ return new Float32Array(0);
886
+ }
887
+
888
+ //#endregion
889
+ export { SQLiteIndexBackend };
890
+ //# sourceMappingURL=sqlite-backend.mjs.map