@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,241 @@
1
+ import { IndexBackend } from "./backend/types.cjs";
2
+ import { DirtyPathQueue } from "./dirty-queue.cjs";
3
+ import { AFSRoot, BlockletIndexDomain } from "@aigne/afs";
4
+
5
+ //#region src/follower.d.ts
6
+ declare const FOLLOWER_CONSUMER_ID = "index";
7
+ /**
8
+ * Derive a follower consumer_id that isolates per `(scope, instanceDid)` —
9
+ * `"index:{scope}:{instanceDid}"`. A single shared backend (Node's `/user/index`
10
+ * file, CF's D1) hosts many callers AND many blocklets per caller; the cursor
11
+ * row must be unique per (caller scope × blocklet instance) because each
12
+ * blocklet follows its OWN instance-space changelog. `instanceDid` is optional
13
+ * for backward compatibility (bare `index:{scope}` for pre-fix rows / callers
14
+ * that don't supply it); passing the bare `FOLLOWER_CONSUMER_ID` is only safe
15
+ * for single-scope callers (tests, standalone use).
16
+ */
17
+ declare function deriveFollowerConsumerId(scope: string, instanceDid?: string | null): string;
18
+ /**
19
+ * Memory-map key for a caller's per-blocklet index resources (the AFSIndex
20
+ * handle, its in-flight build promise, its IndexFollower). Isolation is
21
+ * per-`(scope, instanceDid)`: the SAME caller (scope) gets a SEPARATE index per
22
+ * blocklet (instanceDid). Keying these isolate-lived maps by scope ALONE let a
23
+ * caller's second index-blocklet reuse the FIRST's handle/follower (wrong bound
24
+ * domains + wrong instance-space changelog) — the in-memory twin of the cursor
25
+ * collision `deriveFollowerConsumerId` guards against.
26
+ */
27
+ declare function userIndexKey(scope: string, instanceDid?: string | null): string;
28
+ declare const DEFAULT_MAX_PROCESSED_PER_TICK = 4;
29
+ declare const DEFAULT_MAX_NOOP_PER_TICK = 12;
30
+ declare const DEFAULT_MAX_SCAN_PER_TICK = 400;
31
+ /** Preferred alias for {@link DEFAULT_MAX_SCAN_PER_TICK} in dirty-drain call sites. */
32
+ declare const DEFAULT_MAX_DIRTY_ROWS_PER_TICK = 400;
33
+ interface IndexFollowerBatchResult {
34
+ scanned: number;
35
+ /** Matched PUT rows that consumed the expensive maxProcessed budget. */
36
+ processed: number;
37
+ /**
38
+ * Rows that consumed the maxNoop budget (design §8): delete rows, put-rows
39
+ * whose live state was missing (removed as cleanup), AND non-indexable
40
+ * (unmatched-domain) rows.
41
+ */
42
+ noopProcessed: number;
43
+ /** PUT rows that actually wrote entry/anchor/summary index data. */
44
+ indexed: number;
45
+ /** Delete/remove rows whose index cleanup was attempted. */
46
+ deleted: number;
47
+ /** Matched rows that intentionally wrote no index data (empty or stale content). */
48
+ nooped: number;
49
+ advanced: boolean;
50
+ hasMore: boolean;
51
+ /**
52
+ * At least one dirty row lost the ack race-guard this tick — a newer write
53
+ * bumped `latest_seq` while we processed, so the guarded ack no-oped and the
54
+ * newer dirty row waits for the next tick. NOT an error (real progress was
55
+ * made on the superseded state); surfaced so the CF/Node drain logs can
56
+ * distinguish healthy churn (`ackRace`) from failures (`failed`).
57
+ */
58
+ ackRace: boolean;
59
+ /**
60
+ * Why the tick stopped. `scan_budget` = the dirty-row FETCH budget (maxScan /
61
+ * maxDirtyRows) was exhausted — the name predates the §13 cutover (it used to
62
+ * mean "changelog scan budget"); kept as a stable value across the exported
63
+ * scheduler/daemon types.
64
+ */
65
+ stopReason?: "processed_budget" | "noop_budget" | "scan_budget" | "exhausted" | "aborted";
66
+ /** Log-compat only; dirty mode never advances a seq cursor (both 0). */
67
+ fromSeq: number;
68
+ toSeq: number;
69
+ matched: number;
70
+ unmatched: number;
71
+ skipped: number;
72
+ failed: number;
73
+ aborted: boolean;
74
+ abortReason?: string;
75
+ }
76
+ interface IndexFollowerOptions {
77
+ /** AFS backed by DIDSpaceProvider+PrefixedTreeIndex — CURRENT-state file read/stat
78
+ * for the dirty drain (the `.changelog` consumption line was deleted at §13 cutover). */
79
+ spaceAfs: AFSRoot;
80
+ /** Index backend for writing indexed data. Must implement cursor methods. */
81
+ backend: IndexBackend;
82
+ /** Domain declarations from blocklet.yaml `index:` section. */
83
+ domains: BlockletIndexDomain[];
84
+ /**
85
+ * Cursor row identity for THIS follower. Defaults to the bare
86
+ * `FOLLOWER_CONSUMER_ID` ("index") for single-scope callers (tests,
87
+ * standalone use). Any caller whose backend hosts MULTIPLE scopes MUST pass
88
+ * `deriveFollowerConsumerId(scope)` — otherwise every scope sharing that
89
+ * backend would read/write the SAME `idx_index_cursor` row, each run
90
+ * clobbering the others' progress.
91
+ */
92
+ consumerId?: string;
93
+ /**
94
+ * Optional passage embedder — generates the vector stored alongside each
95
+ * indexed summary for semantic recall (Epic #932). Usually left unset here and
96
+ * wired later via `setEmbedder` (see its doc). Returns null to skip embedding.
97
+ */
98
+ embed?: (text: string) => Promise<number[] | null>;
99
+ /**
100
+ * Source-side dirty queue (index-follower-dirty-path). The follower DRAINS the
101
+ * merged `ds_dirty_path` projection — catch-up is O(unique dirty paths), not
102
+ * O(changelog rows). This is the SOLE catch-up mechanism (§13 cutover — the
103
+ * changelog `/.changelog` replay was deleted); a follower with no dirty queue
104
+ * has no source to drain and `runCatchUp`/`runBatch` are no-ops for it.
105
+ * Normally a fragment-scoped `PrefixedTreeIndex` so `listDirtyPaths` returns
106
+ * stripped `originalPath`s. ack/fail must be a runtime-internal capability,
107
+ * never a public `/user` action.
108
+ */
109
+ dirtyQueue?: DirtyPathQueue;
110
+ }
111
+ declare class IndexFollower {
112
+ private readonly spaceAfs;
113
+ private readonly backend;
114
+ private readonly domains;
115
+ private readonly consumerId;
116
+ private embed?;
117
+ private catchUpLimits?;
118
+ private readonly dirtyQueue?;
119
+ constructor(opts: IndexFollowerOptions);
120
+ /**
121
+ * Override the inline `runCatchUp()` budget — the per-invocation caps on
122
+ * matched PUT attempts, matched delete/remove rows, and dirty rows FETCHED
123
+ * (`maxScan`). Injected by the runtime from the SAME env group the cron uses
124
+ * (`AFS_INDEX_MAX_*_PER_TICK`) so both indexing paths share one budget group.
125
+ * Unset → provider defaults (standalone/tests).
126
+ */
127
+ setCatchUpLimits(limits: {
128
+ maxProcessed: number;
129
+ maxNoop?: number;
130
+ maxScan: number;
131
+ }): void;
132
+ /**
133
+ * Wire (or replace) the passage embedder used at index time — semantic recall
134
+ * (Epic #932). Called by `AFSIndex.setFollower` (query-path follower) and by
135
+ * `rebuildFollowerFromCursor` (CF cron-path follower) so BOTH indexing paths
136
+ * write embeddings; without this a changelog-indexed entry has only FTS +
137
+ * anchors and cross-lingual / semantic queries can't match it. A no-op
138
+ * embedder (or a backend without `writeEmbedding`) degrades cleanly to
139
+ * FTS+anchor.
140
+ */
141
+ setEmbedder(embed: (text: string) => Promise<number[] | null>): void;
142
+ /**
143
+ * Lightweight catch-up: drain any ready dirty rows so a query sees a fresh
144
+ * index without waiting for the next cron tick. Called from execQuery.
145
+ *
146
+ * Dirty drain is the SOLE catch-up mechanism (index-follower-dirty-path §13
147
+ * cutover — the changelog `/.changelog` replay was deleted). Freshness = "are
148
+ * there ready dirty rows under my prefix", so there is no `last_seq` probe: a
149
+ * query sees a fresh index by processing whatever dirty paths are pending
150
+ * (design §4.2, §8). A follower with no dirty queue (standalone/legacy) is a
151
+ * no-op here — it has no source to drain.
152
+ */
153
+ runCatchUp(): Promise<void>;
154
+ /**
155
+ * Bounded per-tick drain of the dirty queue — attempt up to `maxProcessed`
156
+ * matched PUT rows, up to `maxNoop` matched delete/remove/noop rows, fetching
157
+ * at most `maxScan` (a.k.a. `maxDirtyRows`) dirty rows. Called from the cron.
158
+ *
159
+ * Dirty drain is the SOLE mechanism (index-follower-dirty-path §13 cutover —
160
+ * changelog replay was deleted). A follower with no dirty queue has no source
161
+ * to drain, so it is idle. Separate budgets are intentional: matched PUT rows
162
+ * are the expensive read+embed+write axis, delete/noop rows are cheaper.
163
+ */
164
+ runBatch(limits?: {
165
+ maxProcessed?: number;
166
+ maxNoop?: number; /** Max dirty rows fetched this tick. `maxDirtyRows` is the preferred name. */
167
+ maxScan?: number;
168
+ maxDirtyRows?: number;
169
+ }): Promise<IndexFollowerBatchResult>;
170
+ /** Empty batch result (no dirty queue wired / nothing to drain). */
171
+ private idleResult;
172
+ /**
173
+ * Cold rebuild of an EXPLICIT list of space-relative paths — the enumeration
174
+ * is done by the caller (an external driver listing the live tree via D1),
175
+ * NOT in this process. Reads CURRENT live state per path and writes the
176
+ * derived index layers, independent of any queue — so it is the seam the
177
+ * offline reset-and-rebuild (`offlineRebuildScope`) uses to rebuild from live
178
+ * `ds_entries`. Keeping the per-call cost to just `paths.length`
179
+ * read+embed+writes with ZERO in-Worker tree enumeration is what keeps a small
180
+ * batch inside the CF free-tier per-request CPU/subrequest budget (an in-Worker
181
+ * `maxDepth:-1` enumeration pushed cold isolates over that budget into 503s).
182
+ * Each path is matched against the declared domains and indexed through the
183
+ * same `indexEntry` as the drain path (byte-identical output). `paths` are
184
+ * space-relative, e.g. `"items/foo.json"` (leading slash tolerated).
185
+ */
186
+ reindexPaths(paths: string[], onProgress?: (done: number, total: number) => void): Promise<{
187
+ indexed: number;
188
+ failed: number;
189
+ skipped: number;
190
+ }>;
191
+ /**
192
+ * Dirty drain (index-follower-dirty-path §8) — the O(unique dirty paths)
193
+ * replacement for changelog replay. Fetches ready dirty rows under this
194
+ * follower's prefix, and for each: domain-matches the stripped `originalPath`,
195
+ * reads CURRENT live state, and indexes / removes accordingly, then acks with
196
+ * the `latestSeq` race guard. A transient failure marks the row with a backoff
197
+ * (so a poison row can't refill the tick) instead of acking.
198
+ *
199
+ * Dirty mode NEVER writes `idx_index_cursor.last_seq` (design §4.2 — it is a
200
+ * dead column post-cutover); scheduling reads `advanced/hasMore/scanned` +
201
+ * the dirty count. The `dirtyQueue.listDirtyPaths({ prefix: "" })` call assumes
202
+ * a fragment-scoped queue (PrefixedTreeIndex), so `row.path` is already the
203
+ * stripped `originalPath` the domain globs + `spaceAfs.read` expect.
204
+ */
205
+ private processDirty;
206
+ /** Remove the indexed doc(s) for a delete/removed dirty row across matched domains. */
207
+ private removeDirty;
208
+ /** Shared index-removal (delete branch) — throws ChangelogAbortError on failure so the row backs off. */
209
+ private removeIndexed;
210
+ /**
211
+ * Handle a `put` dirty row against CURRENT live state (design §4.3 / §8 step 6):
212
+ * - live exists → indexEntry (writes FTS/anchor/summary/embedding).
213
+ * - live missing (authoritative 404) → remove the indexed doc.
214
+ * - stat throws a NON-404 error → transient → ChangelogAbortError (retry).
215
+ * This is what makes a stale put whose file was deleted converge to "removed",
216
+ * while a replica-lag / D1 blip is NEVER mistaken for a deletion.
217
+ */
218
+ private indexPutDirty;
219
+ /**
220
+ * Does `originalPath` STILL EXIST (via `stat`)? Used to classify a `read()`
221
+ * failure (issue #935 BLOCK fix #3 / sync-engine parity):
222
+ * - stat 404 (or throws) → the file is gone (later delete, or never existed
223
+ * at this position) → the row is stale, safe to skip.
224
+ * - stat succeeds → the file is live; the read failure was transient (R2/D1
225
+ * blip) and must be retried, not lost.
226
+ * Post-cutover the drain only ever indexes live state (cid is never compared —
227
+ * supersession is caught by the ack race-guard), so existence is the only
228
+ * question left here.
229
+ */
230
+ private entryStillCurrent;
231
+ private pathMatchesDomain;
232
+ private indexEntry;
233
+ /**
234
+ * Build the stable index entry path for a file. Uses the domain scope
235
+ * (default "/user") as a prefix so query-scope filtering works.
236
+ */
237
+ private makeEntryPath;
238
+ }
239
+ //#endregion
240
+ export { DEFAULT_MAX_DIRTY_ROWS_PER_TICK, DEFAULT_MAX_NOOP_PER_TICK, DEFAULT_MAX_PROCESSED_PER_TICK, DEFAULT_MAX_SCAN_PER_TICK, FOLLOWER_CONSUMER_ID, IndexFollower, IndexFollowerBatchResult, IndexFollowerOptions, deriveFollowerConsumerId, userIndexKey };
241
+ //# sourceMappingURL=follower.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follower.d.cts","names":[],"sources":["../src/follower.ts"],"mappings":";;;;;cAqDa,oBAAA;;;;;;;;;;;iBAYG,wBAAA,CAAyB,KAAA,UAAe,WAAA;;;;;;;;;;iBAexC,YAAA,CAAa,KAAA,UAAe,WAAA;AAAA,cAe/B,8BAAA;AAAA,cACA,yBAAA;AAAA,cAQA,yBAAA;;cAEA,+BAAA;AAAA,UA+BI,wBAAA;EACf,OAAA;EAkDS;EAhDT,SAAA;EA0Ea;;;;;EApEb,aAAA;EAwCS;EAtCT,OAAA;EAwCS;EAtCT,OAAA;EAqDA;EAnDA,MAAA;EACA,QAAA;EACA,OAAA;EA4Da;;;AAGf;;;;EAvDE,OAAA;EA8GoB;;;;;;EAvGpB,UAAA;EAkDiB;EAhDjB,OAAA;EACA,KAAA;EACA,OAAA;EACA,SAAA;EACA,OAAA;EACA,MAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,oBAAA;EA+DkC;;EA5DjD,QAAA,EAAU,OAAA;EAyEV;EAvEA,OAAA,EAAS,YAAA;EAuE4B;EArErC,OAAA,EAAS,mBAAA;EAoFH;;;;;;;;EA3EN,UAAA;EAoGY;;;;;EA9FZ,KAAA,IAAS,IAAA,aAAiB,OAAA;EA8IxB;;;;;;;;;;EAnIF,UAAA,GAAa,cAAA;AAAA;AAAA,cAGF,aAAA;EAAA,iBACM,QAAA;EAAA,iBACA,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,UAAA;EAAA,QACT,KAAA;EAAA,QAGA,aAAA;EAAA,iBACS,UAAA;cAEL,IAAA,EAAM,oBAAA;;;;;;;;EAgBlB,gBAAA,CAAiB,MAAA;IAAU,YAAA;IAAsB,OAAA;IAAkB,OAAA;EAAA;;;;;;;;;;EAanE,WAAA,CAAY,KAAA,GAAQ,IAAA,aAAiB,OAAA;;;;;;;;;;;;EAe/B,UAAA,CAAA,GAAc,OAAA;;;;;;;;;;;EAmBd,QAAA,CAAS,MAAA;IACb,YAAA;IACA,OAAA;IAEA,OAAA;IACA,YAAA;EAAA,IACE,OAAA,CAAQ,wBAAA;;UAUJ,UAAA;;;;;;;;;;;;;;;EAoCF,YAAA,CACJ,KAAA,YACA,UAAA,IAAc,IAAA,UAAc,KAAA,oBAC3B,OAAA;IAAU,OAAA;IAAiB,MAAA;IAAgB,OAAA;EAAA;;;;;;;;;;;;;;;UAwChC,YAAA;;UAwMA,WAAA;;UAUA,aAAA;;;;;;;;;UAyBA,aAAA;;;;;;;;;;;;UAkDA,iBAAA;EAAA,QASN,iBAAA;EAAA,QAKM,UAAA;;;;;UAwHN,aAAA;AAAA"}
@@ -0,0 +1,241 @@
1
+ import { IndexBackend } from "./backend/types.mjs";
2
+ import { DirtyPathQueue } from "./dirty-queue.mjs";
3
+ import { AFSRoot, BlockletIndexDomain } from "@aigne/afs";
4
+
5
+ //#region src/follower.d.ts
6
+ declare const FOLLOWER_CONSUMER_ID = "index";
7
+ /**
8
+ * Derive a follower consumer_id that isolates per `(scope, instanceDid)` —
9
+ * `"index:{scope}:{instanceDid}"`. A single shared backend (Node's `/user/index`
10
+ * file, CF's D1) hosts many callers AND many blocklets per caller; the cursor
11
+ * row must be unique per (caller scope × blocklet instance) because each
12
+ * blocklet follows its OWN instance-space changelog. `instanceDid` is optional
13
+ * for backward compatibility (bare `index:{scope}` for pre-fix rows / callers
14
+ * that don't supply it); passing the bare `FOLLOWER_CONSUMER_ID` is only safe
15
+ * for single-scope callers (tests, standalone use).
16
+ */
17
+ declare function deriveFollowerConsumerId(scope: string, instanceDid?: string | null): string;
18
+ /**
19
+ * Memory-map key for a caller's per-blocklet index resources (the AFSIndex
20
+ * handle, its in-flight build promise, its IndexFollower). Isolation is
21
+ * per-`(scope, instanceDid)`: the SAME caller (scope) gets a SEPARATE index per
22
+ * blocklet (instanceDid). Keying these isolate-lived maps by scope ALONE let a
23
+ * caller's second index-blocklet reuse the FIRST's handle/follower (wrong bound
24
+ * domains + wrong instance-space changelog) — the in-memory twin of the cursor
25
+ * collision `deriveFollowerConsumerId` guards against.
26
+ */
27
+ declare function userIndexKey(scope: string, instanceDid?: string | null): string;
28
+ declare const DEFAULT_MAX_PROCESSED_PER_TICK = 4;
29
+ declare const DEFAULT_MAX_NOOP_PER_TICK = 12;
30
+ declare const DEFAULT_MAX_SCAN_PER_TICK = 400;
31
+ /** Preferred alias for {@link DEFAULT_MAX_SCAN_PER_TICK} in dirty-drain call sites. */
32
+ declare const DEFAULT_MAX_DIRTY_ROWS_PER_TICK = 400;
33
+ interface IndexFollowerBatchResult {
34
+ scanned: number;
35
+ /** Matched PUT rows that consumed the expensive maxProcessed budget. */
36
+ processed: number;
37
+ /**
38
+ * Rows that consumed the maxNoop budget (design §8): delete rows, put-rows
39
+ * whose live state was missing (removed as cleanup), AND non-indexable
40
+ * (unmatched-domain) rows.
41
+ */
42
+ noopProcessed: number;
43
+ /** PUT rows that actually wrote entry/anchor/summary index data. */
44
+ indexed: number;
45
+ /** Delete/remove rows whose index cleanup was attempted. */
46
+ deleted: number;
47
+ /** Matched rows that intentionally wrote no index data (empty or stale content). */
48
+ nooped: number;
49
+ advanced: boolean;
50
+ hasMore: boolean;
51
+ /**
52
+ * At least one dirty row lost the ack race-guard this tick — a newer write
53
+ * bumped `latest_seq` while we processed, so the guarded ack no-oped and the
54
+ * newer dirty row waits for the next tick. NOT an error (real progress was
55
+ * made on the superseded state); surfaced so the CF/Node drain logs can
56
+ * distinguish healthy churn (`ackRace`) from failures (`failed`).
57
+ */
58
+ ackRace: boolean;
59
+ /**
60
+ * Why the tick stopped. `scan_budget` = the dirty-row FETCH budget (maxScan /
61
+ * maxDirtyRows) was exhausted — the name predates the §13 cutover (it used to
62
+ * mean "changelog scan budget"); kept as a stable value across the exported
63
+ * scheduler/daemon types.
64
+ */
65
+ stopReason?: "processed_budget" | "noop_budget" | "scan_budget" | "exhausted" | "aborted";
66
+ /** Log-compat only; dirty mode never advances a seq cursor (both 0). */
67
+ fromSeq: number;
68
+ toSeq: number;
69
+ matched: number;
70
+ unmatched: number;
71
+ skipped: number;
72
+ failed: number;
73
+ aborted: boolean;
74
+ abortReason?: string;
75
+ }
76
+ interface IndexFollowerOptions {
77
+ /** AFS backed by DIDSpaceProvider+PrefixedTreeIndex — CURRENT-state file read/stat
78
+ * for the dirty drain (the `.changelog` consumption line was deleted at §13 cutover). */
79
+ spaceAfs: AFSRoot;
80
+ /** Index backend for writing indexed data. Must implement cursor methods. */
81
+ backend: IndexBackend;
82
+ /** Domain declarations from blocklet.yaml `index:` section. */
83
+ domains: BlockletIndexDomain[];
84
+ /**
85
+ * Cursor row identity for THIS follower. Defaults to the bare
86
+ * `FOLLOWER_CONSUMER_ID` ("index") for single-scope callers (tests,
87
+ * standalone use). Any caller whose backend hosts MULTIPLE scopes MUST pass
88
+ * `deriveFollowerConsumerId(scope)` — otherwise every scope sharing that
89
+ * backend would read/write the SAME `idx_index_cursor` row, each run
90
+ * clobbering the others' progress.
91
+ */
92
+ consumerId?: string;
93
+ /**
94
+ * Optional passage embedder — generates the vector stored alongside each
95
+ * indexed summary for semantic recall (Epic #932). Usually left unset here and
96
+ * wired later via `setEmbedder` (see its doc). Returns null to skip embedding.
97
+ */
98
+ embed?: (text: string) => Promise<number[] | null>;
99
+ /**
100
+ * Source-side dirty queue (index-follower-dirty-path). The follower DRAINS the
101
+ * merged `ds_dirty_path` projection — catch-up is O(unique dirty paths), not
102
+ * O(changelog rows). This is the SOLE catch-up mechanism (§13 cutover — the
103
+ * changelog `/.changelog` replay was deleted); a follower with no dirty queue
104
+ * has no source to drain and `runCatchUp`/`runBatch` are no-ops for it.
105
+ * Normally a fragment-scoped `PrefixedTreeIndex` so `listDirtyPaths` returns
106
+ * stripped `originalPath`s. ack/fail must be a runtime-internal capability,
107
+ * never a public `/user` action.
108
+ */
109
+ dirtyQueue?: DirtyPathQueue;
110
+ }
111
+ declare class IndexFollower {
112
+ private readonly spaceAfs;
113
+ private readonly backend;
114
+ private readonly domains;
115
+ private readonly consumerId;
116
+ private embed?;
117
+ private catchUpLimits?;
118
+ private readonly dirtyQueue?;
119
+ constructor(opts: IndexFollowerOptions);
120
+ /**
121
+ * Override the inline `runCatchUp()` budget — the per-invocation caps on
122
+ * matched PUT attempts, matched delete/remove rows, and dirty rows FETCHED
123
+ * (`maxScan`). Injected by the runtime from the SAME env group the cron uses
124
+ * (`AFS_INDEX_MAX_*_PER_TICK`) so both indexing paths share one budget group.
125
+ * Unset → provider defaults (standalone/tests).
126
+ */
127
+ setCatchUpLimits(limits: {
128
+ maxProcessed: number;
129
+ maxNoop?: number;
130
+ maxScan: number;
131
+ }): void;
132
+ /**
133
+ * Wire (or replace) the passage embedder used at index time — semantic recall
134
+ * (Epic #932). Called by `AFSIndex.setFollower` (query-path follower) and by
135
+ * `rebuildFollowerFromCursor` (CF cron-path follower) so BOTH indexing paths
136
+ * write embeddings; without this a changelog-indexed entry has only FTS +
137
+ * anchors and cross-lingual / semantic queries can't match it. A no-op
138
+ * embedder (or a backend without `writeEmbedding`) degrades cleanly to
139
+ * FTS+anchor.
140
+ */
141
+ setEmbedder(embed: (text: string) => Promise<number[] | null>): void;
142
+ /**
143
+ * Lightweight catch-up: drain any ready dirty rows so a query sees a fresh
144
+ * index without waiting for the next cron tick. Called from execQuery.
145
+ *
146
+ * Dirty drain is the SOLE catch-up mechanism (index-follower-dirty-path §13
147
+ * cutover — the changelog `/.changelog` replay was deleted). Freshness = "are
148
+ * there ready dirty rows under my prefix", so there is no `last_seq` probe: a
149
+ * query sees a fresh index by processing whatever dirty paths are pending
150
+ * (design §4.2, §8). A follower with no dirty queue (standalone/legacy) is a
151
+ * no-op here — it has no source to drain.
152
+ */
153
+ runCatchUp(): Promise<void>;
154
+ /**
155
+ * Bounded per-tick drain of the dirty queue — attempt up to `maxProcessed`
156
+ * matched PUT rows, up to `maxNoop` matched delete/remove/noop rows, fetching
157
+ * at most `maxScan` (a.k.a. `maxDirtyRows`) dirty rows. Called from the cron.
158
+ *
159
+ * Dirty drain is the SOLE mechanism (index-follower-dirty-path §13 cutover —
160
+ * changelog replay was deleted). A follower with no dirty queue has no source
161
+ * to drain, so it is idle. Separate budgets are intentional: matched PUT rows
162
+ * are the expensive read+embed+write axis, delete/noop rows are cheaper.
163
+ */
164
+ runBatch(limits?: {
165
+ maxProcessed?: number;
166
+ maxNoop?: number; /** Max dirty rows fetched this tick. `maxDirtyRows` is the preferred name. */
167
+ maxScan?: number;
168
+ maxDirtyRows?: number;
169
+ }): Promise<IndexFollowerBatchResult>;
170
+ /** Empty batch result (no dirty queue wired / nothing to drain). */
171
+ private idleResult;
172
+ /**
173
+ * Cold rebuild of an EXPLICIT list of space-relative paths — the enumeration
174
+ * is done by the caller (an external driver listing the live tree via D1),
175
+ * NOT in this process. Reads CURRENT live state per path and writes the
176
+ * derived index layers, independent of any queue — so it is the seam the
177
+ * offline reset-and-rebuild (`offlineRebuildScope`) uses to rebuild from live
178
+ * `ds_entries`. Keeping the per-call cost to just `paths.length`
179
+ * read+embed+writes with ZERO in-Worker tree enumeration is what keeps a small
180
+ * batch inside the CF free-tier per-request CPU/subrequest budget (an in-Worker
181
+ * `maxDepth:-1` enumeration pushed cold isolates over that budget into 503s).
182
+ * Each path is matched against the declared domains and indexed through the
183
+ * same `indexEntry` as the drain path (byte-identical output). `paths` are
184
+ * space-relative, e.g. `"items/foo.json"` (leading slash tolerated).
185
+ */
186
+ reindexPaths(paths: string[], onProgress?: (done: number, total: number) => void): Promise<{
187
+ indexed: number;
188
+ failed: number;
189
+ skipped: number;
190
+ }>;
191
+ /**
192
+ * Dirty drain (index-follower-dirty-path §8) — the O(unique dirty paths)
193
+ * replacement for changelog replay. Fetches ready dirty rows under this
194
+ * follower's prefix, and for each: domain-matches the stripped `originalPath`,
195
+ * reads CURRENT live state, and indexes / removes accordingly, then acks with
196
+ * the `latestSeq` race guard. A transient failure marks the row with a backoff
197
+ * (so a poison row can't refill the tick) instead of acking.
198
+ *
199
+ * Dirty mode NEVER writes `idx_index_cursor.last_seq` (design §4.2 — it is a
200
+ * dead column post-cutover); scheduling reads `advanced/hasMore/scanned` +
201
+ * the dirty count. The `dirtyQueue.listDirtyPaths({ prefix: "" })` call assumes
202
+ * a fragment-scoped queue (PrefixedTreeIndex), so `row.path` is already the
203
+ * stripped `originalPath` the domain globs + `spaceAfs.read` expect.
204
+ */
205
+ private processDirty;
206
+ /** Remove the indexed doc(s) for a delete/removed dirty row across matched domains. */
207
+ private removeDirty;
208
+ /** Shared index-removal (delete branch) — throws ChangelogAbortError on failure so the row backs off. */
209
+ private removeIndexed;
210
+ /**
211
+ * Handle a `put` dirty row against CURRENT live state (design §4.3 / §8 step 6):
212
+ * - live exists → indexEntry (writes FTS/anchor/summary/embedding).
213
+ * - live missing (authoritative 404) → remove the indexed doc.
214
+ * - stat throws a NON-404 error → transient → ChangelogAbortError (retry).
215
+ * This is what makes a stale put whose file was deleted converge to "removed",
216
+ * while a replica-lag / D1 blip is NEVER mistaken for a deletion.
217
+ */
218
+ private indexPutDirty;
219
+ /**
220
+ * Does `originalPath` STILL EXIST (via `stat`)? Used to classify a `read()`
221
+ * failure (issue #935 BLOCK fix #3 / sync-engine parity):
222
+ * - stat 404 (or throws) → the file is gone (later delete, or never existed
223
+ * at this position) → the row is stale, safe to skip.
224
+ * - stat succeeds → the file is live; the read failure was transient (R2/D1
225
+ * blip) and must be retried, not lost.
226
+ * Post-cutover the drain only ever indexes live state (cid is never compared —
227
+ * supersession is caught by the ack race-guard), so existence is the only
228
+ * question left here.
229
+ */
230
+ private entryStillCurrent;
231
+ private pathMatchesDomain;
232
+ private indexEntry;
233
+ /**
234
+ * Build the stable index entry path for a file. Uses the domain scope
235
+ * (default "/user") as a prefix so query-scope filtering works.
236
+ */
237
+ private makeEntryPath;
238
+ }
239
+ //#endregion
240
+ export { DEFAULT_MAX_DIRTY_ROWS_PER_TICK, DEFAULT_MAX_NOOP_PER_TICK, DEFAULT_MAX_PROCESSED_PER_TICK, DEFAULT_MAX_SCAN_PER_TICK, FOLLOWER_CONSUMER_ID, IndexFollower, IndexFollowerBatchResult, IndexFollowerOptions, deriveFollowerConsumerId, userIndexKey };
241
+ //# sourceMappingURL=follower.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follower.d.mts","names":[],"sources":["../src/follower.ts"],"mappings":";;;;;cAqDa,oBAAA;;;;;;;;;;;iBAYG,wBAAA,CAAyB,KAAA,UAAe,WAAA;;;;;;;;;;iBAexC,YAAA,CAAa,KAAA,UAAe,WAAA;AAAA,cAe/B,8BAAA;AAAA,cACA,yBAAA;AAAA,cAQA,yBAAA;;cAEA,+BAAA;AAAA,UA+BI,wBAAA;EACf,OAAA;EAkDS;EAhDT,SAAA;EA0Ea;;;;;EApEb,aAAA;EAwCS;EAtCT,OAAA;EAwCS;EAtCT,OAAA;EAqDA;EAnDA,MAAA;EACA,QAAA;EACA,OAAA;EA4Da;;;AAGf;;;;EAvDE,OAAA;EA8GoB;;;;;;EAvGpB,UAAA;EAkDiB;EAhDjB,OAAA;EACA,KAAA;EACA,OAAA;EACA,SAAA;EACA,OAAA;EACA,MAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,oBAAA;EA+DkC;;EA5DjD,QAAA,EAAU,OAAA;EAyEV;EAvEA,OAAA,EAAS,YAAA;EAuE4B;EArErC,OAAA,EAAS,mBAAA;EAoFH;;;;;;;;EA3EN,UAAA;EAoGY;;;;;EA9FZ,KAAA,IAAS,IAAA,aAAiB,OAAA;EA8IxB;;;;;;;;;;EAnIF,UAAA,GAAa,cAAA;AAAA;AAAA,cAGF,aAAA;EAAA,iBACM,QAAA;EAAA,iBACA,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,UAAA;EAAA,QACT,KAAA;EAAA,QAGA,aAAA;EAAA,iBACS,UAAA;cAEL,IAAA,EAAM,oBAAA;;;;;;;;EAgBlB,gBAAA,CAAiB,MAAA;IAAU,YAAA;IAAsB,OAAA;IAAkB,OAAA;EAAA;;;;;;;;;;EAanE,WAAA,CAAY,KAAA,GAAQ,IAAA,aAAiB,OAAA;;;;;;;;;;;;EAe/B,UAAA,CAAA,GAAc,OAAA;;;;;;;;;;;EAmBd,QAAA,CAAS,MAAA;IACb,YAAA;IACA,OAAA;IAEA,OAAA;IACA,YAAA;EAAA,IACE,OAAA,CAAQ,wBAAA;;UAUJ,UAAA;;;;;;;;;;;;;;;EAoCF,YAAA,CACJ,KAAA,YACA,UAAA,IAAc,IAAA,UAAc,KAAA,oBAC3B,OAAA;IAAU,OAAA;IAAiB,MAAA;IAAgB,OAAA;EAAA;;;;;;;;;;;;;;;UAwChC,YAAA;;UAwMA,WAAA;;UAUA,aAAA;;;;;;;;;UAyBA,aAAA;;;;;;;;;;;;UAkDA,iBAAA;EAAA,QASN,iBAAA;EAAA,QAKM,UAAA;;;;;UAwHN,aAAA;AAAA"}