@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,74 @@
1
+ //#region src/extraction/manager.ts
2
+ var ExtractorManager = class {
3
+ registrations = /* @__PURE__ */ new Map();
4
+ /**
5
+ * Register an extractor for a domain.
6
+ * @param domain - Domain name this extractor serves
7
+ * @param registration - Extractor configuration
8
+ */
9
+ register(domain, registration) {
10
+ this.registrations.set(domain, registration);
11
+ }
12
+ /**
13
+ * Unregister an extractor for a domain.
14
+ */
15
+ unregister(domain) {
16
+ this.registrations.delete(domain);
17
+ }
18
+ /**
19
+ * Find the first extractor whose scope matches the given path.
20
+ * @returns [domainName, registration] or undefined if no match
21
+ */
22
+ findForPath(path) {
23
+ for (const [domain, reg] of this.registrations) if (path.startsWith(reg.scope)) return [domain, reg];
24
+ }
25
+ /**
26
+ * Dispatch extraction for a path using the matching extractor.
27
+ * Calls the extractor's AFS exec endpoint and returns the result.
28
+ *
29
+ * @returns ExtractionResult or null if no extractor matched or extraction failed
30
+ */
31
+ async extract(path, root) {
32
+ const match = this.findForPath(path);
33
+ if (!match) return null;
34
+ const [domain, reg] = match;
35
+ if (!reg.extractor || !root.exec) return null;
36
+ try {
37
+ const execResult = await root.exec(reg.extractor, { path }, {});
38
+ if (!execResult.success || !execResult.data) return null;
39
+ const data = execResult.data;
40
+ return {
41
+ domain,
42
+ result: {
43
+ anchors: data.anchors ?? [],
44
+ summary: data.summary,
45
+ embedding: data.embedding
46
+ }
47
+ };
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * List all registered extractors.
54
+ */
55
+ list() {
56
+ return this.registrations;
57
+ }
58
+ /**
59
+ * Check if any extractors are registered.
60
+ */
61
+ get hasRegistrations() {
62
+ return this.registrations.size > 0;
63
+ }
64
+ /**
65
+ * Clear all registrations.
66
+ */
67
+ clear() {
68
+ this.registrations.clear();
69
+ }
70
+ };
71
+
72
+ //#endregion
73
+ export { ExtractorManager };
74
+ //# sourceMappingURL=manager.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.mjs","names":[],"sources":["../../src/extraction/manager.ts"],"sourcesContent":["/**\n * ExtractorManager — manages extractor registrations and dispatches extraction requests.\n *\n * Extractors are external AFS exec endpoints that accept a path and return\n * { anchors, summary, embedding? }. They are registered per-domain with a scope\n * that determines which write events they handle.\n */\nimport type { AFSRoot } from \"@aigne/afs\";\nimport type { Anchor, ExtractionResult, ExtractorRegistration } from \"../types.js\";\n\nexport class ExtractorManager {\n private readonly registrations = new Map<string, ExtractorRegistration>();\n\n /**\n * Register an extractor for a domain.\n * @param domain - Domain name this extractor serves\n * @param registration - Extractor configuration\n */\n register(domain: string, registration: ExtractorRegistration): void {\n this.registrations.set(domain, registration);\n }\n\n /**\n * Unregister an extractor for a domain.\n */\n unregister(domain: string): void {\n this.registrations.delete(domain);\n }\n\n /**\n * Find the first extractor whose scope matches the given path.\n * @returns [domainName, registration] or undefined if no match\n */\n findForPath(path: string): [string, ExtractorRegistration] | undefined {\n for (const [domain, reg] of this.registrations) {\n if (path.startsWith(reg.scope)) {\n return [domain, reg];\n }\n }\n return undefined;\n }\n\n /**\n * Dispatch extraction for a path using the matching extractor.\n * Calls the extractor's AFS exec endpoint and returns the result.\n *\n * @returns ExtractionResult or null if no extractor matched or extraction failed\n */\n async extract(\n path: string,\n root: AFSRoot,\n ): Promise<{ domain: string; result: ExtractionResult } | null> {\n const match = this.findForPath(path);\n if (!match) return null;\n\n const [domain, reg] = match;\n if (!reg.extractor || !root.exec) return null;\n\n try {\n const execResult = await root.exec(reg.extractor, { path }, {});\n if (!execResult.success || !execResult.data) return null;\n\n const data = execResult.data as {\n anchors?: Anchor[];\n summary?: string;\n embedding?: number[];\n };\n return {\n domain,\n result: {\n anchors: (data.anchors as Anchor[]) ?? [],\n summary: data.summary,\n embedding: data.embedding,\n },\n };\n } catch {\n return null;\n }\n }\n\n /**\n * List all registered extractors.\n */\n list(): ReadonlyMap<string, ExtractorRegistration> {\n return this.registrations;\n }\n\n /**\n * Check if any extractors are registered.\n */\n get hasRegistrations(): boolean {\n return this.registrations.size > 0;\n }\n\n /**\n * Clear all registrations.\n */\n clear(): void {\n this.registrations.clear();\n }\n}\n"],"mappings":";AAUA,IAAa,mBAAb,MAA8B;CAC5B,AAAiB,gCAAgB,IAAI,KAAoC;;;;;;CAOzE,SAAS,QAAgB,cAA2C;AAClE,OAAK,cAAc,IAAI,QAAQ,aAAa;;;;;CAM9C,WAAW,QAAsB;AAC/B,OAAK,cAAc,OAAO,OAAO;;;;;;CAOnC,YAAY,MAA2D;AACrE,OAAK,MAAM,CAAC,QAAQ,QAAQ,KAAK,cAC/B,KAAI,KAAK,WAAW,IAAI,MAAM,CAC5B,QAAO,CAAC,QAAQ,IAAI;;;;;;;;CAY1B,MAAM,QACJ,MACA,MAC8D;EAC9D,MAAM,QAAQ,KAAK,YAAY,KAAK;AACpC,MAAI,CAAC,MAAO,QAAO;EAEnB,MAAM,CAAC,QAAQ,OAAO;AACtB,MAAI,CAAC,IAAI,aAAa,CAAC,KAAK,KAAM,QAAO;AAEzC,MAAI;GACF,MAAM,aAAa,MAAM,KAAK,KAAK,IAAI,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC;AAC/D,OAAI,CAAC,WAAW,WAAW,CAAC,WAAW,KAAM,QAAO;GAEpD,MAAM,OAAO,WAAW;AAKxB,UAAO;IACL;IACA,QAAQ;KACN,SAAU,KAAK,WAAwB,EAAE;KACzC,SAAS,KAAK;KACd,WAAW,KAAK;KACjB;IACF;UACK;AACN,UAAO;;;;;;CAOX,OAAmD;AACjD,SAAO,KAAK;;;;;CAMd,IAAI,mBAA4B;AAC9B,SAAO,KAAK,cAAc,OAAO;;;;;CAMnC,QAAc;AACZ,OAAK,cAAc,OAAO"}
@@ -0,0 +1,86 @@
1
+
2
+ //#region src/extraction/path-anchors.ts
3
+ /**
4
+ * Path-derived topic anchor extractor.
5
+ *
6
+ * Promoted from the retrieval bench's hand-rolled helper after it
7
+ * lifted the docs/ corpus from R@5=46.7% (FTS only) to R@5=100%
8
+ * (with these anchors + text-embedding-3-small). The win came from
9
+ * making path segments — already the strongest semantic signal in
10
+ * any docs tree — directly addressable as topic anchors.
11
+ *
12
+ * Strategy:
13
+ *
14
+ * 1. Each `/`-delimited segment is added as a topic. Numeric ADR
15
+ * prefixes (`0001-`) and the `.md` extension get stripped.
16
+ * Example: `docs/adr/0001-afs-top-level-path-categories.md` →
17
+ * `docs`, `adr`, `afs-top-level-path-categories`.
18
+ *
19
+ * 2. Project-jargon prefixes (afs-, aup-, ash-, adr-, aigne-, did-,
20
+ * dev-) are also stripped to yield a second anchor without the
21
+ * prefix — `afs-top-level-path-categories` →
22
+ * `top-level-path-categories`. Lets queries that name the topic
23
+ * without the namespace prefix still match.
24
+ *
25
+ * 3. Trailing 3-4 grams of the stripped form. Specific enough
26
+ * ("aup-surface-family", "level-path-categories") to add real
27
+ * retrieval signal; long enough (≥8 chars) to not crowd out
28
+ * gold matches with generic words. Only trailing because the
29
+ * head of a slug usually IS the namespace prefix already
30
+ * captured above.
31
+ *
32
+ * 4. ADR ids surface as `ADR-0001` so questions that name an ADR
33
+ * pin the right doc deterministically.
34
+ *
35
+ * 5. The first H1 (when content provided) becomes a topic — long
36
+ * titles fall back to the first 80 chars to keep the lookup
37
+ * table compact.
38
+ *
39
+ * Sub-token expansion (1- and 2-grams) was tried earlier and hurt
40
+ * R@1 because every doc shares short tokens like "afs" and "agent" —
41
+ * the over-broad match crowded out specific gold hits.
42
+ */
43
+ const JARGON_PREFIX = /^(afs|aup|ash|adr|aigne|did|dev)-/i;
44
+ /**
45
+ * Extract a single `topic`-typed anchor entry from an AFS path.
46
+ *
47
+ * Returns the canonical shape consumed by `AFSIndex.execIndex`:
48
+ * `[{ type: "topic", values: string[], confidence: 1 }]`. Empty when
49
+ * nothing useful surfaces (e.g. path was just `/` or all segments
50
+ * were stop-words).
51
+ */
52
+ function extractPathTopicAnchors(entryPath, options = {}) {
53
+ const topics = /* @__PURE__ */ new Set();
54
+ const cleanPath = entryPath.replace(/^\/+/, "");
55
+ for (const seg of cleanPath.split("/")) {
56
+ const stripped = seg.replace(/\.md$/, "").replace(/^\d+-/, "");
57
+ if (!stripped || stripped.toLowerCase() === "readme") continue;
58
+ topics.add(stripped);
59
+ const noPrefix = stripped.replace(JARGON_PREFIX, "");
60
+ if (noPrefix !== stripped && noPrefix.length > 0) topics.add(noPrefix);
61
+ const parts = stripped.toLowerCase().split("-").filter(Boolean);
62
+ if (parts.length >= 4) for (let n = 3; n <= 4; n++) for (let i = parts.length - n; i >= 0 && i >= parts.length - 4; i--) {
63
+ const gram = parts.slice(i, i + n).join("-");
64
+ if (gram.length >= 8) topics.add(gram);
65
+ }
66
+ }
67
+ const adrMatch = /\b(\d{4})-/.exec(cleanPath);
68
+ if (adrMatch) topics.add(`ADR-${adrMatch[1]}`);
69
+ if (options.content) {
70
+ const h1 = options.content.match(/^#\s+(.+)$/m);
71
+ if (h1) {
72
+ const title = h1[1].slice(0, 80).trim();
73
+ if (title) topics.add(title);
74
+ }
75
+ }
76
+ if (topics.size === 0) return [];
77
+ const cap = options.maxValues ?? 32;
78
+ return [{
79
+ type: "topic",
80
+ values: [...topics].slice(0, cap),
81
+ confidence: 1
82
+ }];
83
+ }
84
+
85
+ //#endregion
86
+ exports.extractPathTopicAnchors = extractPathTopicAnchors;
@@ -0,0 +1,72 @@
1
+ //#region src/extraction/path-anchors.d.ts
2
+ /**
3
+ * Path-derived topic anchor extractor.
4
+ *
5
+ * Promoted from the retrieval bench's hand-rolled helper after it
6
+ * lifted the docs/ corpus from R@5=46.7% (FTS only) to R@5=100%
7
+ * (with these anchors + text-embedding-3-small). The win came from
8
+ * making path segments — already the strongest semantic signal in
9
+ * any docs tree — directly addressable as topic anchors.
10
+ *
11
+ * Strategy:
12
+ *
13
+ * 1. Each `/`-delimited segment is added as a topic. Numeric ADR
14
+ * prefixes (`0001-`) and the `.md` extension get stripped.
15
+ * Example: `docs/adr/0001-afs-top-level-path-categories.md` →
16
+ * `docs`, `adr`, `afs-top-level-path-categories`.
17
+ *
18
+ * 2. Project-jargon prefixes (afs-, aup-, ash-, adr-, aigne-, did-,
19
+ * dev-) are also stripped to yield a second anchor without the
20
+ * prefix — `afs-top-level-path-categories` →
21
+ * `top-level-path-categories`. Lets queries that name the topic
22
+ * without the namespace prefix still match.
23
+ *
24
+ * 3. Trailing 3-4 grams of the stripped form. Specific enough
25
+ * ("aup-surface-family", "level-path-categories") to add real
26
+ * retrieval signal; long enough (≥8 chars) to not crowd out
27
+ * gold matches with generic words. Only trailing because the
28
+ * head of a slug usually IS the namespace prefix already
29
+ * captured above.
30
+ *
31
+ * 4. ADR ids surface as `ADR-0001` so questions that name an ADR
32
+ * pin the right doc deterministically.
33
+ *
34
+ * 5. The first H1 (when content provided) becomes a topic — long
35
+ * titles fall back to the first 80 chars to keep the lookup
36
+ * table compact.
37
+ *
38
+ * Sub-token expansion (1- and 2-grams) was tried earlier and hurt
39
+ * R@1 because every doc shares short tokens like "afs" and "agent" —
40
+ * the over-broad match crowded out specific gold hits.
41
+ */
42
+ interface PathAnchorOptions {
43
+ /**
44
+ * Optional document body. When supplied, the first H1 line
45
+ * (`# Title`) becomes a topic. Truncated to 80 chars so a long
46
+ * title doesn't blow up the lookup table.
47
+ */
48
+ content?: string;
49
+ /**
50
+ * Maximum number of distinct anchor values to emit. Defaults to 32.
51
+ * The anchor backend stores one row per (entry_path, key), so a
52
+ * very high cap inflates `idx_anchor_key` without proportional recall
53
+ * gain. 32 was the sweet spot in the docs/ corpus bench.
54
+ */
55
+ maxValues?: number;
56
+ }
57
+ /**
58
+ * Extract a single `topic`-typed anchor entry from an AFS path.
59
+ *
60
+ * Returns the canonical shape consumed by `AFSIndex.execIndex`:
61
+ * `[{ type: "topic", values: string[], confidence: 1 }]`. Empty when
62
+ * nothing useful surfaces (e.g. path was just `/` or all segments
63
+ * were stop-words).
64
+ */
65
+ declare function extractPathTopicAnchors(entryPath: string, options?: PathAnchorOptions): Array<{
66
+ type: "topic";
67
+ values: string[];
68
+ confidence: number;
69
+ }>;
70
+ //#endregion
71
+ export { PathAnchorOptions, extractPathTopicAnchors };
72
+ //# sourceMappingURL=path-anchors.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-anchors.d.cts","names":[],"sources":["../../src/extraction/path-anchors.ts"],"mappings":";;AA2CA;;;;;AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAxBiB,iBAAA;;;;;;EAMf,OAAA;;;;;;;EAOA,SAAA;AAAA;;;;;;;;;iBAWc,uBAAA,CACd,SAAA,UACA,OAAA,GAAS,iBAAA,GACR,KAAA;EAAQ,IAAA;EAAe,MAAA;EAAkB,UAAA;AAAA"}
@@ -0,0 +1,72 @@
1
+ //#region src/extraction/path-anchors.d.ts
2
+ /**
3
+ * Path-derived topic anchor extractor.
4
+ *
5
+ * Promoted from the retrieval bench's hand-rolled helper after it
6
+ * lifted the docs/ corpus from R@5=46.7% (FTS only) to R@5=100%
7
+ * (with these anchors + text-embedding-3-small). The win came from
8
+ * making path segments — already the strongest semantic signal in
9
+ * any docs tree — directly addressable as topic anchors.
10
+ *
11
+ * Strategy:
12
+ *
13
+ * 1. Each `/`-delimited segment is added as a topic. Numeric ADR
14
+ * prefixes (`0001-`) and the `.md` extension get stripped.
15
+ * Example: `docs/adr/0001-afs-top-level-path-categories.md` →
16
+ * `docs`, `adr`, `afs-top-level-path-categories`.
17
+ *
18
+ * 2. Project-jargon prefixes (afs-, aup-, ash-, adr-, aigne-, did-,
19
+ * dev-) are also stripped to yield a second anchor without the
20
+ * prefix — `afs-top-level-path-categories` →
21
+ * `top-level-path-categories`. Lets queries that name the topic
22
+ * without the namespace prefix still match.
23
+ *
24
+ * 3. Trailing 3-4 grams of the stripped form. Specific enough
25
+ * ("aup-surface-family", "level-path-categories") to add real
26
+ * retrieval signal; long enough (≥8 chars) to not crowd out
27
+ * gold matches with generic words. Only trailing because the
28
+ * head of a slug usually IS the namespace prefix already
29
+ * captured above.
30
+ *
31
+ * 4. ADR ids surface as `ADR-0001` so questions that name an ADR
32
+ * pin the right doc deterministically.
33
+ *
34
+ * 5. The first H1 (when content provided) becomes a topic — long
35
+ * titles fall back to the first 80 chars to keep the lookup
36
+ * table compact.
37
+ *
38
+ * Sub-token expansion (1- and 2-grams) was tried earlier and hurt
39
+ * R@1 because every doc shares short tokens like "afs" and "agent" —
40
+ * the over-broad match crowded out specific gold hits.
41
+ */
42
+ interface PathAnchorOptions {
43
+ /**
44
+ * Optional document body. When supplied, the first H1 line
45
+ * (`# Title`) becomes a topic. Truncated to 80 chars so a long
46
+ * title doesn't blow up the lookup table.
47
+ */
48
+ content?: string;
49
+ /**
50
+ * Maximum number of distinct anchor values to emit. Defaults to 32.
51
+ * The anchor backend stores one row per (entry_path, key), so a
52
+ * very high cap inflates `idx_anchor_key` without proportional recall
53
+ * gain. 32 was the sweet spot in the docs/ corpus bench.
54
+ */
55
+ maxValues?: number;
56
+ }
57
+ /**
58
+ * Extract a single `topic`-typed anchor entry from an AFS path.
59
+ *
60
+ * Returns the canonical shape consumed by `AFSIndex.execIndex`:
61
+ * `[{ type: "topic", values: string[], confidence: 1 }]`. Empty when
62
+ * nothing useful surfaces (e.g. path was just `/` or all segments
63
+ * were stop-words).
64
+ */
65
+ declare function extractPathTopicAnchors(entryPath: string, options?: PathAnchorOptions): Array<{
66
+ type: "topic";
67
+ values: string[];
68
+ confidence: number;
69
+ }>;
70
+ //#endregion
71
+ export { PathAnchorOptions, extractPathTopicAnchors };
72
+ //# sourceMappingURL=path-anchors.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-anchors.d.mts","names":[],"sources":["../../src/extraction/path-anchors.ts"],"mappings":";;AA2CA;;;;;AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAxBiB,iBAAA;;;;;;EAMf,OAAA;;;;;;;EAOA,SAAA;AAAA;;;;;;;;;iBAWc,uBAAA,CACd,SAAA,UACA,OAAA,GAAS,iBAAA,GACR,KAAA;EAAQ,IAAA;EAAe,MAAA;EAAkB,UAAA;AAAA"}
@@ -0,0 +1,86 @@
1
+ //#region src/extraction/path-anchors.ts
2
+ /**
3
+ * Path-derived topic anchor extractor.
4
+ *
5
+ * Promoted from the retrieval bench's hand-rolled helper after it
6
+ * lifted the docs/ corpus from R@5=46.7% (FTS only) to R@5=100%
7
+ * (with these anchors + text-embedding-3-small). The win came from
8
+ * making path segments — already the strongest semantic signal in
9
+ * any docs tree — directly addressable as topic anchors.
10
+ *
11
+ * Strategy:
12
+ *
13
+ * 1. Each `/`-delimited segment is added as a topic. Numeric ADR
14
+ * prefixes (`0001-`) and the `.md` extension get stripped.
15
+ * Example: `docs/adr/0001-afs-top-level-path-categories.md` →
16
+ * `docs`, `adr`, `afs-top-level-path-categories`.
17
+ *
18
+ * 2. Project-jargon prefixes (afs-, aup-, ash-, adr-, aigne-, did-,
19
+ * dev-) are also stripped to yield a second anchor without the
20
+ * prefix — `afs-top-level-path-categories` →
21
+ * `top-level-path-categories`. Lets queries that name the topic
22
+ * without the namespace prefix still match.
23
+ *
24
+ * 3. Trailing 3-4 grams of the stripped form. Specific enough
25
+ * ("aup-surface-family", "level-path-categories") to add real
26
+ * retrieval signal; long enough (≥8 chars) to not crowd out
27
+ * gold matches with generic words. Only trailing because the
28
+ * head of a slug usually IS the namespace prefix already
29
+ * captured above.
30
+ *
31
+ * 4. ADR ids surface as `ADR-0001` so questions that name an ADR
32
+ * pin the right doc deterministically.
33
+ *
34
+ * 5. The first H1 (when content provided) becomes a topic — long
35
+ * titles fall back to the first 80 chars to keep the lookup
36
+ * table compact.
37
+ *
38
+ * Sub-token expansion (1- and 2-grams) was tried earlier and hurt
39
+ * R@1 because every doc shares short tokens like "afs" and "agent" —
40
+ * the over-broad match crowded out specific gold hits.
41
+ */
42
+ const JARGON_PREFIX = /^(afs|aup|ash|adr|aigne|did|dev)-/i;
43
+ /**
44
+ * Extract a single `topic`-typed anchor entry from an AFS path.
45
+ *
46
+ * Returns the canonical shape consumed by `AFSIndex.execIndex`:
47
+ * `[{ type: "topic", values: string[], confidence: 1 }]`. Empty when
48
+ * nothing useful surfaces (e.g. path was just `/` or all segments
49
+ * were stop-words).
50
+ */
51
+ function extractPathTopicAnchors(entryPath, options = {}) {
52
+ const topics = /* @__PURE__ */ new Set();
53
+ const cleanPath = entryPath.replace(/^\/+/, "");
54
+ for (const seg of cleanPath.split("/")) {
55
+ const stripped = seg.replace(/\.md$/, "").replace(/^\d+-/, "");
56
+ if (!stripped || stripped.toLowerCase() === "readme") continue;
57
+ topics.add(stripped);
58
+ const noPrefix = stripped.replace(JARGON_PREFIX, "");
59
+ if (noPrefix !== stripped && noPrefix.length > 0) topics.add(noPrefix);
60
+ const parts = stripped.toLowerCase().split("-").filter(Boolean);
61
+ if (parts.length >= 4) for (let n = 3; n <= 4; n++) for (let i = parts.length - n; i >= 0 && i >= parts.length - 4; i--) {
62
+ const gram = parts.slice(i, i + n).join("-");
63
+ if (gram.length >= 8) topics.add(gram);
64
+ }
65
+ }
66
+ const adrMatch = /\b(\d{4})-/.exec(cleanPath);
67
+ if (adrMatch) topics.add(`ADR-${adrMatch[1]}`);
68
+ if (options.content) {
69
+ const h1 = options.content.match(/^#\s+(.+)$/m);
70
+ if (h1) {
71
+ const title = h1[1].slice(0, 80).trim();
72
+ if (title) topics.add(title);
73
+ }
74
+ }
75
+ if (topics.size === 0) return [];
76
+ const cap = options.maxValues ?? 32;
77
+ return [{
78
+ type: "topic",
79
+ values: [...topics].slice(0, cap),
80
+ confidence: 1
81
+ }];
82
+ }
83
+
84
+ //#endregion
85
+ export { extractPathTopicAnchors };
86
+ //# sourceMappingURL=path-anchors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-anchors.mjs","names":[],"sources":["../../src/extraction/path-anchors.ts"],"sourcesContent":["/**\n * Path-derived topic anchor extractor.\n *\n * Promoted from the retrieval bench's hand-rolled helper after it\n * lifted the docs/ corpus from R@5=46.7% (FTS only) to R@5=100%\n * (with these anchors + text-embedding-3-small). The win came from\n * making path segments — already the strongest semantic signal in\n * any docs tree — directly addressable as topic anchors.\n *\n * Strategy:\n *\n * 1. Each `/`-delimited segment is added as a topic. Numeric ADR\n * prefixes (`0001-`) and the `.md` extension get stripped.\n * Example: `docs/adr/0001-afs-top-level-path-categories.md` →\n * `docs`, `adr`, `afs-top-level-path-categories`.\n *\n * 2. Project-jargon prefixes (afs-, aup-, ash-, adr-, aigne-, did-,\n * dev-) are also stripped to yield a second anchor without the\n * prefix — `afs-top-level-path-categories` →\n * `top-level-path-categories`. Lets queries that name the topic\n * without the namespace prefix still match.\n *\n * 3. Trailing 3-4 grams of the stripped form. Specific enough\n * (\"aup-surface-family\", \"level-path-categories\") to add real\n * retrieval signal; long enough (≥8 chars) to not crowd out\n * gold matches with generic words. Only trailing because the\n * head of a slug usually IS the namespace prefix already\n * captured above.\n *\n * 4. ADR ids surface as `ADR-0001` so questions that name an ADR\n * pin the right doc deterministically.\n *\n * 5. The first H1 (when content provided) becomes a topic — long\n * titles fall back to the first 80 chars to keep the lookup\n * table compact.\n *\n * Sub-token expansion (1- and 2-grams) was tried earlier and hurt\n * R@1 because every doc shares short tokens like \"afs\" and \"agent\" —\n * the over-broad match crowded out specific gold hits.\n */\n\nconst JARGON_PREFIX = /^(afs|aup|ash|adr|aigne|did|dev)-/i;\n\nexport interface PathAnchorOptions {\n /**\n * Optional document body. When supplied, the first H1 line\n * (`# Title`) becomes a topic. Truncated to 80 chars so a long\n * title doesn't blow up the lookup table.\n */\n content?: string;\n /**\n * Maximum number of distinct anchor values to emit. Defaults to 32.\n * The anchor backend stores one row per (entry_path, key), so a\n * very high cap inflates `idx_anchor_key` without proportional recall\n * gain. 32 was the sweet spot in the docs/ corpus bench.\n */\n maxValues?: number;\n}\n\n/**\n * Extract a single `topic`-typed anchor entry from an AFS path.\n *\n * Returns the canonical shape consumed by `AFSIndex.execIndex`:\n * `[{ type: \"topic\", values: string[], confidence: 1 }]`. Empty when\n * nothing useful surfaces (e.g. path was just `/` or all segments\n * were stop-words).\n */\nexport function extractPathTopicAnchors(\n entryPath: string,\n options: PathAnchorOptions = {},\n): Array<{ type: \"topic\"; values: string[]; confidence: number }> {\n const topics = new Set<string>();\n const cleanPath = entryPath.replace(/^\\/+/, \"\");\n\n for (const seg of cleanPath.split(\"/\")) {\n const stripped = seg.replace(/\\.md$/, \"\").replace(/^\\d+-/, \"\");\n if (!stripped || stripped.toLowerCase() === \"readme\") continue;\n\n topics.add(stripped);\n\n const noPrefix = stripped.replace(JARGON_PREFIX, \"\");\n if (noPrefix !== stripped && noPrefix.length > 0) topics.add(noPrefix);\n\n const parts = stripped.toLowerCase().split(\"-\").filter(Boolean);\n if (parts.length >= 4) {\n for (let n = 3; n <= 4; n++) {\n for (let i = parts.length - n; i >= 0 && i >= parts.length - 4; i--) {\n const gram = parts.slice(i, i + n).join(\"-\");\n if (gram.length >= 8) topics.add(gram);\n }\n }\n }\n }\n\n const adrMatch = /\\b(\\d{4})-/.exec(cleanPath);\n if (adrMatch) topics.add(`ADR-${adrMatch[1]}`);\n\n if (options.content) {\n const h1 = options.content.match(/^#\\s+(.+)$/m);\n if (h1) {\n const title = h1[1]!.slice(0, 80).trim();\n if (title) topics.add(title);\n }\n }\n\n if (topics.size === 0) return [];\n\n const cap = options.maxValues ?? 32;\n return [\n {\n type: \"topic\",\n values: [...topics].slice(0, cap),\n confidence: 1,\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAM,gBAAgB;;;;;;;;;AA0BtB,SAAgB,wBACd,WACA,UAA6B,EAAE,EACiC;CAChE,MAAM,yBAAS,IAAI,KAAa;CAChC,MAAM,YAAY,UAAU,QAAQ,QAAQ,GAAG;AAE/C,MAAK,MAAM,OAAO,UAAU,MAAM,IAAI,EAAE;EACtC,MAAM,WAAW,IAAI,QAAQ,SAAS,GAAG,CAAC,QAAQ,SAAS,GAAG;AAC9D,MAAI,CAAC,YAAY,SAAS,aAAa,KAAK,SAAU;AAEtD,SAAO,IAAI,SAAS;EAEpB,MAAM,WAAW,SAAS,QAAQ,eAAe,GAAG;AACpD,MAAI,aAAa,YAAY,SAAS,SAAS,EAAG,QAAO,IAAI,SAAS;EAEtE,MAAM,QAAQ,SAAS,aAAa,CAAC,MAAM,IAAI,CAAC,OAAO,QAAQ;AAC/D,MAAI,MAAM,UAAU,EAClB,MAAK,IAAI,IAAI,GAAG,KAAK,GAAG,IACtB,MAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG,KAAK;GACnE,MAAM,OAAO,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI;AAC5C,OAAI,KAAK,UAAU,EAAG,QAAO,IAAI,KAAK;;;CAM9C,MAAM,WAAW,aAAa,KAAK,UAAU;AAC7C,KAAI,SAAU,QAAO,IAAI,OAAO,SAAS,KAAK;AAE9C,KAAI,QAAQ,SAAS;EACnB,MAAM,KAAK,QAAQ,QAAQ,MAAM,cAAc;AAC/C,MAAI,IAAI;GACN,MAAM,QAAQ,GAAG,GAAI,MAAM,GAAG,GAAG,CAAC,MAAM;AACxC,OAAI,MAAO,QAAO,IAAI,MAAM;;;AAIhC,KAAI,OAAO,SAAS,EAAG,QAAO,EAAE;CAEhC,MAAM,MAAM,QAAQ,aAAa;AACjC,QAAO,CACL;EACE,MAAM;EACN,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI;EACjC,YAAY;EACb,CACF"}
@@ -0,0 +1,92 @@
1
+
2
+ //#region src/follower-scheduler.ts
3
+ function clampSpent(value, remaining) {
4
+ if (!Number.isFinite(value ?? 0) || (value ?? 0) <= 0) return 0;
5
+ return Math.min(value ?? 0, remaining);
6
+ }
7
+ /**
8
+ * Fairly spend one index-follower tick across a selected scope set.
9
+ *
10
+ * Each scope receives a small bounded slice first; scopes that still report
11
+ * `hasMore` are appended to the tail and can receive another slice only after
12
+ * the other selected scopes had their chance. The helper is intentionally
13
+ * storage/runtime-agnostic so Cloudflare and Node share the same starvation
14
+ * semantics while keeping different budget sizes.
15
+ */
16
+ async function runIndexFollowerRoundRobin(opts) {
17
+ let remainingProcessed = Math.max(0, opts.maxProcessed);
18
+ let remainingNoop = Math.max(0, opts.maxNoop);
19
+ let remainingScan = Math.max(0, opts.maxScan);
20
+ const perScopeMaxProcessed = Math.max(0, opts.perScopeMaxProcessed ?? 1);
21
+ const perScopeMaxNoop = Math.max(0, opts.perScopeMaxNoop ?? 2);
22
+ const perScopeMaxScan = Math.max(1, opts.perScopeMaxScan ?? 50);
23
+ const maxRuns = opts.maxRuns ?? opts.scopes.length + Math.ceil((remainingProcessed || 0) / Math.max(1, perScopeMaxProcessed || 1)) + Math.ceil((remainingNoop || 0) / Math.max(1, perScopeMaxNoop || 1)) + Math.ceil((remainingScan || 0) / Math.max(1, perScopeMaxScan));
24
+ const queue = [...opts.scopes];
25
+ const runs = [];
26
+ let indexedThisTick = 0;
27
+ let deletedThisTick = 0;
28
+ let noopedThisTick = 0;
29
+ let ackRaceThisTick = 0;
30
+ let stoppedBy = "drained";
31
+ let sawBudgetBlockedScope = false;
32
+ while (queue.length > 0) {
33
+ if (opts.shouldStop?.()) {
34
+ stoppedBy = "time";
35
+ break;
36
+ }
37
+ if (runs.length >= maxRuns) {
38
+ stoppedBy = "max_runs";
39
+ break;
40
+ }
41
+ if (remainingScan <= 0 || remainingProcessed <= 0 && remainingNoop <= 0) {
42
+ stoppedBy = "budget";
43
+ break;
44
+ }
45
+ const scope = queue.shift();
46
+ const limits = {
47
+ maxProcessed: Math.min(perScopeMaxProcessed, remainingProcessed),
48
+ maxNoop: Math.min(perScopeMaxNoop, remainingNoop),
49
+ maxScan: Math.min(perScopeMaxScan, remainingScan)
50
+ };
51
+ if (limits.maxScan <= 0 || limits.maxProcessed <= 0 && limits.maxNoop <= 0) {
52
+ stoppedBy = "budget";
53
+ break;
54
+ }
55
+ const result = await opts.runScope(scope, limits);
56
+ runs.push({
57
+ scope,
58
+ limits,
59
+ result
60
+ });
61
+ remainingProcessed -= clampSpent(result.processed, remainingProcessed);
62
+ remainingNoop -= clampSpent(result.noopProcessed, remainingNoop);
63
+ remainingScan -= clampSpent(result.scanned, remainingScan);
64
+ indexedThisTick += result.indexed;
65
+ deletedThisTick += result.deleted;
66
+ noopedThisTick += result.nooped;
67
+ if (result.ackRace) ackRaceThisTick++;
68
+ if (!result.advanced && result.hasMore && [
69
+ "processed_budget",
70
+ "noop_budget",
71
+ "scan_budget"
72
+ ].includes(result.stopReason ?? "")) sawBudgetBlockedScope = true;
73
+ if (result.advanced && result.hasMore && !result.aborted && remainingScan > 0 && (remainingProcessed > 0 || remainingNoop > 0)) queue.push(scope);
74
+ }
75
+ return {
76
+ runs,
77
+ processedThisTick: opts.maxProcessed - remainingProcessed,
78
+ noopThisTick: opts.maxNoop - remainingNoop,
79
+ indexedThisTick,
80
+ deletedThisTick,
81
+ noopedThisTick,
82
+ scannedThisTick: opts.maxScan - remainingScan,
83
+ ackRaceThisTick,
84
+ stoppedBy: stoppedBy === "drained" && sawBudgetBlockedScope ? "budget" : stoppedBy,
85
+ remainingProcessed,
86
+ remainingNoop,
87
+ remainingScan
88
+ };
89
+ }
90
+
91
+ //#endregion
92
+ exports.runIndexFollowerRoundRobin = runIndexFollowerRoundRobin;
@@ -0,0 +1,58 @@
1
+ import { IndexFollowerBatchResult } from "./follower.cjs";
2
+
3
+ //#region src/follower-scheduler.d.ts
4
+ interface IndexFollowerRunLimits {
5
+ maxProcessed: number;
6
+ maxNoop: number;
7
+ maxScan: number;
8
+ }
9
+ interface IndexFollowerRoundRobinOptions<T> {
10
+ scopes: T[];
11
+ maxProcessed: number;
12
+ maxNoop: number;
13
+ maxScan: number;
14
+ perScopeMaxProcessed?: number;
15
+ perScopeMaxNoop?: number;
16
+ perScopeMaxScan?: number;
17
+ maxRuns?: number;
18
+ shouldStop?: () => boolean;
19
+ runScope: (scope: T, limits: IndexFollowerRunLimits) => Promise<IndexFollowerBatchResult>;
20
+ }
21
+ interface IndexFollowerRoundRobinRun<T> {
22
+ scope: T;
23
+ limits: IndexFollowerRunLimits;
24
+ result: IndexFollowerBatchResult;
25
+ }
26
+ interface IndexFollowerRoundRobinResult<T> {
27
+ runs: IndexFollowerRoundRobinRun<T>[];
28
+ processedThisTick: number;
29
+ noopThisTick: number;
30
+ indexedThisTick: number;
31
+ deletedThisTick: number;
32
+ noopedThisTick: number;
33
+ scannedThisTick: number;
34
+ /**
35
+ * Drain RUNS that lost the ack race-guard this tick (healthy concurrent-write
36
+ * churn, not failures). Counted per `runScope` call, so a single hot scope
37
+ * requeued within the tick can contribute more than once — this is a run
38
+ * occurrence count, not a distinct-scope count.
39
+ */
40
+ ackRaceThisTick: number;
41
+ stoppedBy: "drained" | "budget" | "max_runs" | "time";
42
+ remainingProcessed: number;
43
+ remainingNoop: number;
44
+ remainingScan: number;
45
+ }
46
+ /**
47
+ * Fairly spend one index-follower tick across a selected scope set.
48
+ *
49
+ * Each scope receives a small bounded slice first; scopes that still report
50
+ * `hasMore` are appended to the tail and can receive another slice only after
51
+ * the other selected scopes had their chance. The helper is intentionally
52
+ * storage/runtime-agnostic so Cloudflare and Node share the same starvation
53
+ * semantics while keeping different budget sizes.
54
+ */
55
+ declare function runIndexFollowerRoundRobin<T>(opts: IndexFollowerRoundRobinOptions<T>): Promise<IndexFollowerRoundRobinResult<T>>;
56
+ //#endregion
57
+ export { IndexFollowerRoundRobinOptions, IndexFollowerRoundRobinResult, IndexFollowerRoundRobinRun, IndexFollowerRunLimits, runIndexFollowerRoundRobin };
58
+ //# sourceMappingURL=follower-scheduler.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follower-scheduler.d.cts","names":[],"sources":["../src/follower-scheduler.ts"],"mappings":";;;UAMiB,sBAAA;EACf,YAAA;EACA,OAAA;EACA,OAAA;AAAA;AAAA,UAGe,8BAAA;EACf,MAAA,EAAQ,CAAA;EACR,YAAA;EACA,OAAA;EACA,OAAA;EACA,oBAAA;EACA,eAAA;EACA,eAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA,GAAW,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,sBAAA,KAA2B,OAAA,CAAQ,wBAAA;AAAA;AAAA,UAGjD,0BAAA;EACf,KAAA,EAAO,CAAA;EACP,MAAA,EAAQ,sBAAA;EACR,MAAA,EAAQ,wBAAA;AAAA;AAAA,UAGO,6BAAA;EACf,IAAA,EAAM,0BAAA,CAA2B,CAAA;EACjC,iBAAA;EACA,YAAA;EACA,eAAA;EACA,eAAA;EACA,cAAA;EACA,eAAA;EAlBA;;;;;;EAyBA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,aAAA;EACA,aAAA;AAAA;;;;;;;;;;iBAiBoB,0BAAA,GAAA,CACpB,IAAA,EAAM,8BAAA,CAA+B,CAAA,IACpC,OAAA,CAAQ,6BAAA,CAA8B,CAAA"}
@@ -0,0 +1,58 @@
1
+ import { IndexFollowerBatchResult } from "./follower.mjs";
2
+
3
+ //#region src/follower-scheduler.d.ts
4
+ interface IndexFollowerRunLimits {
5
+ maxProcessed: number;
6
+ maxNoop: number;
7
+ maxScan: number;
8
+ }
9
+ interface IndexFollowerRoundRobinOptions<T> {
10
+ scopes: T[];
11
+ maxProcessed: number;
12
+ maxNoop: number;
13
+ maxScan: number;
14
+ perScopeMaxProcessed?: number;
15
+ perScopeMaxNoop?: number;
16
+ perScopeMaxScan?: number;
17
+ maxRuns?: number;
18
+ shouldStop?: () => boolean;
19
+ runScope: (scope: T, limits: IndexFollowerRunLimits) => Promise<IndexFollowerBatchResult>;
20
+ }
21
+ interface IndexFollowerRoundRobinRun<T> {
22
+ scope: T;
23
+ limits: IndexFollowerRunLimits;
24
+ result: IndexFollowerBatchResult;
25
+ }
26
+ interface IndexFollowerRoundRobinResult<T> {
27
+ runs: IndexFollowerRoundRobinRun<T>[];
28
+ processedThisTick: number;
29
+ noopThisTick: number;
30
+ indexedThisTick: number;
31
+ deletedThisTick: number;
32
+ noopedThisTick: number;
33
+ scannedThisTick: number;
34
+ /**
35
+ * Drain RUNS that lost the ack race-guard this tick (healthy concurrent-write
36
+ * churn, not failures). Counted per `runScope` call, so a single hot scope
37
+ * requeued within the tick can contribute more than once — this is a run
38
+ * occurrence count, not a distinct-scope count.
39
+ */
40
+ ackRaceThisTick: number;
41
+ stoppedBy: "drained" | "budget" | "max_runs" | "time";
42
+ remainingProcessed: number;
43
+ remainingNoop: number;
44
+ remainingScan: number;
45
+ }
46
+ /**
47
+ * Fairly spend one index-follower tick across a selected scope set.
48
+ *
49
+ * Each scope receives a small bounded slice first; scopes that still report
50
+ * `hasMore` are appended to the tail and can receive another slice only after
51
+ * the other selected scopes had their chance. The helper is intentionally
52
+ * storage/runtime-agnostic so Cloudflare and Node share the same starvation
53
+ * semantics while keeping different budget sizes.
54
+ */
55
+ declare function runIndexFollowerRoundRobin<T>(opts: IndexFollowerRoundRobinOptions<T>): Promise<IndexFollowerRoundRobinResult<T>>;
56
+ //#endregion
57
+ export { IndexFollowerRoundRobinOptions, IndexFollowerRoundRobinResult, IndexFollowerRoundRobinRun, IndexFollowerRunLimits, runIndexFollowerRoundRobin };
58
+ //# sourceMappingURL=follower-scheduler.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follower-scheduler.d.mts","names":[],"sources":["../src/follower-scheduler.ts"],"mappings":";;;UAMiB,sBAAA;EACf,YAAA;EACA,OAAA;EACA,OAAA;AAAA;AAAA,UAGe,8BAAA;EACf,MAAA,EAAQ,CAAA;EACR,YAAA;EACA,OAAA;EACA,OAAA;EACA,oBAAA;EACA,eAAA;EACA,eAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA,GAAW,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,sBAAA,KAA2B,OAAA,CAAQ,wBAAA;AAAA;AAAA,UAGjD,0BAAA;EACf,KAAA,EAAO,CAAA;EACP,MAAA,EAAQ,sBAAA;EACR,MAAA,EAAQ,wBAAA;AAAA;AAAA,UAGO,6BAAA;EACf,IAAA,EAAM,0BAAA,CAA2B,CAAA;EACjC,iBAAA;EACA,YAAA;EACA,eAAA;EACA,eAAA;EACA,cAAA;EACA,eAAA;EAlBA;;;;;;EAyBA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,aAAA;EACA,aAAA;AAAA;;;;;;;;;;iBAiBoB,0BAAA,GAAA,CACpB,IAAA,EAAM,8BAAA,CAA+B,CAAA,IACpC,OAAA,CAAQ,6BAAA,CAA8B,CAAA"}