@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,1167 @@
1
+ import { generateTimeKeys } from "./keys/time.mjs";
2
+ import { KeyGeneratorRegistry } from "./keys/registry.mjs";
3
+ import { SQLiteIndexBackend } from "./backend/sqlite-backend.mjs";
4
+ import { createEmbedder } from "./embed.mjs";
5
+ import { extractFromMeta } from "./extraction/defaults.mjs";
6
+ import { ExtractorManager } from "./extraction/manager.mjs";
7
+ import { createNormalizedValueGenerator } from "./keys/normalized.mjs";
8
+ import { QueryEngine } from "./query/engine.mjs";
9
+ import { DEFAULT_SEARCH_MODE, SEARCH_MODES } from "./types.mjs";
10
+ import { __decorate } from "./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs";
11
+ import { explainZodError, makeNsLog } from "@aigne/afs";
12
+ import { joinURL } from "ufo";
13
+ import { AFSBaseProvider, Actions, Explain, List, Meta, Read, Search } from "@aigne/afs/provider";
14
+ import { z } from "zod/v4";
15
+
16
+ //#region src/index-provider.ts
17
+ const log = makeNsLog("provider:index");
18
+ const IndexArgsSchema = z.object({
19
+ entryPath: z.string(),
20
+ domain: z.string().optional(),
21
+ anchors: z.array(z.object({
22
+ type: z.string(),
23
+ confidence: z.number()
24
+ }).passthrough()).optional(),
25
+ summary: z.string().optional(),
26
+ embeddingSummary: z.string().optional(),
27
+ embedding: z.array(z.number()).optional(),
28
+ sourcePath: z.string().optional(),
29
+ sourceHash: z.string().optional(),
30
+ extractorId: z.string().optional(),
31
+ meta: z.record(z.string(), z.unknown()).optional()
32
+ });
33
+ const QueryArgsSchema = z.object({
34
+ mode: z.enum(SEARCH_MODES).optional(),
35
+ ftsMode: z.enum(["and", "or"]).optional(),
36
+ anchors: z.array(z.object({
37
+ type: z.string(),
38
+ values: z.array(z.string())
39
+ })).optional(),
40
+ text: z.string().optional(),
41
+ embeddingText: z.string().optional(),
42
+ embedding: z.array(z.number()).optional(),
43
+ rewriter: z.enum(["none", "multi-query"]).optional(),
44
+ opts: z.object({
45
+ scope: z.string().optional(),
46
+ pathPrefix: z.string().optional(),
47
+ limit: z.number().optional(),
48
+ cursor: z.string().optional(),
49
+ minResults: z.number().optional(),
50
+ minConfidence: z.number().optional(),
51
+ metaFilter: z.record(z.string(), z.unknown()).optional(),
52
+ embeddingFloor: z.number().optional(),
53
+ anchorFilter: z.boolean().optional(),
54
+ explain: z.boolean().optional()
55
+ }).optional()
56
+ });
57
+ const ReindexArgsSchema = z.object({
58
+ scope: z.string().optional(),
59
+ extractorId: z.string().optional(),
60
+ domain: z.string().optional()
61
+ });
62
+ const RegisterDomainArgsSchema = z.object({
63
+ name: z.string(),
64
+ types: z.array(z.object({ name: z.string() }).passthrough()).optional()
65
+ });
66
+ const GetArgsSchema = z.object({
67
+ entryPath: z.string().optional(),
68
+ entryPaths: z.array(z.string()).optional()
69
+ });
70
+ const CleanupArgsSchema = z.object({
71
+ state: z.enum(["stale", "removed"]).optional().default("stale"),
72
+ domain: z.string().optional(),
73
+ scope: z.string().optional()
74
+ });
75
+ const VerifyArgsSchema = z.object({
76
+ domain: z.string().optional(),
77
+ scope: z.string().optional()
78
+ });
79
+ const DiscoverArgsSchema = z.object({
80
+ providerPath: z.string(),
81
+ domain: z.string().optional()
82
+ });
83
+ const afsIndexOptionsSchema = z.object({
84
+ dbPath: z.string().optional(),
85
+ defaultDomain: z.string().optional()
86
+ });
87
+ var AFSIndex = class extends AFSBaseProvider {
88
+ name;
89
+ description;
90
+ accessMode = "readwrite";
91
+ options;
92
+ keyRegistry = new KeyGeneratorRegistry();
93
+ backend;
94
+ queryEngine;
95
+ domains = /* @__PURE__ */ new Map();
96
+ extractors = new ExtractorManager();
97
+ unsubscribes = [];
98
+ afsRoot;
99
+ initialized = false;
100
+ follower;
101
+ catchUpInflight;
102
+ static schema() {
103
+ return afsIndexOptionsSchema;
104
+ }
105
+ static manifest() {
106
+ return {
107
+ name: "index",
108
+ description: "Full-text search and anchor-based indexing for AFS entries.\n- FTS5 full-text search on summaries\n- Structured anchor indexing (time, person, topic, location)\n- Combined query modes: anchor, fts, semantic\n- Actions: index, query, reindex, cleanup",
109
+ uriTemplate: "index://{dbPath+}",
110
+ category: "composite",
111
+ schema: z.object({
112
+ dbPath: z.string(),
113
+ defaultDomain: z.string().optional()
114
+ }),
115
+ tags: [
116
+ "index",
117
+ "search",
118
+ "fts5",
119
+ "sqlite",
120
+ "anchor"
121
+ ],
122
+ capabilityTags: [
123
+ "read-write",
124
+ "search",
125
+ "auth:none",
126
+ "local"
127
+ ],
128
+ security: {
129
+ riskLevel: "local",
130
+ resourceAccess: ["local-filesystem"],
131
+ dataSensitivity: [],
132
+ notes: ["Stores SQLite index database on local filesystem"]
133
+ }
134
+ };
135
+ }
136
+ constructor(options = {}) {
137
+ super();
138
+ this.options = options;
139
+ this.name = options.name ?? "index";
140
+ this.description = options.description ?? "AFS Index Provider — unified structured indexing";
141
+ if (options.timeKeyGranularity?.length) {
142
+ const include = new Set(options.timeKeyGranularity);
143
+ this.keyRegistry.register("time", (anchor) => generateTimeKeys(anchor, include));
144
+ }
145
+ }
146
+ async ensureInitialized() {
147
+ if (this.initialized) return;
148
+ this.backend = this.options.backend ?? new SQLiteIndexBackend(this.options.dbPath ?? ":memory:", this.keyRegistry, { ftsTokenizer: this.options.ftsTokenizer });
149
+ this.backend.setKeyRegistry?.(this.keyRegistry);
150
+ await this.backend.initialize();
151
+ this.queryEngine = new QueryEngine(this.backend);
152
+ this.initialized = true;
153
+ }
154
+ onMount(root, _mountPath) {
155
+ this.afsRoot = root;
156
+ const unsubWrite = root.subscribe?.({ type: "afs:write" }, (event) => {
157
+ this.handleWriteEvent(event.path);
158
+ });
159
+ if (unsubWrite) this.unsubscribes.push(unsubWrite);
160
+ const unsubDelete = root.subscribe?.({ type: "afs:delete" }, (event) => {
161
+ this.handleDeleteEvent(event.path);
162
+ });
163
+ if (unsubDelete) this.unsubscribes.push(unsubDelete);
164
+ const unsubMount = root.subscribe?.({ type: "afs:mount" }, (event) => {
165
+ if (event.source === this.name) return;
166
+ this.handleMountEvent(event.path);
167
+ });
168
+ if (unsubMount) this.unsubscribes.push(unsubMount);
169
+ }
170
+ async handleMountEvent(providerPath) {
171
+ try {
172
+ if (!this.afsRoot) return;
173
+ const domainName = providerPath.split("/").filter(Boolean).pop() ?? "unknown";
174
+ const result = await this.discoverProvider(providerPath, domainName);
175
+ if (result.anchorTypes.length === 0 && !result.config && !result.extractor) this.domains.delete(domainName);
176
+ } catch {}
177
+ }
178
+ async handleDeleteEvent(path) {
179
+ try {
180
+ await this.ensureInitialized();
181
+ if (!await this.backend.getEntry(path)) return;
182
+ await this.backend.removeAnchors(path);
183
+ await this.backend.removeSummary(path);
184
+ if (this.backend.removeEmbedding) await this.backend.removeEmbedding(path);
185
+ await this.backend.removeEntry(path);
186
+ } catch {}
187
+ }
188
+ async handleWriteEvent(path) {
189
+ try {
190
+ if (!this.afsRoot) return;
191
+ await this.ensureInitialized();
192
+ let domain;
193
+ let result;
194
+ const extracted = this.extractors.hasRegistrations ? await this.extractors.extract(path, this.afsRoot) : null;
195
+ if (extracted) {
196
+ domain = extracted.domain;
197
+ result = extracted.result;
198
+ } else {
199
+ const readResult = await this.afsRoot.read?.(path);
200
+ if (!readResult?.data?.meta) return;
201
+ const entry = readResult.data;
202
+ const defaultResult = extractFromMeta(path, entry.meta);
203
+ if (!defaultResult) return;
204
+ domain = this.options.defaultDomain ?? "default";
205
+ result = defaultResult;
206
+ }
207
+ if (!this.shouldIndex(path, domain)) return;
208
+ const now = (/* @__PURE__ */ new Date()).toISOString();
209
+ await this.backend.writeEntry({
210
+ entryPath: path,
211
+ sourcePath: path,
212
+ domain,
213
+ state: "active",
214
+ indexedAt: now,
215
+ updatedAt: now,
216
+ anchorCount: result.anchors.length
217
+ });
218
+ if (result.anchors.length > 0) await this.backend.writeAnchors(path, result.anchors);
219
+ if (result.summary) await this.backend.writeSummary(path, result.summary);
220
+ if (this.backend.writeEmbedding) {
221
+ const embedding = result.embedding ?? await this.autoEmbed(result.summary);
222
+ if (embedding) await this.backend.writeEmbedding(path, embedding);
223
+ }
224
+ } catch {}
225
+ }
226
+ async close() {
227
+ for (const unsub of this.unsubscribes) unsub();
228
+ this.unsubscribes.length = 0;
229
+ this.extractors.clear();
230
+ if (this.initialized) {
231
+ await this.backend.close();
232
+ this.initialized = false;
233
+ }
234
+ }
235
+ async readRoot(ctx) {
236
+ return this.buildEntry(ctx.path, {
237
+ content: this.description,
238
+ meta: {
239
+ kind: "afs:directory",
240
+ childrenCount: 3
241
+ }
242
+ });
243
+ }
244
+ async listRoot(ctx) {
245
+ return { data: [
246
+ this.buildEntry(joinURL(ctx.path, "domains"), { meta: {
247
+ kind: "afs:directory",
248
+ childrenCount: this.domains.size
249
+ } }),
250
+ this.buildEntry(joinURL(ctx.path, "stats"), { meta: {
251
+ kind: "afs:directory",
252
+ childrenCount: 3
253
+ } }),
254
+ this.buildEntry(joinURL(ctx.path, "config"), { meta: {
255
+ kind: "afs:directory",
256
+ childrenCount: 3
257
+ } })
258
+ ] };
259
+ }
260
+ async listActions(ctx) {
261
+ const actions = [
262
+ "index",
263
+ "query",
264
+ "get",
265
+ "reindex",
266
+ "cleanup",
267
+ "verify",
268
+ "register-domain",
269
+ "discover"
270
+ ];
271
+ const READ_ACTIONS = new Set([
272
+ "query",
273
+ "get",
274
+ "discover",
275
+ "verify"
276
+ ]);
277
+ return { data: actions.map((name) => this.buildEntry(joinURL(ctx.path, name), {
278
+ id: name,
279
+ meta: {
280
+ kind: "afs:executable",
281
+ effect: READ_ACTIONS.has(name) ? "read" : "write"
282
+ }
283
+ })) };
284
+ }
285
+ readDomains(ctx) {
286
+ return this.buildEntry(ctx.path, { meta: {
287
+ kind: "afs:directory",
288
+ childrenCount: this.domains.size
289
+ } });
290
+ }
291
+ readStatsDir(ctx) {
292
+ return this.buildEntry(ctx.path, { meta: {
293
+ kind: "afs:directory",
294
+ childrenCount: 3
295
+ } });
296
+ }
297
+ readConfigDir(ctx) {
298
+ return this.buildEntry(ctx.path, { meta: {
299
+ kind: "afs:directory",
300
+ childrenCount: 3
301
+ } });
302
+ }
303
+ async listDomains(ctx) {
304
+ return { data: Array.from(this.domains.values()).map((d) => this.buildEntry(joinURL(ctx.path, d.name), {
305
+ id: d.name,
306
+ content: {
307
+ types: d.types.map((t) => t.name),
308
+ indexable: d.config?.indexable
309
+ }
310
+ })) };
311
+ }
312
+ async listStatsDir(ctx) {
313
+ return { data: [
314
+ "total-entries",
315
+ "active-entries",
316
+ "stale-entries"
317
+ ].map((key) => this.buildEntry(joinURL(ctx.path, key), {
318
+ id: key,
319
+ meta: {
320
+ kind: "afs:value",
321
+ childrenCount: 0
322
+ }
323
+ })) };
324
+ }
325
+ listConfig(ctx) {
326
+ return { data: [
327
+ "llm-provider",
328
+ "embedding-enabled",
329
+ "default-domain"
330
+ ].map((key) => this.buildEntry(joinURL(ctx.path, key), {
331
+ id: key,
332
+ meta: {
333
+ kind: "afs:value",
334
+ childrenCount: 0
335
+ }
336
+ })) };
337
+ }
338
+ listStatsKey() {
339
+ return { data: [] };
340
+ }
341
+ listConfigKey() {
342
+ return { data: [] };
343
+ }
344
+ async listDomainName(ctx) {
345
+ await this.ensureInitialized();
346
+ return { data: (await this.backend.listEntries({ domain: ctx.params.name })).map((e) => this.buildEntry(joinURL(ctx.path, encodeURIComponent(e.entryPath)), {
347
+ id: e.entryPath,
348
+ content: {
349
+ entryPath: e.entryPath,
350
+ sourcePath: e.sourcePath,
351
+ state: e.state,
352
+ anchorCount: e.anchorCount,
353
+ indexedAt: e.indexedAt,
354
+ updatedAt: e.updatedAt
355
+ },
356
+ meta: {
357
+ kind: "index:entry",
358
+ domain: e.domain,
359
+ state: e.state,
360
+ anchorCount: e.anchorCount,
361
+ childrenCount: 0,
362
+ ...e.meta ?? {}
363
+ },
364
+ createdAt: new Date(e.indexedAt),
365
+ updatedAt: new Date(e.updatedAt)
366
+ })) };
367
+ }
368
+ readRootMeta(ctx) {
369
+ const meta = {
370
+ kind: "afs:directory",
371
+ childrenCount: 3
372
+ };
373
+ return this.buildEntry(ctx.path, {
374
+ content: meta,
375
+ meta
376
+ });
377
+ }
378
+ readDomainsMeta(ctx) {
379
+ const meta = {
380
+ kind: "afs:directory",
381
+ childrenCount: this.domains.size
382
+ };
383
+ return this.buildEntry(ctx.path, {
384
+ content: meta,
385
+ meta
386
+ });
387
+ }
388
+ readStatsMeta(ctx) {
389
+ const meta = {
390
+ kind: "afs:directory",
391
+ childrenCount: 3
392
+ };
393
+ return this.buildEntry(ctx.path, {
394
+ content: meta,
395
+ meta
396
+ });
397
+ }
398
+ readConfigMeta(ctx) {
399
+ const meta = {
400
+ kind: "afs:directory",
401
+ childrenCount: 3
402
+ };
403
+ return this.buildEntry(ctx.path, {
404
+ content: meta,
405
+ meta
406
+ });
407
+ }
408
+ readStatsKeyMeta(ctx) {
409
+ const meta = {
410
+ kind: "afs:value",
411
+ childrenCount: 0
412
+ };
413
+ return this.buildEntry(ctx.path, {
414
+ content: meta,
415
+ meta
416
+ });
417
+ }
418
+ readConfigKeyMeta(ctx) {
419
+ const meta = {
420
+ kind: "afs:value",
421
+ childrenCount: 0
422
+ };
423
+ return this.buildEntry(ctx.path, {
424
+ content: meta,
425
+ meta
426
+ });
427
+ }
428
+ async readDomainNameMeta(ctx) {
429
+ await this.ensureInitialized();
430
+ const meta = {
431
+ kind: "afs:directory",
432
+ childrenCount: await this.backend.countEntries({ domain: ctx.params.name })
433
+ };
434
+ return this.buildEntry(ctx.path, {
435
+ content: meta,
436
+ meta
437
+ });
438
+ }
439
+ readCapabilities(ctx) {
440
+ return this.buildEntry(ctx.path, { content: { operations: this.getOperationsDeclaration() } });
441
+ }
442
+ explainRoot(_ctx) {
443
+ return {
444
+ content: `AFS Index Provider — unified structured indexing.\n\nCombines anchor indexing, full-text search, and optional embedding search. Domains: ${this.domains.size} registered.`,
445
+ format: "text"
446
+ };
447
+ }
448
+ explainPath(ctx) {
449
+ return {
450
+ content: `Index provider path: /${ctx.params.path}`,
451
+ format: "text"
452
+ };
453
+ }
454
+ async searchHandler(_ctx, query, options) {
455
+ await this.ensureInitialized();
456
+ try {
457
+ return { data: (await this.queryEngine.query({
458
+ mode: "fts",
459
+ text: query,
460
+ opts: { limit: options?.limit }
461
+ })).map((r) => this.buildEntry(r.entryPath, { content: {
462
+ matchType: r.matchType,
463
+ score: r.score
464
+ } })) };
465
+ } catch {
466
+ return { data: [] };
467
+ }
468
+ }
469
+ async readConfig(ctx) {
470
+ switch (ctx.params.key) {
471
+ case "llm-provider": return this.buildEntry(ctx.path, { content: { value: this.options.llmProvider ?? null } });
472
+ case "embedding-enabled": return this.buildEntry(ctx.path, { content: { value: this.options.embeddingEnabled ?? false } });
473
+ case "default-domain": return this.buildEntry(ctx.path, { content: { value: this.options.defaultDomain ?? "default" } });
474
+ default: return;
475
+ }
476
+ }
477
+ async readStats(ctx) {
478
+ await this.ensureInitialized();
479
+ switch (ctx.params.key) {
480
+ case "total-entries": {
481
+ const count = await this.backend.countEntries({});
482
+ return this.buildEntry(ctx.path, { content: { value: count } });
483
+ }
484
+ case "active-entries": {
485
+ const count = await this.backend.countEntries({ state: "active" });
486
+ return this.buildEntry(ctx.path, { content: { value: count } });
487
+ }
488
+ case "stale-entries": {
489
+ const count = await this.backend.countEntries({ state: "stale" });
490
+ return this.buildEntry(ctx.path, { content: { value: count } });
491
+ }
492
+ default: return;
493
+ }
494
+ }
495
+ async execIndex(_ctx, args) {
496
+ const parsed = IndexArgsSchema.safeParse(args);
497
+ if (!parsed.success) return {
498
+ success: false,
499
+ error: {
500
+ code: "VALIDATION_ERROR",
501
+ message: explainZodError(parsed.error, IndexArgsSchema)
502
+ }
503
+ };
504
+ await this.ensureInitialized();
505
+ const { entryPath, anchors: rawAnchors, summary, embeddingSummary, embedding: explicitEmbedding, sourcePath, sourceHash, extractorId, meta } = parsed.data;
506
+ const domain = parsed.data.domain ?? this.options.defaultDomain ?? "default";
507
+ const anchors = rawAnchors ?? [];
508
+ const now = (/* @__PURE__ */ new Date()).toISOString();
509
+ const entry = {
510
+ entryPath,
511
+ sourcePath,
512
+ sourceHash,
513
+ domain,
514
+ state: "active",
515
+ extractorId,
516
+ indexedAt: now,
517
+ updatedAt: now,
518
+ anchorCount: anchors.length,
519
+ meta
520
+ };
521
+ await this.backend.writeEntry(entry);
522
+ if (anchors.length > 0) await this.backend.writeAnchors(entryPath, anchors);
523
+ if (summary) await this.backend.writeSummary(entryPath, summary);
524
+ if (this.backend.writeEmbedding) {
525
+ const embedding = explicitEmbedding ?? await this.autoEmbed(embeddingSummary ?? summary);
526
+ if (embedding) await this.backend.writeEmbedding(entryPath, embedding);
527
+ }
528
+ return {
529
+ success: true,
530
+ data: {
531
+ entryPath,
532
+ anchorCount: anchors.length
533
+ }
534
+ };
535
+ }
536
+ async execQuery(_ctx, args) {
537
+ const misplaced = [
538
+ "limit",
539
+ "cursor",
540
+ "anchorFilter",
541
+ "explain",
542
+ "scope",
543
+ "pathPrefix",
544
+ "minConfidence",
545
+ "metaFilter",
546
+ "embeddingFloor",
547
+ "minResults"
548
+ ].filter((k) => k in args);
549
+ if (misplaced.length > 0) return {
550
+ success: false,
551
+ error: {
552
+ code: "VALIDATION_ERROR",
553
+ message: `Parameter(s) ${misplaced.map((k) => `"${k}"`).join(", ")} must be nested inside "opts" (e.g. { opts: { ${misplaced[0]}: ... } })`
554
+ }
555
+ };
556
+ const parsed = QueryArgsSchema.safeParse(args);
557
+ if (!parsed.success) return {
558
+ success: false,
559
+ error: {
560
+ code: "VALIDATION_ERROR",
561
+ message: explainZodError(parsed.error, QueryArgsSchema)
562
+ }
563
+ };
564
+ await this.ensureInitialized();
565
+ if (this.follower && !parsed.data.opts?.cursor) {
566
+ const { task, started } = this.startCatchUpOnce();
567
+ const schedule = this.options.scheduleBackground;
568
+ if (schedule) {
569
+ if (started) schedule(task);
570
+ } else await task;
571
+ }
572
+ const { anchors, text, embeddingText, opts } = parsed.data;
573
+ const mode = parsed.data.mode ?? this.options.defaultSearchMode ?? DEFAULT_SEARCH_MODE;
574
+ const ftsMode = parsed.data.ftsMode;
575
+ const rewriter = parsed.data.rewriter ?? "none";
576
+ if (rewriter !== "none" && opts?.cursor) return {
577
+ success: false,
578
+ error: {
579
+ code: "VALIDATION_ERROR",
580
+ message: "Query cursor is not supported with rewriter mode"
581
+ }
582
+ };
583
+ if (rewriter === "multi-query" && !parsed.data.embedding && (text || embeddingText) && this.resolveChatProvider()) {
584
+ const original = embeddingText ?? text;
585
+ return {
586
+ success: true,
587
+ data: { results: await this.queryWithMultiQuery({
588
+ original,
589
+ ftsText: text,
590
+ mode,
591
+ anchors,
592
+ opts
593
+ }) }
594
+ };
595
+ }
596
+ const embedding = parsed.data.embedding ?? (mode === "semantic" ? await this.autoEmbed(embeddingText ?? text, "query") : null);
597
+ let page;
598
+ try {
599
+ page = await this.queryEngine.queryPage({
600
+ mode,
601
+ ftsMode,
602
+ anchors,
603
+ text,
604
+ embedding: embedding ?? void 0,
605
+ opts: this.options.embeddingFloor != null && opts?.embeddingFloor == null ? {
606
+ ...opts,
607
+ embeddingFloor: this.options.embeddingFloor
608
+ } : opts,
609
+ boundDomains: this.options.boundDomains
610
+ });
611
+ } catch (err) {
612
+ if (err instanceof Error && err.message === "Invalid or stale query cursor") return {
613
+ success: false,
614
+ error: {
615
+ code: "VALIDATION_ERROR",
616
+ message: err.message
617
+ }
618
+ };
619
+ throw err;
620
+ }
621
+ const { results } = page;
622
+ if (opts?.explain) {
623
+ const explanation = await this.buildExplanation({
624
+ mode,
625
+ text,
626
+ embeddingText,
627
+ anchors,
628
+ opts,
629
+ embeddingUsed: embedding !== null && embedding !== void 0,
630
+ results
631
+ });
632
+ log.debug("query explain", {
633
+ ...explanation,
634
+ results: results.map((r) => ({
635
+ path: r.entryPath,
636
+ matchType: r.matchType,
637
+ score: r.score,
638
+ explanation: r.explanation
639
+ }))
640
+ });
641
+ return {
642
+ success: true,
643
+ data: {
644
+ results,
645
+ explanation,
646
+ ...page.meta ? { meta: page.meta } : {}
647
+ }
648
+ };
649
+ }
650
+ return {
651
+ success: true,
652
+ data: {
653
+ results,
654
+ ...page.meta ? { meta: page.meta } : {}
655
+ }
656
+ };
657
+ }
658
+ /**
659
+ * Build a "why did the query return what it returned" diagnostic.
660
+ *
661
+ * Cheap probes only: scope cardinality (countEntries), result-type
662
+ * breakdown (already in the result list), echo of the parsed inputs.
663
+ * Does not re-run the query — purely additive per planning doc §P4.
664
+ */
665
+ async buildExplanation(args) {
666
+ const { mode, text, embeddingText, anchors, opts, embeddingUsed, results } = args;
667
+ const scopeFilter = opts?.scope;
668
+ const domain = scopeFilter ? scopeFilter.replace(/^\/+/, "") : void 0;
669
+ let totalIndexedInScope;
670
+ try {
671
+ totalIndexedInScope = await this.backend.countEntries({
672
+ ...domain ? { domain } : {},
673
+ ...scopeFilter ? { scope: scopeFilter } : {},
674
+ state: "active"
675
+ });
676
+ } catch {
677
+ totalIndexedInScope = void 0;
678
+ }
679
+ const matchTypeBreakdown = {};
680
+ for (const r of results) matchTypeBreakdown[r.matchType] = (matchTypeBreakdown[r.matchType] ?? 0) + 1;
681
+ let whyEmpty;
682
+ if (results.length === 0) if (totalIndexedInScope === 0) whyEmpty = `No entries indexed in scope "${scopeFilter ?? "(no scope)"}" — likely a domain mismatch or missing ingestion.`;
683
+ else if (mode === "fts" && !text) whyEmpty = "FTS mode requested but `text` was empty.";
684
+ else if (mode === "anchor" && (!anchors || anchors.length === 0)) whyEmpty = "Anchor mode requested but `anchors` was empty.";
685
+ else if (mode === "semantic" && !embeddingUsed) whyEmpty = "Semantic mode requested but no embedding was generated (no `text`/`embeddingText` and no auto-embed available).";
686
+ else whyEmpty = `Index has ${totalIndexedInScope ?? "?"} entries in scope but none matched query.`;
687
+ return {
688
+ mode,
689
+ ftsQuery: text,
690
+ embeddingText: embeddingText ?? text,
691
+ embeddingUsed,
692
+ anchors,
693
+ scope: scopeFilter,
694
+ metaFilter: opts?.metaFilter,
695
+ totalIndexedInScope,
696
+ resultCount: results.length,
697
+ matchTypeBreakdown,
698
+ whyEmpty
699
+ };
700
+ }
701
+ async execGet(_ctx, args) {
702
+ const parsed = GetArgsSchema.safeParse(args);
703
+ if (!parsed.success) return {
704
+ success: false,
705
+ error: {
706
+ code: "VALIDATION_ERROR",
707
+ message: explainZodError(parsed.error, GetArgsSchema)
708
+ }
709
+ };
710
+ await this.ensureInitialized();
711
+ const paths = parsed.data.entryPaths ?? (parsed.data.entryPath ? [parsed.data.entryPath] : []);
712
+ if (paths.length === 0) return {
713
+ success: false,
714
+ error: {
715
+ code: "VALIDATION_ERROR",
716
+ message: "entryPath or entryPaths required"
717
+ }
718
+ };
719
+ const bound = this.options.boundDomains;
720
+ const inBounds = (entry) => !entry || !bound || bound.length === 0 || bound.includes(entry.domain);
721
+ const entries = [];
722
+ for (const p of paths) {
723
+ const entry = await this.backend.getEntry(p);
724
+ if (!inBounds(entry)) {
725
+ entries.push({
726
+ entryPath: p,
727
+ entry: null,
728
+ summary: null
729
+ });
730
+ continue;
731
+ }
732
+ const summary = this.backend.getSummary ? await this.backend.getSummary(p) : null;
733
+ entries.push({
734
+ entryPath: p,
735
+ entry,
736
+ summary
737
+ });
738
+ }
739
+ return {
740
+ success: true,
741
+ data: { entries }
742
+ };
743
+ }
744
+ async execReindex(_ctx, args) {
745
+ const parsed = ReindexArgsSchema.safeParse(args);
746
+ if (!parsed.success) return {
747
+ success: false,
748
+ error: {
749
+ code: "VALIDATION_ERROR",
750
+ message: explainZodError(parsed.error, ReindexArgsSchema)
751
+ }
752
+ };
753
+ await this.ensureInitialized();
754
+ const { scope, extractorId, domain } = parsed.data;
755
+ return {
756
+ success: true,
757
+ data: { staleCount: await this.backend.markStale({
758
+ scope,
759
+ extractorId,
760
+ domain,
761
+ boundDomains: this.options.boundDomains
762
+ }) }
763
+ };
764
+ }
765
+ async execCleanup(_ctx, args) {
766
+ const parsed = CleanupArgsSchema.safeParse(args);
767
+ if (!parsed.success) return {
768
+ success: false,
769
+ error: {
770
+ code: "VALIDATION_ERROR",
771
+ message: explainZodError(parsed.error, CleanupArgsSchema)
772
+ }
773
+ };
774
+ await this.ensureInitialized();
775
+ const { state, domain, scope } = parsed.data;
776
+ const paths = (await this.backend.listEntries({
777
+ state,
778
+ domain,
779
+ scope,
780
+ boundDomains: this.options.boundDomains
781
+ })).map((e) => e.entryPath);
782
+ let purgedCount;
783
+ if (this.backend.purgeEntries) purgedCount = await this.backend.purgeEntries(paths);
784
+ else {
785
+ purgedCount = 0;
786
+ for (const path of paths) {
787
+ await this.backend.removeAnchors(path);
788
+ await this.backend.removeSummary(path);
789
+ if (this.backend.removeEmbedding) await this.backend.removeEmbedding(path);
790
+ await this.backend.removeEntry(path);
791
+ purgedCount++;
792
+ }
793
+ }
794
+ return {
795
+ success: true,
796
+ data: { purgedCount }
797
+ };
798
+ }
799
+ /**
800
+ * `verify` — index self-stats for drift detection.
801
+ *
802
+ * Returns the index's own view of what it holds: total entries, breakdown
803
+ * by domain and state, and how many entries have zero anchors (a common
804
+ * symptom of a half-finished or corrupted index). A caller compares these
805
+ * against the source-of-truth provider (e.g. memory's `health.totalEntries`)
806
+ * to decide whether to trigger a rebuild — without guessing from empty
807
+ * query results.
808
+ */
809
+ async execVerify(_ctx, args) {
810
+ const parsed = VerifyArgsSchema.safeParse(args);
811
+ if (!parsed.success) return {
812
+ success: false,
813
+ error: {
814
+ code: "VALIDATION_ERROR",
815
+ message: explainZodError(parsed.error, VerifyArgsSchema)
816
+ }
817
+ };
818
+ await this.ensureInitialized();
819
+ const { domain, scope } = parsed.data;
820
+ const entries = await this.backend.listEntries({
821
+ ...domain ? { domain } : {},
822
+ ...scope ? { scope } : {},
823
+ boundDomains: this.options.boundDomains
824
+ });
825
+ const byDomain = {};
826
+ const byState = {};
827
+ let anchorlessCount = 0;
828
+ for (const e of entries) {
829
+ byDomain[e.domain] = (byDomain[e.domain] ?? 0) + 1;
830
+ byState[e.state] = (byState[e.state] ?? 0) + 1;
831
+ if (!e.anchorCount) anchorlessCount++;
832
+ }
833
+ return {
834
+ success: true,
835
+ data: {
836
+ totalEntries: entries.length,
837
+ byDomain,
838
+ byState,
839
+ anchorlessCount,
840
+ ...domain ? { domain } : {},
841
+ ...scope ? { scope } : {}
842
+ }
843
+ };
844
+ }
845
+ /**
846
+ * Register one or more domains from a manifest projection, wiring each
847
+ * declared anchor type into the key registry (normalizedValue by default).
848
+ *
849
+ * This is the non-exec entry point for session-view wiring: called at index
850
+ * handle creation time so domains survive Worker restarts (the in-memory
851
+ * handles are rebuilt from the manifest on every cold start).
852
+ */
853
+ registerDomains(domains) {
854
+ for (const domain of domains) {
855
+ this.domains.set(domain.name, {
856
+ name: domain.name,
857
+ ...domain.scope ? { scope: domain.scope } : {},
858
+ types: domain.anchorTypes,
859
+ ...domain.config ? { config: domain.config } : {}
860
+ });
861
+ this.registerAnchorTypeKeyGenerators(domain.anchorTypes);
862
+ }
863
+ }
864
+ /**
865
+ * Attach a changelog follower so execQuery triggers a lightweight catch-up
866
+ * before returning results. Called once at index handle creation time by the
867
+ * CF session-view (cf-session-view.ts) after building the follower.
868
+ */
869
+ setFollower(follower) {
870
+ this.follower = follower;
871
+ follower.setEmbedder((text) => this.autoEmbed(text, "passage"));
872
+ }
873
+ startCatchUpOnce() {
874
+ if (this.catchUpInflight) return {
875
+ task: this.catchUpInflight,
876
+ started: false
877
+ };
878
+ const follower = this.follower;
879
+ if (!follower) return {
880
+ task: Promise.resolve(),
881
+ started: false
882
+ };
883
+ let task;
884
+ task = follower.runCatchUp().catch(() => {}).finally(() => {
885
+ if (this.catchUpInflight === task) this.catchUpInflight = void 0;
886
+ });
887
+ this.catchUpInflight = task;
888
+ return {
889
+ task,
890
+ started: true
891
+ };
892
+ }
893
+ /**
894
+ * Wire declared anchor types into the shared key registry. Types with an
895
+ * explicit function keyGenerator use it directly; types without one get a
896
+ * normalizedValue generator (lowercase+trim) instead of the exact fallback
897
+ * (JSON.stringify, field-order/case-sensitive).
898
+ *
899
+ * Built-in types (time/location/person/topic — registered in
900
+ * `KeyGeneratorRegistry`'s constructor) are never overwritten here, even if
901
+ * a manifest or `.index/anchor-types/` declaration reuses one of those
902
+ * names: the built-in's multi-dimensional key generator always wins (issue
903
+ * #934 P0 — see `KeyGeneratorRegistry.isBuiltin`). Shared by
904
+ * `registerDomains` (manifest wiring) and `discoverProvider` (`.index/`
905
+ * convention wiring) — previously duplicated inline in both.
906
+ */
907
+ registerAnchorTypeKeyGenerators(anchorTypes) {
908
+ for (const anchorType of anchorTypes) {
909
+ if (this.keyRegistry.isBuiltin(anchorType.name)) continue;
910
+ if (typeof anchorType.keyGenerator === "function") this.keyRegistry.register(anchorType.name, anchorType.keyGenerator);
911
+ else this.keyRegistry.register(anchorType.name, createNormalizedValueGenerator());
912
+ }
913
+ }
914
+ async execRegisterDomain(_ctx, args) {
915
+ const parsed = RegisterDomainArgsSchema.safeParse(args);
916
+ if (!parsed.success) return {
917
+ success: false,
918
+ error: {
919
+ code: "VALIDATION_ERROR",
920
+ message: explainZodError(parsed.error, RegisterDomainArgsSchema)
921
+ }
922
+ };
923
+ const name = parsed.data.name;
924
+ const types = parsed.data.types ?? [];
925
+ this.domains.set(name, {
926
+ name,
927
+ types
928
+ });
929
+ return {
930
+ success: true,
931
+ data: {
932
+ domain: name,
933
+ typeCount: types.length
934
+ }
935
+ };
936
+ }
937
+ async execDiscover(_ctx, args) {
938
+ const parsed = DiscoverArgsSchema.safeParse(args);
939
+ if (!parsed.success) return {
940
+ success: false,
941
+ error: {
942
+ code: "VALIDATION_ERROR",
943
+ message: explainZodError(parsed.error, DiscoverArgsSchema)
944
+ }
945
+ };
946
+ if (!this.afsRoot) return {
947
+ success: false,
948
+ error: {
949
+ code: "NOT_MOUNTED",
950
+ message: "Provider not mounted (no AFS root)"
951
+ }
952
+ };
953
+ const { providerPath } = parsed.data;
954
+ const domainName = parsed.data.domain ?? providerPath.split("/").filter(Boolean).pop() ?? "unknown";
955
+ const result = await this.discoverProvider(providerPath, domainName);
956
+ return {
957
+ success: true,
958
+ data: {
959
+ domain: result.domainName,
960
+ anchorTypes: result.anchorTypes.map((t) => t.name),
961
+ config: result.config,
962
+ extractor: result.extractor
963
+ }
964
+ };
965
+ }
966
+ async discoverProvider(providerPath, domainName) {
967
+ const root = this.afsRoot;
968
+ let config;
969
+ try {
970
+ const content = (await root.read(joinURL(providerPath, ".index", "config"))).data?.content;
971
+ if (content && typeof content === "object") config = content;
972
+ } catch {}
973
+ const anchorTypes = [];
974
+ try {
975
+ const typesPath = joinURL(providerPath, ".index", "anchor-types");
976
+ const typesList = await root.list(typesPath);
977
+ for (const entry of typesList.data) if (entry.id) try {
978
+ const content = (await root.read(joinURL(typesPath, entry.id))).data?.content;
979
+ if (content && typeof content === "object" && "name" in content) anchorTypes.push(content);
980
+ } catch {}
981
+ } catch {}
982
+ let extractor;
983
+ try {
984
+ const content = (await root.read(joinURL(providerPath, ".index", "extractor"))).data?.content;
985
+ if (content && typeof content === "object" && "path" in content) extractor = content;
986
+ } catch {}
987
+ this.domains.set(domainName, {
988
+ name: domainName,
989
+ scope: providerPath,
990
+ types: anchorTypes,
991
+ config,
992
+ extractor
993
+ });
994
+ this.registerAnchorTypeKeyGenerators(anchorTypes);
995
+ if (extractor) this.extractors.register(domainName, {
996
+ id: `${domainName}-extractor`,
997
+ scope: providerPath,
998
+ mode: extractor.mode,
999
+ extractor: extractor.path
1000
+ });
1001
+ return {
1002
+ domainName,
1003
+ anchorTypes,
1004
+ config,
1005
+ extractor
1006
+ };
1007
+ }
1008
+ /**
1009
+ * Check if a path should be indexed based on .index/config rules.
1010
+ * Finds the domain whose scope covers the path and applies its config.
1011
+ * If no matching domain config exists, allows indexing by default.
1012
+ */
1013
+ shouldIndex(path, _domain) {
1014
+ let config;
1015
+ for (const domainDef of this.domains.values()) if (domainDef.scope && path.startsWith(domainDef.scope) && domainDef.config) {
1016
+ config = domainDef.config;
1017
+ break;
1018
+ }
1019
+ if (!config) return true;
1020
+ const { indexable, skip } = config;
1021
+ if (skip?.some((pattern) => matchGlob(path, pattern))) return false;
1022
+ if (indexable && indexable.length > 0) return indexable.some((pattern) => matchGlob(path, pattern));
1023
+ return true;
1024
+ }
1025
+ /**
1026
+ * Auto-generate embedding via LLM provider if configured and enabled.
1027
+ * Returns null if LLM is not available or embedding is disabled.
1028
+ *
1029
+ * @param type - "passage" for write-time (document), "query" for query-time.
1030
+ * Models like E5 require different prefixes for query vs passage.
1031
+ */
1032
+ async autoEmbed(text, type = "passage") {
1033
+ return createEmbedder({
1034
+ embeddingEnabled: this.options.embeddingEnabled,
1035
+ llmProvider: this.options.llmProvider,
1036
+ embedRoot: this.options.embeddingAfsRoot ?? this.afsRoot
1037
+ })(text, type);
1038
+ }
1039
+ /**
1040
+ * Resolve the chat-action path used by query rewriters. Prefers an
1041
+ * explicit `chatProvider` config; falls back to `<llmProvider>/.actions/chat`
1042
+ * because the same AigneHub default endpoint hosts both embedding
1043
+ * and chat actions in practice.
1044
+ */
1045
+ resolveChatProvider() {
1046
+ if (this.options.chatProvider) return this.options.chatProvider;
1047
+ if (this.options.llmProvider) return joinURL(this.options.llmProvider, ".actions", "chat");
1048
+ return null;
1049
+ }
1050
+ /**
1051
+ * Generate paraphrases of `question` via the chat provider. Returns
1052
+ * the paraphrases plus the original question (the original is kept
1053
+ * as a safety net in case the rewrites drift). Returns just `[question]`
1054
+ * if the chat call fails — degrades gracefully to baseline.
1055
+ */
1056
+ async generateParaphrases(question) {
1057
+ const chatPath = this.resolveChatProvider();
1058
+ const chatRoot = this.options.embeddingAfsRoot ?? this.afsRoot;
1059
+ if (!chatPath || !chatRoot) return [question];
1060
+ try {
1061
+ const result = await chatRoot.exec(chatPath, { messages: [{
1062
+ role: "system",
1063
+ content: "Rewrite a search query as 3 different paraphrases that mean the same thing but use different vocabulary. Each paraphrase should be a single line. Output exactly 3 lines, no numbering, no preamble. Keep them concrete and search-ready."
1064
+ }, {
1065
+ role: "user",
1066
+ content: question
1067
+ }] }, {});
1068
+ if (!result.success || !result.data) return [question];
1069
+ const data = result.data;
1070
+ if (!data.text) return [question];
1071
+ return [question, ...data.text.split("\n").map((l) => l.replace(/^\s*[-*0-9.)\]]+\s*/, "").trim()).filter((l) => l.length > 0).slice(0, 3)];
1072
+ } catch {
1073
+ return [question];
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Multi-query retrieval: generate N paraphrases, embed each, run a
1078
+ * separate semantic query per variant, and RRF-fuse the result lists.
1079
+ * The `index-diverse-bench` shows 2.6× MRR lift on paraphrase queries
1080
+ * with this strategy. Each variant pulls top-(2×limit) so the fusion
1081
+ * has enough candidates to differentiate; the final list is trimmed
1082
+ * to the caller's requested limit.
1083
+ */
1084
+ async queryWithMultiQuery(input) {
1085
+ const { original, ftsText, mode, anchors, opts } = input;
1086
+ const variants = await this.generateParaphrases(original);
1087
+ const requestedLimit = opts?.limit ?? 10;
1088
+ const fanoutLimit = Math.max(requestedLimit * 2, 10);
1089
+ const lists = [];
1090
+ for (const variant of variants) {
1091
+ const embedding = await this.autoEmbed(variant, "query");
1092
+ const variantResults = await this.queryEngine.query({
1093
+ mode,
1094
+ anchors,
1095
+ text: variant === original ? ftsText : variant,
1096
+ embedding: embedding ?? void 0,
1097
+ opts: {
1098
+ ...opts,
1099
+ limit: fanoutLimit
1100
+ },
1101
+ boundDomains: this.options.boundDomains
1102
+ });
1103
+ lists.push({ entries: variantResults });
1104
+ }
1105
+ const k = 60;
1106
+ const scores = /* @__PURE__ */ new Map();
1107
+ for (const list of lists) for (let i = 0; i < list.entries.length; i++) {
1108
+ const entry = list.entries[i];
1109
+ const contribution = 1 / (k + i + 1);
1110
+ const existing = scores.get(entry.entryPath);
1111
+ if (existing) existing.score += contribution;
1112
+ else scores.set(entry.entryPath, {
1113
+ entry,
1114
+ score: contribution
1115
+ });
1116
+ }
1117
+ return [...scores.values()].sort((a, b) => b.score - a.score).slice(0, requestedLimit).map(({ entry, score }) => ({
1118
+ ...entry,
1119
+ score
1120
+ }));
1121
+ }
1122
+ };
1123
+ __decorate([Read("/")], AFSIndex.prototype, "readRoot", null);
1124
+ __decorate([List("/")], AFSIndex.prototype, "listRoot", null);
1125
+ __decorate([Actions("/")], AFSIndex.prototype, "listActions", null);
1126
+ __decorate([Read("/domains")], AFSIndex.prototype, "readDomains", null);
1127
+ __decorate([Read("/stats")], AFSIndex.prototype, "readStatsDir", null);
1128
+ __decorate([Read("/config")], AFSIndex.prototype, "readConfigDir", null);
1129
+ __decorate([List("/domains")], AFSIndex.prototype, "listDomains", null);
1130
+ __decorate([List("/stats")], AFSIndex.prototype, "listStatsDir", null);
1131
+ __decorate([List("/config")], AFSIndex.prototype, "listConfig", null);
1132
+ __decorate([List("/stats/:key")], AFSIndex.prototype, "listStatsKey", null);
1133
+ __decorate([List("/config/:key")], AFSIndex.prototype, "listConfigKey", null);
1134
+ __decorate([List("/domains/:name")], AFSIndex.prototype, "listDomainName", null);
1135
+ __decorate([Meta("/")], AFSIndex.prototype, "readRootMeta", null);
1136
+ __decorate([Meta("/domains")], AFSIndex.prototype, "readDomainsMeta", null);
1137
+ __decorate([Meta("/stats")], AFSIndex.prototype, "readStatsMeta", null);
1138
+ __decorate([Meta("/config")], AFSIndex.prototype, "readConfigMeta", null);
1139
+ __decorate([Meta("/stats/:key")], AFSIndex.prototype, "readStatsKeyMeta", null);
1140
+ __decorate([Meta("/config/:key")], AFSIndex.prototype, "readConfigKeyMeta", null);
1141
+ __decorate([Meta("/domains/:name")], AFSIndex.prototype, "readDomainNameMeta", null);
1142
+ __decorate([Read("/.meta/.capabilities")], AFSIndex.prototype, "readCapabilities", null);
1143
+ __decorate([Explain("/")], AFSIndex.prototype, "explainRoot", null);
1144
+ __decorate([Explain("/:path*")], AFSIndex.prototype, "explainPath", null);
1145
+ __decorate([Search("/:path*")], AFSIndex.prototype, "searchHandler", null);
1146
+ __decorate([Read("/config/:key")], AFSIndex.prototype, "readConfig", null);
1147
+ __decorate([Read("/stats/:key")], AFSIndex.prototype, "readStats", null);
1148
+ __decorate([Actions.Exec("/", "index")], AFSIndex.prototype, "execIndex", null);
1149
+ __decorate([Actions.Exec("/", "query")], AFSIndex.prototype, "execQuery", null);
1150
+ __decorate([Actions.Exec("/", "get")], AFSIndex.prototype, "execGet", null);
1151
+ __decorate([Actions.Exec("/", "reindex")], AFSIndex.prototype, "execReindex", null);
1152
+ __decorate([Actions.Exec("/", "cleanup")], AFSIndex.prototype, "execCleanup", null);
1153
+ __decorate([Actions.Exec("/", "verify")], AFSIndex.prototype, "execVerify", null);
1154
+ __decorate([Actions.Exec("/", "register-domain")], AFSIndex.prototype, "execRegisterDomain", null);
1155
+ __decorate([Actions.Exec("/", "discover")], AFSIndex.prototype, "execDiscover", null);
1156
+ /**
1157
+ * Simple glob matching — supports `*` (single segment) and `**` (multi-segment).
1158
+ * Also matches bare extensions like `*.md` against any path ending with `.md`.
1159
+ */
1160
+ function matchGlob(path, pattern) {
1161
+ const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "⧫").replace(/\*/g, "[^/]*").replace(/⧫/g, ".*").replace(/\?/g, "[^/]");
1162
+ return (/* @__PURE__ */ new RegExp(`(^|/)${regexStr}$`)).test(path);
1163
+ }
1164
+
1165
+ //#endregion
1166
+ export { AFSIndex };
1167
+ //# sourceMappingURL=index-provider.mjs.map