@kolisachint/hoocode-agent 0.4.163 → 0.4.165

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 (164) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/cli/args.d.ts +0 -6
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +0 -22
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-frontmatter.d.ts.map +1 -1
  7. package/dist/core/agent-frontmatter.js +0 -6
  8. package/dist/core/agent-frontmatter.js.map +1 -1
  9. package/dist/core/agent-session-services.d.ts +0 -2
  10. package/dist/core/agent-session-services.d.ts.map +1 -1
  11. package/dist/core/agent-session-services.js +0 -2
  12. package/dist/core/agent-session-services.js.map +1 -1
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +0 -2
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/embsearch/client.d.ts +18 -2
  17. package/dist/core/embsearch/client.d.ts.map +1 -1
  18. package/dist/core/embsearch/client.js +20 -3
  19. package/dist/core/embsearch/client.js.map +1 -1
  20. package/dist/core/embsearch/embsearch-service.d.ts +18 -1
  21. package/dist/core/embsearch/embsearch-service.d.ts.map +1 -1
  22. package/dist/core/embsearch/embsearch-service.js +52 -4
  23. package/dist/core/embsearch/embsearch-service.js.map +1 -1
  24. package/dist/core/sdk.d.ts +0 -16
  25. package/dist/core/sdk.d.ts.map +1 -1
  26. package/dist/core/sdk.js +0 -2
  27. package/dist/core/sdk.js.map +1 -1
  28. package/dist/core/search/eval-compare.d.ts +57 -0
  29. package/dist/core/search/eval-compare.d.ts.map +1 -0
  30. package/dist/core/search/eval-compare.js +114 -0
  31. package/dist/core/search/eval-compare.js.map +1 -0
  32. package/dist/core/search/eval-gold.d.ts +47 -0
  33. package/dist/core/search/eval-gold.d.ts.map +1 -0
  34. package/dist/core/search/eval-gold.js +172 -0
  35. package/dist/core/search/eval-gold.js.map +1 -0
  36. package/dist/core/search/eval-harness.d.ts +140 -0
  37. package/dist/core/search/eval-harness.d.ts.map +1 -0
  38. package/dist/core/search/eval-harness.js +225 -0
  39. package/dist/core/search/eval-harness.js.map +1 -0
  40. package/dist/core/search/eval.d.ts +66 -8
  41. package/dist/core/search/eval.d.ts.map +1 -1
  42. package/dist/core/search/eval.js +67 -12
  43. package/dist/core/search/eval.js.map +1 -1
  44. package/dist/core/search/hybrid-search.d.ts +16 -0
  45. package/dist/core/search/hybrid-search.d.ts.map +1 -1
  46. package/dist/core/search/hybrid-search.js +54 -2
  47. package/dist/core/search/hybrid-search.js.map +1 -1
  48. package/dist/core/search/mode.d.ts +21 -5
  49. package/dist/core/search/mode.d.ts.map +1 -1
  50. package/dist/core/search/mode.js +23 -10
  51. package/dist/core/search/mode.js.map +1 -1
  52. package/dist/core/search/rerank.d.ts.map +1 -1
  53. package/dist/core/search/rerank.js +70 -11
  54. package/dist/core/search/rerank.js.map +1 -1
  55. package/dist/core/search/rrf.d.ts +19 -6
  56. package/dist/core/search/rrf.d.ts.map +1 -1
  57. package/dist/core/search/rrf.js +19 -6
  58. package/dist/core/search/rrf.js.map +1 -1
  59. package/dist/core/search/types.d.ts +11 -1
  60. package/dist/core/search/types.d.ts.map +1 -1
  61. package/dist/core/search/types.js.map +1 -1
  62. package/dist/core/settings-defaults.d.ts +0 -3
  63. package/dist/core/settings-defaults.d.ts.map +1 -1
  64. package/dist/core/settings-defaults.js +0 -3
  65. package/dist/core/settings-defaults.js.map +1 -1
  66. package/dist/core/settings-manager.d.ts +0 -6
  67. package/dist/core/settings-manager.d.ts.map +1 -1
  68. package/dist/core/settings-manager.js +0 -24
  69. package/dist/core/settings-manager.js.map +1 -1
  70. package/dist/core/settings-types.d.ts +0 -3
  71. package/dist/core/settings-types.d.ts.map +1 -1
  72. package/dist/core/settings-types.js.map +1 -1
  73. package/dist/core/tools/bash.d.ts.map +1 -1
  74. package/dist/core/tools/bash.js +3 -1
  75. package/dist/core/tools/bash.js.map +1 -1
  76. package/dist/core/tools/edit.d.ts.map +1 -1
  77. package/dist/core/tools/edit.js +5 -5
  78. package/dist/core/tools/edit.js.map +1 -1
  79. package/dist/core/tools/find.d.ts.map +1 -1
  80. package/dist/core/tools/find.js +8 -10
  81. package/dist/core/tools/find.js.map +1 -1
  82. package/dist/core/tools/grep.d.ts.map +1 -1
  83. package/dist/core/tools/grep.js +4 -4
  84. package/dist/core/tools/grep.js.map +1 -1
  85. package/dist/core/tools/index.d.ts +0 -20
  86. package/dist/core/tools/index.d.ts.map +1 -1
  87. package/dist/core/tools/index.js +0 -13
  88. package/dist/core/tools/index.js.map +1 -1
  89. package/dist/core/tools/ls.d.ts.map +1 -1
  90. package/dist/core/tools/ls.js +2 -2
  91. package/dist/core/tools/ls.js.map +1 -1
  92. package/dist/core/tools/read.d.ts.map +1 -1
  93. package/dist/core/tools/read.js +10 -8
  94. package/dist/core/tools/read.js.map +1 -1
  95. package/dist/core/tools/search.d.ts.map +1 -1
  96. package/dist/core/tools/search.js +6 -2
  97. package/dist/core/tools/search.js.map +1 -1
  98. package/dist/main.d.ts.map +1 -1
  99. package/dist/main.js +0 -30
  100. package/dist/main.js.map +1 -1
  101. package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
  102. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/settings-selector.js +2 -2
  104. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  105. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.js +0 -18
  107. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/dist/utils/tools-manager.d.ts +1 -1
  109. package/dist/utils/tools-manager.d.ts.map +1 -1
  110. package/dist/utils/tools-manager.js +1 -49
  111. package/dist/utils/tools-manager.js.map +1 -1
  112. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  113. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  114. package/examples/extensions/sandbox/package.json +1 -1
  115. package/examples/extensions/with-deps/package.json +1 -1
  116. package/package.json +7 -4
  117. package/dist/core/tools/browser/browser-continue.d.ts +0 -24
  118. package/dist/core/tools/browser/browser-continue.d.ts.map +0 -1
  119. package/dist/core/tools/browser/browser-continue.js +0 -84
  120. package/dist/core/tools/browser/browser-continue.js.map +0 -1
  121. package/dist/core/tools/browser/browser-run.d.ts +0 -47
  122. package/dist/core/tools/browser/browser-run.d.ts.map +0 -1
  123. package/dist/core/tools/browser/browser-run.js +0 -377
  124. package/dist/core/tools/browser/browser-run.js.map +0 -1
  125. package/dist/core/tools/browser/browsertools-shared.d.ts +0 -177
  126. package/dist/core/tools/browser/browsertools-shared.d.ts.map +0 -1
  127. package/dist/core/tools/browser/browsertools-shared.js +0 -335
  128. package/dist/core/tools/browser/browsertools-shared.js.map +0 -1
  129. package/dist/core/tools/browser/index.d.ts +0 -8
  130. package/dist/core/tools/browser/index.d.ts.map +0 -1
  131. package/dist/core/tools/browser/index.js +0 -8
  132. package/dist/core/tools/browser/index.js.map +0 -1
  133. package/dist/core/tools/doc/docedit.d.ts +0 -43
  134. package/dist/core/tools/doc/docedit.d.ts.map +0 -1
  135. package/dist/core/tools/doc/docedit.js +0 -133
  136. package/dist/core/tools/doc/docedit.js.map +0 -1
  137. package/dist/core/tools/doc/docgrep.d.ts +0 -27
  138. package/dist/core/tools/doc/docgrep.d.ts.map +0 -1
  139. package/dist/core/tools/doc/docgrep.js +0 -106
  140. package/dist/core/tools/doc/docgrep.js.map +0 -1
  141. package/dist/core/tools/doc/docpeek.d.ts +0 -24
  142. package/dist/core/tools/doc/docpeek.d.ts.map +0 -1
  143. package/dist/core/tools/doc/docpeek.js +0 -112
  144. package/dist/core/tools/doc/docpeek.js.map +0 -1
  145. package/dist/core/tools/doc/docread.d.ts +0 -31
  146. package/dist/core/tools/doc/docread.d.ts.map +0 -1
  147. package/dist/core/tools/doc/docread.js +0 -113
  148. package/dist/core/tools/doc/docread.js.map +0 -1
  149. package/dist/core/tools/doc/docscan.d.ts +0 -28
  150. package/dist/core/tools/doc/docscan.d.ts.map +0 -1
  151. package/dist/core/tools/doc/docscan.js +0 -110
  152. package/dist/core/tools/doc/docscan.js.map +0 -1
  153. package/dist/core/tools/doc/docwrite.d.ts +0 -43
  154. package/dist/core/tools/doc/docwrite.d.ts.map +0 -1
  155. package/dist/core/tools/doc/docwrite.js +0 -102
  156. package/dist/core/tools/doc/docwrite.js.map +0 -1
  157. package/dist/core/tools/doc/index.d.ts +0 -11
  158. package/dist/core/tools/doc/index.d.ts.map +0 -1
  159. package/dist/core/tools/doc/index.js +0 -11
  160. package/dist/core/tools/doc/index.js.map +0 -1
  161. package/dist/core/tools/filetools-shared.d.ts +0 -268
  162. package/dist/core/tools/filetools-shared.d.ts.map +0 -1
  163. package/dist/core/tools/filetools-shared.js +0 -403
  164. package/dist/core/tools/filetools-shared.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../../../src/core/search/mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,kBAAkB,CAAC;IACzB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;yEAIyE;AACzE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAI9D;AAED,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,UAAU,EACrB,cAAc,EAAE,OAAO,EACvB,sBAAsB,CAAC,EAAE,MAAM,GAC7B,cAAc,CAahB","sourcesContent":["/**\n * Availability-first search mode resolution\n * (docs/hybrid-retrieval-design.md, Decision 4).\n *\n * No clever query router: with a hot local daemon, running both retrievers\n * costs one extra embedding query, while misrouting costs recall. `auto`\n * therefore means hybrid whenever the index is available, dropping to lexical\n * only on strong lexical signals. Requested semantic/hybrid degrade to\n * lexical (with a recorded reason) when the index is unavailable — never an\n * error, unlike the old semantic_search tool.\n */\n\nimport type { ResolvedSearchMode, SearchMode } from \"./types.js\";\n\nexport interface ModeResolution {\n\tmode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a forced degradation of the request. */\n\tdegradedReason?: string;\n}\n\n/** Regex metacharacters or quoted stringsqueries where exact matching is\n * clearly what the caller wants. Path-like queries deliberately do NOT\n * count: the eval gate showed them scoring 0% lexically (content grep\n * cannot find a file by its own name) and 100% in hybrid, where the\n * embedding side and the reranker's path-affinity signal carry them. */\nexport function hasStrongLexicalSignals(query: string): boolean {\n\tif (/[\"'`]/.test(query)) return true;\n\tif (/[\\\\^$|()[\\]{}*+?]/.test(query)) return true;\n\treturn false;\n}\n\nexport function resolveSearchMode(\n\tquery: string,\n\trequested: SearchMode,\n\tembedAvailable: boolean,\n\tembedUnavailableReason?: string,\n): ModeResolution {\n\tif (requested === \"lexical\") return { mode: \"lexical\" };\n\n\tif (!embedAvailable) {\n\t\tconst reason = embedUnavailableReason ?? \"semantic index unavailable\";\n\t\treturn requested === \"auto\"\n\t\t\t? { mode: \"lexical\" }\n\t\t\t: { mode: \"lexical\", degradedReason: `${requested} requested but ${reason}` };\n\t}\n\n\tif (requested === \"semantic\" || requested === \"hybrid\") return { mode: requested };\n\n\treturn hasStrongLexicalSignals(query) ? { mode: \"lexical\" } : { mode: \"hybrid\" };\n}\n"]}
1
+ {"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../../../src/core/search/mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,kBAAkB,CAAC;IACzB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,UAAU,EACrB,cAAc,EAAE,OAAO,EACvB,sBAAsB,CAAC,EAAE,MAAM,GAC7B,cAAc,CAahB","sourcesContent":["/**\n * Availability-first search mode resolution\n * (docs/hybrid-retrieval-design.md, Decision 4).\n *\n * No clever query router: with a hot local daemon, running both retrievers\n * costs one extra embedding query, while misrouting costs recall. `auto`\n * therefore means hybrid whenever the index is available, dropping to lexical\n * only on strong lexical signals. Requested semantic/hybrid degrade to\n * lexical (with a recorded reason) when the index is unavailable — never an\n * error, unlike the old semantic_search tool.\n */\n\nimport type { ResolvedSearchMode, SearchMode } from \"./types.js\";\n\nexport interface ModeResolution {\n\tmode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a forced degradation of the request. */\n\tdegradedReason?: string;\n}\n\n/**\n * Regex metacharacters *outside* any quoted segmentthe only remaining\n * signal that the caller wants exact matching rather than ranked discovery.\n *\n * Being quoted is deliberately NOT such a signal any more. It used to route\n * every quoted query to lexical-only, which is where all ten error-fragment\n * queries in the gold set land, and lexical is the worst leg for them: R@1\n * 0.000 and MRR 0.483, against 0.600 and 0.800 for the same queries in\n * hybrid. Recall survived the routing but rank did not, and rank is what an\n * agent reads.\n *\n * Metacharacters inside a quoted segment carry no information either, because\n * `buildLexicalQueryPlan` searches a quoted segment verbatim — it escapes the\n * content, so `\"initTheme() first.\"` is matched literally and its parentheses\n * say nothing about the caller's intent. Strip quoted spans before looking.\n *\n * Path-like queries also do NOT count: the eval gate showed them scoring 0%\n * lexically (content grep cannot find a file by its own name) and far better\n * in hybrid, where the embedding side and the reranker's path-affinity signal\n * carry them.\n */\nexport function hasStrongLexicalSignals(query: string): boolean {\n\tconst unquoted = query.replace(/[\"'`][^\"'`]*[\"'`]/g, \" \");\n\treturn /[\\\\^$|()[\\]{}*+?]/.test(unquoted);\n}\n\nexport function resolveSearchMode(\n\tquery: string,\n\trequested: SearchMode,\n\tembedAvailable: boolean,\n\tembedUnavailableReason?: string,\n): ModeResolution {\n\tif (requested === \"lexical\") return { mode: \"lexical\" };\n\n\tif (!embedAvailable) {\n\t\tconst reason = embedUnavailableReason ?? \"semantic index unavailable\";\n\t\treturn requested === \"auto\"\n\t\t\t? { mode: \"lexical\" }\n\t\t\t: { mode: \"lexical\", degradedReason: `${requested} requested but ${reason}` };\n\t}\n\n\tif (requested === \"semantic\" || requested === \"hybrid\") return { mode: requested };\n\n\treturn hasStrongLexicalSignals(query) ? { mode: \"lexical\" } : { mode: \"hybrid\" };\n}\n"]}
@@ -9,17 +9,30 @@
9
9
  * lexical (with a recorded reason) when the index is unavailable — never an
10
10
  * error, unlike the old semantic_search tool.
11
11
  */
12
- /** Regex metacharacters or quoted strings — queries where exact matching is
13
- * clearly what the caller wants. Path-like queries deliberately do NOT
14
- * count: the eval gate showed them scoring 0% lexically (content grep
15
- * cannot find a file by its own name) and 100% in hybrid, where the
16
- * embedding side and the reranker's path-affinity signal carry them. */
12
+ /**
13
+ * Regex metacharacters *outside* any quoted segment the only remaining
14
+ * signal that the caller wants exact matching rather than ranked discovery.
15
+ *
16
+ * Being quoted is deliberately NOT such a signal any more. It used to route
17
+ * every quoted query to lexical-only, which is where all ten error-fragment
18
+ * queries in the gold set land, and lexical is the worst leg for them: R@1
19
+ * 0.000 and MRR 0.483, against 0.600 and 0.800 for the same queries in
20
+ * hybrid. Recall survived the routing but rank did not, and rank is what an
21
+ * agent reads.
22
+ *
23
+ * Metacharacters inside a quoted segment carry no information either, because
24
+ * `buildLexicalQueryPlan` searches a quoted segment verbatim — it escapes the
25
+ * content, so `"initTheme() first."` is matched literally and its parentheses
26
+ * say nothing about the caller's intent. Strip quoted spans before looking.
27
+ *
28
+ * Path-like queries also do NOT count: the eval gate showed them scoring 0%
29
+ * lexically (content grep cannot find a file by its own name) and far better
30
+ * in hybrid, where the embedding side and the reranker's path-affinity signal
31
+ * carry them.
32
+ */
17
33
  export function hasStrongLexicalSignals(query) {
18
- if (/["'`]/.test(query))
19
- return true;
20
- if (/[\\^$|()[\]{}*+?]/.test(query))
21
- return true;
22
- return false;
34
+ const unquoted = query.replace(/["'`][^"'`]*["'`]/g, " ");
35
+ return /[\\^$|()[\]{}*+?]/.test(unquoted);
23
36
  }
24
37
  export function resolveSearchMode(query, requested, embedAvailable, embedUnavailableReason) {
25
38
  if (requested === "lexical")
@@ -1 +1 @@
1
- {"version":3,"file":"mode.js","sourceRoot":"","sources":["../../../src/core/search/mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH;;;;yEAIyE;AACzE,MAAM,UAAU,uBAAuB,CAAC,KAAa,EAAW;IAC/D,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,MAAM,UAAU,iBAAiB,CAChC,KAAa,EACb,SAAqB,EACrB,cAAuB,EACvB,sBAA+B,EACd;IACjB,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAExD,IAAI,CAAC,cAAc,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,sBAAsB,IAAI,4BAA4B,CAAC;QACtE,OAAO,SAAS,KAAK,MAAM;YAC1B,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE;YACrB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,SAAS,kBAAkB,MAAM,EAAE,EAAE,CAAC;IAChF,CAAC;IAED,IAAI,SAAS,KAAK,UAAU,IAAI,SAAS,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAEnF,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAAA,CACjF","sourcesContent":["/**\n * Availability-first search mode resolution\n * (docs/hybrid-retrieval-design.md, Decision 4).\n *\n * No clever query router: with a hot local daemon, running both retrievers\n * costs one extra embedding query, while misrouting costs recall. `auto`\n * therefore means hybrid whenever the index is available, dropping to lexical\n * only on strong lexical signals. Requested semantic/hybrid degrade to\n * lexical (with a recorded reason) when the index is unavailable — never an\n * error, unlike the old semantic_search tool.\n */\n\nimport type { ResolvedSearchMode, SearchMode } from \"./types.js\";\n\nexport interface ModeResolution {\n\tmode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a forced degradation of the request. */\n\tdegradedReason?: string;\n}\n\n/** Regex metacharacters or quoted stringsqueries where exact matching is\n * clearly what the caller wants. Path-like queries deliberately do NOT\n * count: the eval gate showed them scoring 0% lexically (content grep\n * cannot find a file by its own name) and 100% in hybrid, where the\n * embedding side and the reranker's path-affinity signal carry them. */\nexport function hasStrongLexicalSignals(query: string): boolean {\n\tif (/[\"'`]/.test(query)) return true;\n\tif (/[\\\\^$|()[\\]{}*+?]/.test(query)) return true;\n\treturn false;\n}\n\nexport function resolveSearchMode(\n\tquery: string,\n\trequested: SearchMode,\n\tembedAvailable: boolean,\n\tembedUnavailableReason?: string,\n): ModeResolution {\n\tif (requested === \"lexical\") return { mode: \"lexical\" };\n\n\tif (!embedAvailable) {\n\t\tconst reason = embedUnavailableReason ?? \"semantic index unavailable\";\n\t\treturn requested === \"auto\"\n\t\t\t? { mode: \"lexical\" }\n\t\t\t: { mode: \"lexical\", degradedReason: `${requested} requested but ${reason}` };\n\t}\n\n\tif (requested === \"semantic\" || requested === \"hybrid\") return { mode: requested };\n\n\treturn hasStrongLexicalSignals(query) ? { mode: \"lexical\" } : { mode: \"hybrid\" };\n}\n"]}
1
+ {"version":3,"file":"mode.js","sourceRoot":"","sources":["../../../src/core/search/mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAa,EAAW;IAC/D,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC;IAC1D,OAAO,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAAA,CAC1C;AAED,MAAM,UAAU,iBAAiB,CAChC,KAAa,EACb,SAAqB,EACrB,cAAuB,EACvB,sBAA+B,EACd;IACjB,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAExD,IAAI,CAAC,cAAc,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,sBAAsB,IAAI,4BAA4B,CAAC;QACtE,OAAO,SAAS,KAAK,MAAM;YAC1B,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE;YACrB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,SAAS,kBAAkB,MAAM,EAAE,EAAE,CAAC;IAChF,CAAC;IAED,IAAI,SAAS,KAAK,UAAU,IAAI,SAAS,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAEnF,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAAA,CACjF","sourcesContent":["/**\n * Availability-first search mode resolution\n * (docs/hybrid-retrieval-design.md, Decision 4).\n *\n * No clever query router: with a hot local daemon, running both retrievers\n * costs one extra embedding query, while misrouting costs recall. `auto`\n * therefore means hybrid whenever the index is available, dropping to lexical\n * only on strong lexical signals. Requested semantic/hybrid degrade to\n * lexical (with a recorded reason) when the index is unavailable — never an\n * error, unlike the old semantic_search tool.\n */\n\nimport type { ResolvedSearchMode, SearchMode } from \"./types.js\";\n\nexport interface ModeResolution {\n\tmode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a forced degradation of the request. */\n\tdegradedReason?: string;\n}\n\n/**\n * Regex metacharacters *outside* any quoted segmentthe only remaining\n * signal that the caller wants exact matching rather than ranked discovery.\n *\n * Being quoted is deliberately NOT such a signal any more. It used to route\n * every quoted query to lexical-only, which is where all ten error-fragment\n * queries in the gold set land, and lexical is the worst leg for them: R@1\n * 0.000 and MRR 0.483, against 0.600 and 0.800 for the same queries in\n * hybrid. Recall survived the routing but rank did not, and rank is what an\n * agent reads.\n *\n * Metacharacters inside a quoted segment carry no information either, because\n * `buildLexicalQueryPlan` searches a quoted segment verbatim — it escapes the\n * content, so `\"initTheme() first.\"` is matched literally and its parentheses\n * say nothing about the caller's intent. Strip quoted spans before looking.\n *\n * Path-like queries also do NOT count: the eval gate showed them scoring 0%\n * lexically (content grep cannot find a file by its own name) and far better\n * in hybrid, where the embedding side and the reranker's path-affinity signal\n * carry them.\n */\nexport function hasStrongLexicalSignals(query: string): boolean {\n\tconst unquoted = query.replace(/[\"'`][^\"'`]*[\"'`]/g, \" \");\n\treturn /[\\\\^$|()[\\]{}*+?]/.test(unquoted);\n}\n\nexport function resolveSearchMode(\n\tquery: string,\n\trequested: SearchMode,\n\tembedAvailable: boolean,\n\tembedUnavailableReason?: string,\n): ModeResolution {\n\tif (requested === \"lexical\") return { mode: \"lexical\" };\n\n\tif (!embedAvailable) {\n\t\tconst reason = embedUnavailableReason ?? \"semantic index unavailable\";\n\t\treturn requested === \"auto\"\n\t\t\t? { mode: \"lexical\" }\n\t\t\t: { mode: \"lexical\", degradedReason: `${requested} requested but ${reason}` };\n\t}\n\n\tif (requested === \"semantic\" || requested === \"hybrid\") return { mode: requested };\n\n\treturn hasStrongLexicalSignals(query) ? { mode: \"lexical\" } : { mode: \"hybrid\" };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"rerank.d.ts","sourceRoot":"","sources":["../../../src/core/search/rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAYjD,MAAM,WAAW,YAAY;IAC5B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAwDhH","sourcesContent":["/**\n * Deterministic reranker over the fused top-50\n * (docs/hybrid-retrieval-design.md, step 7 of the shipping order).\n *\n * The eval gate showed fused Recall@50 well above Recall@5/10 — the right\n * candidates survive fusion but sit too deep. This reranker re-orders them\n * using evidence that is only cheap to compute *after* fusion, when there\n * are ≤50 candidates instead of thousands of lines:\n *\n * - term coverage: how many distinct query terms appear in the candidate's\n * actual expanded window (read from disk);\n * - path affinity: query terms appearing in the candidate's file path —\n * this is what lets a query like `core/search/hybrid-search.ts` rank the\n * file itself first, which content grep alone cannot do;\n * - fused prior: the RRF ordering, so retriever consensus still counts.\n *\n * Purely lexical-statistical and deterministic — no model, no I/O beyond\n * reading candidate windows. A cross-encoder can later replace the scoring\n * function behind the same signature; that model work belongs to\n * `kolisachint/embeddingsearchtools`, not here.\n */\n\nimport { readFileSync } from \"fs\";\nimport path from \"path\";\nimport { buildLexicalQueryPlan } from \"./lexical-retriever.js\";\nimport type { FusedCandidate } from \"./types.js\";\n\n/** Weights of the scoring blend. The eval harness (scripts/search-eval.mjs)\n * is the instrument for changing them — don't tune blind. */\nconst WEIGHT_FUSED_PRIOR = 0.4;\nconst WEIGHT_TERM_COVERAGE = 0.35;\nconst WEIGHT_PATH_AFFINITY = 0.25;\n/** Additive bonus when the query *is* the candidate's path (or its suffix):\n * the caller named the file, so no amount of content evidence elsewhere\n * should outrank it. */\nconst EXACT_PATH_BONUS = 0.5;\n\nexport interface RerankResult {\n\tcandidates: FusedCandidate[];\n\tlatencyMs: number;\n}\n\nexport function rerankCandidates(query: string, candidates: readonly FusedCandidate[], cwd: string): RerankResult {\n\tconst startedMs = Date.now();\n\tconst plan = buildLexicalQueryPlan(query);\n\tif (!plan || candidates.length < 2) {\n\t\treturn { candidates: [...candidates], latencyMs: Date.now() - startedMs };\n\t}\n\tconst terms = plan.terms;\n\tconst queryPath = query.trim().toLowerCase();\n\n\tconst fileCache = new Map<string, string[] | undefined>();\n\tconst readLines = (rel: string): string[] | undefined => {\n\t\tif (!fileCache.has(rel)) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(path.resolve(cwd, rel), \"utf-8\");\n\t\t\t\tfileCache.set(rel, content.toLowerCase().split(\"\\n\"));\n\t\t\t} catch {\n\t\t\t\tfileCache.set(rel, undefined);\n\t\t\t}\n\t\t}\n\t\treturn fileCache.get(rel);\n\t};\n\n\tconst scored = candidates.map((candidate, index) => {\n\t\t// Fused prior: normalized RRF ordering, 1 for the top candidate.\n\t\tconst fusedPrior = 1 - index / candidates.length;\n\n\t\tconst lines = readLines(candidate.path);\n\t\tlet termCoverage = 0;\n\t\tif (lines && terms.length > 0) {\n\t\t\tconst window = lines\n\t\t\t\t.slice(Math.max(0, candidate.startLine - 1), Math.min(lines.length, candidate.endLine))\n\t\t\t\t.join(\"\\n\");\n\t\t\ttermCoverage = terms.filter((t) => window.includes(t)).length / terms.length;\n\t\t}\n\n\t\tconst lowerPath = candidate.path.toLowerCase();\n\t\t// A quoted phrase rarely names a file; split it into path-ish tokens so\n\t\t// `\"token budget exceeded\"` still gets partial path credit.\n\t\tconst pathTerms = terms.length === 1 ? terms[0].split(/[^a-z0-9_$]+/).filter((t) => t.length >= 3) : terms;\n\t\tconst pathAffinity =\n\t\t\tpathTerms.length > 0 ? pathTerms.filter((t) => lowerPath.includes(t)).length / pathTerms.length : 0;\n\n\t\tconst exactPath =\n\t\t\tqueryPath.length >= 3 && (lowerPath === queryPath || lowerPath.endsWith(`/${queryPath}`)) ? 1 : 0;\n\n\t\tconst score =\n\t\t\tWEIGHT_FUSED_PRIOR * fusedPrior +\n\t\t\tWEIGHT_TERM_COVERAGE * termCoverage +\n\t\t\tWEIGHT_PATH_AFFINITY * pathAffinity +\n\t\t\tEXACT_PATH_BONUS * exactPath;\n\t\treturn { candidate, index, score };\n\t});\n\n\t// Stable, deterministic: score desc, fused order as tie-break.\n\tscored.sort((a, b) => b.score - a.score || a.index - b.index);\n\treturn { candidates: scored.map((s) => s.candidate), latencyMs: Date.now() - startedMs };\n}\n"]}
1
+ {"version":3,"file":"rerank.d.ts","sourceRoot":"","sources":["../../../src/core/search/rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAsDjD,MAAM,WAAW,YAAY;IAC5B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAkFhH","sourcesContent":["/**\n * Deterministic reranker over the fused top-50\n * (docs/hybrid-retrieval-design.md, step 7 of the shipping order).\n *\n * The eval gate showed fused Recall@50 well above Recall@5/10 — the right\n * candidates survive fusion but sit too deep. This reranker re-orders them\n * using evidence that is only cheap to compute *after* fusion, when there\n * are ≤50 candidates instead of thousands of lines:\n *\n * - term coverage: how many distinct query terms appear in the candidate's\n * actual expanded window (read from disk);\n * - path affinity: query terms appearing in the candidate's file path —\n * this is what lets a query like `core/search/hybrid-search.ts` rank the\n * file itself first, which content grep alone cannot do;\n * - fused prior: the RRF ordering, so retriever consensus still counts.\n *\n * Purely lexical-statistical and deterministic — no model, no I/O beyond\n * reading candidate windows. A cross-encoder can later replace the scoring\n * function behind the same signature; that model work belongs to\n * `kolisachint/embeddingsearchtools`, not here.\n */\n\nimport { readFileSync } from \"fs\";\nimport path from \"path\";\nimport { buildLexicalQueryPlan } from \"./lexical-retriever.js\";\nimport type { FusedCandidate } from \"./types.js\";\n\n/** Weights of the scoring blend. The eval harness (`bun run search-eval`) is\n * the instrument for changing them — don't tune blind. */\nconst WEIGHT_FUSED_PRIOR = 0.4;\nconst WEIGHT_TERM_COVERAGE = 0.35;\nconst WEIGHT_PATH_AFFINITY = 0.25;\n/** Additive bonus when the query *is* the candidate's path (or its suffix):\n * the caller named the file, so no amount of content evidence elsewhere\n * should outrank it. */\nconst EXACT_PATH_BONUS = 0.5;\n/**\n * Additive bonus when the window *declares* a query term rather than merely\n * mentioning it.\n *\n * This targets the largest measured gap in the eval: on the 22 exact-symbol\n * queries the definition is in the top 10 about 85% of the time but ranked\n * first only about 20% of the time. Call sites outnumber definitions and\n * contain the identical identifier, so term coverage — which saturates at 1.0\n * for both — cannot separate them. Structure can.\n */\nconst DECLARATION_BONUS = 0.3;\n\n/** Keywords that introduce a definition across the languages this indexes.\n * Matched against lowercased text, so the term is lowercased too. */\nconst DECLARATION_KEYWORDS =\n\t\"function|class|interface|type|enum|struct|impl|trait|fn|def|const|let|var|namespace|module\";\n\n/** Does `window` declare `term`, as opposed to referencing it? */\nfunction declaresTerm(window: string, term: string): boolean {\n\tconst escaped = term.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t// `function foo(`, `class Foo {`, `const foo =` ...\n\tif (new RegExp(`\\\\b(?:${DECLARATION_KEYWORDS})\\\\s+${escaped}\\\\b`).test(window)) return true;\n\t// `foo(...) {` at the start of a line — methods, Go/Rust receivers, Python defs\n\t// already covered above, but this catches object-literal and class members.\n\tif (\n\t\tnew RegExp(`^\\\\s*(?:(?:async|public|private|protected|static|export)\\\\s+)*${escaped}\\\\s*[(<]`, \"m\").test(window)\n\t) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n/** Inverse document frequency over the candidate pool.\n *\n * True corpus IDF lives in the BM25 index and is not exposed over the daemon\n * protocol, so this approximates it with the candidate set: a term present in\n * every candidate discriminates nothing, one present in three carries the\n * signal. That is the comparison the reranker actually needs to make, since it\n * only ever orders candidates against each other. */\nfunction inverseDocumentFrequency(documentFrequency: number, total: number): number {\n\treturn Math.log(1 + total / Math.max(1, documentFrequency));\n}\n\nexport interface RerankResult {\n\tcandidates: FusedCandidate[];\n\tlatencyMs: number;\n}\n\nexport function rerankCandidates(query: string, candidates: readonly FusedCandidate[], cwd: string): RerankResult {\n\tconst startedMs = Date.now();\n\tconst plan = buildLexicalQueryPlan(query);\n\tif (!plan || candidates.length < 2) {\n\t\treturn { candidates: [...candidates], latencyMs: Date.now() - startedMs };\n\t}\n\tconst terms = plan.terms;\n\tconst queryPath = query.trim().toLowerCase();\n\n\tconst fileCache = new Map<string, string[] | undefined>();\n\tconst readLines = (rel: string): string[] | undefined => {\n\t\tif (!fileCache.has(rel)) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(path.resolve(cwd, rel), \"utf-8\");\n\t\t\t\tfileCache.set(rel, content.toLowerCase().split(\"\\n\"));\n\t\t\t} catch {\n\t\t\t\tfileCache.set(rel, undefined);\n\t\t\t}\n\t\t}\n\t\treturn fileCache.get(rel);\n\t};\n\n\t// Read every candidate window once: the term/declaration signals and the\n\t// candidate-pool IDF all need them, and files repeat across candidates.\n\tconst windows = candidates.map((candidate) => {\n\t\tconst lines = readLines(candidate.path);\n\t\tif (!lines) return undefined;\n\t\treturn lines.slice(Math.max(0, candidate.startLine - 1), Math.min(lines.length, candidate.endLine)).join(\"\\n\");\n\t});\n\n\t// Candidate-pool document frequency per term, for the IDF weighting below.\n\tconst documentFrequency = new Map<string, number>();\n\tfor (const term of terms) {\n\t\tdocumentFrequency.set(term, windows.filter((w) => w?.includes(term)).length);\n\t}\n\tconst termWeight = new Map(\n\t\tterms.map((t) => [t, inverseDocumentFrequency(documentFrequency.get(t) ?? 0, candidates.length)]),\n\t);\n\tconst totalTermWeight = terms.reduce((sum, t) => sum + (termWeight.get(t) ?? 0), 0);\n\n\t// Fused prior normalized by score, not by position: a candidate both\n\t// retrievers agreed on should outrank one that squeaked in, and a uniform\n\t// 1 - index/length ramp throws that magnitude away.\n\tconst maxRrfScore = Math.max(...candidates.map((c) => c.rrfScore), Number.MIN_VALUE);\n\n\tconst scored = candidates.map((candidate, index) => {\n\t\tconst fusedPrior = candidate.rrfScore / maxRrfScore;\n\n\t\tconst window = windows[index];\n\t\tlet termCoverage = 0;\n\t\tlet declaresAnyTerm = false;\n\t\tif (window && terms.length > 0) {\n\t\t\tconst present = terms.filter((t) => window.includes(t));\n\t\t\ttermCoverage =\n\t\t\t\ttotalTermWeight > 0\n\t\t\t\t\t? present.reduce((sum, t) => sum + (termWeight.get(t) ?? 0), 0) / totalTermWeight\n\t\t\t\t\t: present.length / terms.length;\n\t\t\tdeclaresAnyTerm = present.some((t) => declaresTerm(window, t));\n\t\t}\n\n\t\tconst lowerPath = candidate.path.toLowerCase();\n\t\t// A quoted phrase rarely names a file; split it into path-ish tokens so\n\t\t// `\"token budget exceeded\"` still gets partial path credit.\n\t\tconst pathTerms = terms.length === 1 ? terms[0].split(/[^a-z0-9_$]+/).filter((t) => t.length >= 3) : terms;\n\t\tconst pathAffinity =\n\t\t\tpathTerms.length > 0 ? pathTerms.filter((t) => lowerPath.includes(t)).length / pathTerms.length : 0;\n\n\t\tconst exactPath =\n\t\t\tqueryPath.length >= 3 && (lowerPath === queryPath || lowerPath.endsWith(`/${queryPath}`)) ? 1 : 0;\n\n\t\tconst score =\n\t\t\tWEIGHT_FUSED_PRIOR * fusedPrior +\n\t\t\tWEIGHT_TERM_COVERAGE * termCoverage +\n\t\t\tWEIGHT_PATH_AFFINITY * pathAffinity +\n\t\t\tEXACT_PATH_BONUS * exactPath +\n\t\t\t(declaresAnyTerm ? DECLARATION_BONUS : 0);\n\t\treturn { candidate, index, score };\n\t});\n\n\t// Stable, deterministic: score desc, fused order as tie-break.\n\tscored.sort((a, b) => b.score - a.score || a.index - b.index);\n\treturn { candidates: scored.map((s) => s.candidate), latencyMs: Date.now() - startedMs };\n}\n"]}
@@ -22,8 +22,8 @@
22
22
  import { readFileSync } from "fs";
23
23
  import path from "path";
24
24
  import { buildLexicalQueryPlan } from "./lexical-retriever.js";
25
- /** Weights of the scoring blend. The eval harness (scripts/search-eval.mjs)
26
- * is the instrument for changing them — don't tune blind. */
25
+ /** Weights of the scoring blend. The eval harness (`bun run search-eval`) is
26
+ * the instrument for changing them — don't tune blind. */
27
27
  const WEIGHT_FUSED_PRIOR = 0.4;
28
28
  const WEIGHT_TERM_COVERAGE = 0.35;
29
29
  const WEIGHT_PATH_AFFINITY = 0.25;
@@ -31,6 +31,43 @@ const WEIGHT_PATH_AFFINITY = 0.25;
31
31
  * the caller named the file, so no amount of content evidence elsewhere
32
32
  * should outrank it. */
33
33
  const EXACT_PATH_BONUS = 0.5;
34
+ /**
35
+ * Additive bonus when the window *declares* a query term rather than merely
36
+ * mentioning it.
37
+ *
38
+ * This targets the largest measured gap in the eval: on the 22 exact-symbol
39
+ * queries the definition is in the top 10 about 85% of the time but ranked
40
+ * first only about 20% of the time. Call sites outnumber definitions and
41
+ * contain the identical identifier, so term coverage — which saturates at 1.0
42
+ * for both — cannot separate them. Structure can.
43
+ */
44
+ const DECLARATION_BONUS = 0.3;
45
+ /** Keywords that introduce a definition across the languages this indexes.
46
+ * Matched against lowercased text, so the term is lowercased too. */
47
+ const DECLARATION_KEYWORDS = "function|class|interface|type|enum|struct|impl|trait|fn|def|const|let|var|namespace|module";
48
+ /** Does `window` declare `term`, as opposed to referencing it? */
49
+ function declaresTerm(window, term) {
50
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51
+ // `function foo(`, `class Foo {`, `const foo =` ...
52
+ if (new RegExp(`\\b(?:${DECLARATION_KEYWORDS})\\s+${escaped}\\b`).test(window))
53
+ return true;
54
+ // `foo(...) {` at the start of a line — methods, Go/Rust receivers, Python defs
55
+ // already covered above, but this catches object-literal and class members.
56
+ if (new RegExp(`^\\s*(?:(?:async|public|private|protected|static|export)\\s+)*${escaped}\\s*[(<]`, "m").test(window)) {
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+ /** Inverse document frequency over the candidate pool.
62
+ *
63
+ * True corpus IDF lives in the BM25 index and is not exposed over the daemon
64
+ * protocol, so this approximates it with the candidate set: a term present in
65
+ * every candidate discriminates nothing, one present in three carries the
66
+ * signal. That is the comparison the reranker actually needs to make, since it
67
+ * only ever orders candidates against each other. */
68
+ function inverseDocumentFrequency(documentFrequency, total) {
69
+ return Math.log(1 + total / Math.max(1, documentFrequency));
70
+ }
34
71
  export function rerankCandidates(query, candidates, cwd) {
35
72
  const startedMs = Date.now();
36
73
  const plan = buildLexicalQueryPlan(query);
@@ -52,16 +89,37 @@ export function rerankCandidates(query, candidates, cwd) {
52
89
  }
53
90
  return fileCache.get(rel);
54
91
  };
55
- const scored = candidates.map((candidate, index) => {
56
- // Fused prior: normalized RRF ordering, 1 for the top candidate.
57
- const fusedPrior = 1 - index / candidates.length;
92
+ // Read every candidate window once: the term/declaration signals and the
93
+ // candidate-pool IDF all need them, and files repeat across candidates.
94
+ const windows = candidates.map((candidate) => {
58
95
  const lines = readLines(candidate.path);
96
+ if (!lines)
97
+ return undefined;
98
+ return lines.slice(Math.max(0, candidate.startLine - 1), Math.min(lines.length, candidate.endLine)).join("\n");
99
+ });
100
+ // Candidate-pool document frequency per term, for the IDF weighting below.
101
+ const documentFrequency = new Map();
102
+ for (const term of terms) {
103
+ documentFrequency.set(term, windows.filter((w) => w?.includes(term)).length);
104
+ }
105
+ const termWeight = new Map(terms.map((t) => [t, inverseDocumentFrequency(documentFrequency.get(t) ?? 0, candidates.length)]));
106
+ const totalTermWeight = terms.reduce((sum, t) => sum + (termWeight.get(t) ?? 0), 0);
107
+ // Fused prior normalized by score, not by position: a candidate both
108
+ // retrievers agreed on should outrank one that squeaked in, and a uniform
109
+ // 1 - index/length ramp throws that magnitude away.
110
+ const maxRrfScore = Math.max(...candidates.map((c) => c.rrfScore), Number.MIN_VALUE);
111
+ const scored = candidates.map((candidate, index) => {
112
+ const fusedPrior = candidate.rrfScore / maxRrfScore;
113
+ const window = windows[index];
59
114
  let termCoverage = 0;
60
- if (lines && terms.length > 0) {
61
- const window = lines
62
- .slice(Math.max(0, candidate.startLine - 1), Math.min(lines.length, candidate.endLine))
63
- .join("\n");
64
- termCoverage = terms.filter((t) => window.includes(t)).length / terms.length;
115
+ let declaresAnyTerm = false;
116
+ if (window && terms.length > 0) {
117
+ const present = terms.filter((t) => window.includes(t));
118
+ termCoverage =
119
+ totalTermWeight > 0
120
+ ? present.reduce((sum, t) => sum + (termWeight.get(t) ?? 0), 0) / totalTermWeight
121
+ : present.length / terms.length;
122
+ declaresAnyTerm = present.some((t) => declaresTerm(window, t));
65
123
  }
66
124
  const lowerPath = candidate.path.toLowerCase();
67
125
  // A quoted phrase rarely names a file; split it into path-ish tokens so
@@ -72,7 +130,8 @@ export function rerankCandidates(query, candidates, cwd) {
72
130
  const score = WEIGHT_FUSED_PRIOR * fusedPrior +
73
131
  WEIGHT_TERM_COVERAGE * termCoverage +
74
132
  WEIGHT_PATH_AFFINITY * pathAffinity +
75
- EXACT_PATH_BONUS * exactPath;
133
+ EXACT_PATH_BONUS * exactPath +
134
+ (declaresAnyTerm ? DECLARATION_BONUS : 0);
76
135
  return { candidate, index, score };
77
136
  });
78
137
  // Stable, deterministic: score desc, fused order as tie-break.
@@ -1 +1 @@
1
- {"version":3,"file":"rerank.js","sourceRoot":"","sources":["../../../src/core/search/rerank.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Deterministic reranker over the fused top-50\n * (docs/hybrid-retrieval-design.md, step 7 of the shipping order).\n *\n * The eval gate showed fused Recall@50 well above Recall@5/10 — the right\n * candidates survive fusion but sit too deep. This reranker re-orders them\n * using evidence that is only cheap to compute *after* fusion, when there\n * are ≤50 candidates instead of thousands of lines:\n *\n * - term coverage: how many distinct query terms appear in the candidate's\n * actual expanded window (read from disk);\n * - path affinity: query terms appearing in the candidate's file path —\n * this is what lets a query like `core/search/hybrid-search.ts` rank the\n * file itself first, which content grep alone cannot do;\n * - fused prior: the RRF ordering, so retriever consensus still counts.\n *\n * Purely lexical-statistical and deterministic — no model, no I/O beyond\n * reading candidate windows. A cross-encoder can later replace the scoring\n * function behind the same signature; that model work belongs to\n * `kolisachint/embeddingsearchtools`, not here.\n */\n\nimport { readFileSync } from \"fs\";\nimport path from \"path\";\nimport { buildLexicalQueryPlan } from \"./lexical-retriever.js\";\nimport type { FusedCandidate } from \"./types.js\";\n\n/** Weights of the scoring blend. The eval harness (scripts/search-eval.mjs)\n * is the instrument for changing them — don't tune blind. */\nconst WEIGHT_FUSED_PRIOR = 0.4;\nconst WEIGHT_TERM_COVERAGE = 0.35;\nconst WEIGHT_PATH_AFFINITY = 0.25;\n/** Additive bonus when the query *is* the candidate's path (or its suffix):\n * the caller named the file, so no amount of content evidence elsewhere\n * should outrank it. */\nconst EXACT_PATH_BONUS = 0.5;\n\nexport interface RerankResult {\n\tcandidates: FusedCandidate[];\n\tlatencyMs: number;\n}\n\nexport function rerankCandidates(query: string, candidates: readonly FusedCandidate[], cwd: string): RerankResult {\n\tconst startedMs = Date.now();\n\tconst plan = buildLexicalQueryPlan(query);\n\tif (!plan || candidates.length < 2) {\n\t\treturn { candidates: [...candidates], latencyMs: Date.now() - startedMs };\n\t}\n\tconst terms = plan.terms;\n\tconst queryPath = query.trim().toLowerCase();\n\n\tconst fileCache = new Map<string, string[] | undefined>();\n\tconst readLines = (rel: string): string[] | undefined => {\n\t\tif (!fileCache.has(rel)) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(path.resolve(cwd, rel), \"utf-8\");\n\t\t\t\tfileCache.set(rel, content.toLowerCase().split(\"\\n\"));\n\t\t\t} catch {\n\t\t\t\tfileCache.set(rel, undefined);\n\t\t\t}\n\t\t}\n\t\treturn fileCache.get(rel);\n\t};\n\n\tconst scored = candidates.map((candidate, index) => {\n\t\t// Fused prior: normalized RRF ordering, 1 for the top candidate.\n\t\tconst fusedPrior = 1 - index / candidates.length;\n\n\t\tconst lines = readLines(candidate.path);\n\t\tlet termCoverage = 0;\n\t\tif (lines && terms.length > 0) {\n\t\t\tconst window = lines\n\t\t\t\t.slice(Math.max(0, candidate.startLine - 1), Math.min(lines.length, candidate.endLine))\n\t\t\t\t.join(\"\\n\");\n\t\t\ttermCoverage = terms.filter((t) => window.includes(t)).length / terms.length;\n\t\t}\n\n\t\tconst lowerPath = candidate.path.toLowerCase();\n\t\t// A quoted phrase rarely names a file; split it into path-ish tokens so\n\t\t// `\"token budget exceeded\"` still gets partial path credit.\n\t\tconst pathTerms = terms.length === 1 ? terms[0].split(/[^a-z0-9_$]+/).filter((t) => t.length >= 3) : terms;\n\t\tconst pathAffinity =\n\t\t\tpathTerms.length > 0 ? pathTerms.filter((t) => lowerPath.includes(t)).length / pathTerms.length : 0;\n\n\t\tconst exactPath =\n\t\t\tqueryPath.length >= 3 && (lowerPath === queryPath || lowerPath.endsWith(`/${queryPath}`)) ? 1 : 0;\n\n\t\tconst score =\n\t\t\tWEIGHT_FUSED_PRIOR * fusedPrior +\n\t\t\tWEIGHT_TERM_COVERAGE * termCoverage +\n\t\t\tWEIGHT_PATH_AFFINITY * pathAffinity +\n\t\t\tEXACT_PATH_BONUS * exactPath;\n\t\treturn { candidate, index, score };\n\t});\n\n\t// Stable, deterministic: score desc, fused order as tie-break.\n\tscored.sort((a, b) => b.score - a.score || a.index - b.index);\n\treturn { candidates: scored.map((s) => s.candidate), latencyMs: Date.now() - startedMs };\n}\n"]}
1
+ {"version":3,"file":"rerank.js","sourceRoot":"","sources":["../../../src/core/search/rerank.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Deterministic reranker over the fused top-50\n * (docs/hybrid-retrieval-design.md, step 7 of the shipping order).\n *\n * The eval gate showed fused Recall@50 well above Recall@5/10 — the right\n * candidates survive fusion but sit too deep. This reranker re-orders them\n * using evidence that is only cheap to compute *after* fusion, when there\n * are ≤50 candidates instead of thousands of lines:\n *\n * - term coverage: how many distinct query terms appear in the candidate's\n * actual expanded window (read from disk);\n * - path affinity: query terms appearing in the candidate's file path —\n * this is what lets a query like `core/search/hybrid-search.ts` rank the\n * file itself first, which content grep alone cannot do;\n * - fused prior: the RRF ordering, so retriever consensus still counts.\n *\n * Purely lexical-statistical and deterministic — no model, no I/O beyond\n * reading candidate windows. A cross-encoder can later replace the scoring\n * function behind the same signature; that model work belongs to\n * `kolisachint/embeddingsearchtools`, not here.\n */\n\nimport { readFileSync } from \"fs\";\nimport path from \"path\";\nimport { buildLexicalQueryPlan } from \"./lexical-retriever.js\";\nimport type { FusedCandidate } from \"./types.js\";\n\n/** Weights of the scoring blend. The eval harness (`bun run search-eval`) is\n * the instrument for changing them — don't tune blind. */\nconst WEIGHT_FUSED_PRIOR = 0.4;\nconst WEIGHT_TERM_COVERAGE = 0.35;\nconst WEIGHT_PATH_AFFINITY = 0.25;\n/** Additive bonus when the query *is* the candidate's path (or its suffix):\n * the caller named the file, so no amount of content evidence elsewhere\n * should outrank it. */\nconst EXACT_PATH_BONUS = 0.5;\n/**\n * Additive bonus when the window *declares* a query term rather than merely\n * mentioning it.\n *\n * This targets the largest measured gap in the eval: on the 22 exact-symbol\n * queries the definition is in the top 10 about 85% of the time but ranked\n * first only about 20% of the time. Call sites outnumber definitions and\n * contain the identical identifier, so term coverage — which saturates at 1.0\n * for both — cannot separate them. Structure can.\n */\nconst DECLARATION_BONUS = 0.3;\n\n/** Keywords that introduce a definition across the languages this indexes.\n * Matched against lowercased text, so the term is lowercased too. */\nconst DECLARATION_KEYWORDS =\n\t\"function|class|interface|type|enum|struct|impl|trait|fn|def|const|let|var|namespace|module\";\n\n/** Does `window` declare `term`, as opposed to referencing it? */\nfunction declaresTerm(window: string, term: string): boolean {\n\tconst escaped = term.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t// `function foo(`, `class Foo {`, `const foo =` ...\n\tif (new RegExp(`\\\\b(?:${DECLARATION_KEYWORDS})\\\\s+${escaped}\\\\b`).test(window)) return true;\n\t// `foo(...) {` at the start of a line — methods, Go/Rust receivers, Python defs\n\t// already covered above, but this catches object-literal and class members.\n\tif (\n\t\tnew RegExp(`^\\\\s*(?:(?:async|public|private|protected|static|export)\\\\s+)*${escaped}\\\\s*[(<]`, \"m\").test(window)\n\t) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n/** Inverse document frequency over the candidate pool.\n *\n * True corpus IDF lives in the BM25 index and is not exposed over the daemon\n * protocol, so this approximates it with the candidate set: a term present in\n * every candidate discriminates nothing, one present in three carries the\n * signal. That is the comparison the reranker actually needs to make, since it\n * only ever orders candidates against each other. */\nfunction inverseDocumentFrequency(documentFrequency: number, total: number): number {\n\treturn Math.log(1 + total / Math.max(1, documentFrequency));\n}\n\nexport interface RerankResult {\n\tcandidates: FusedCandidate[];\n\tlatencyMs: number;\n}\n\nexport function rerankCandidates(query: string, candidates: readonly FusedCandidate[], cwd: string): RerankResult {\n\tconst startedMs = Date.now();\n\tconst plan = buildLexicalQueryPlan(query);\n\tif (!plan || candidates.length < 2) {\n\t\treturn { candidates: [...candidates], latencyMs: Date.now() - startedMs };\n\t}\n\tconst terms = plan.terms;\n\tconst queryPath = query.trim().toLowerCase();\n\n\tconst fileCache = new Map<string, string[] | undefined>();\n\tconst readLines = (rel: string): string[] | undefined => {\n\t\tif (!fileCache.has(rel)) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(path.resolve(cwd, rel), \"utf-8\");\n\t\t\t\tfileCache.set(rel, content.toLowerCase().split(\"\\n\"));\n\t\t\t} catch {\n\t\t\t\tfileCache.set(rel, undefined);\n\t\t\t}\n\t\t}\n\t\treturn fileCache.get(rel);\n\t};\n\n\t// Read every candidate window once: the term/declaration signals and the\n\t// candidate-pool IDF all need them, and files repeat across candidates.\n\tconst windows = candidates.map((candidate) => {\n\t\tconst lines = readLines(candidate.path);\n\t\tif (!lines) return undefined;\n\t\treturn lines.slice(Math.max(0, candidate.startLine - 1), Math.min(lines.length, candidate.endLine)).join(\"\\n\");\n\t});\n\n\t// Candidate-pool document frequency per term, for the IDF weighting below.\n\tconst documentFrequency = new Map<string, number>();\n\tfor (const term of terms) {\n\t\tdocumentFrequency.set(term, windows.filter((w) => w?.includes(term)).length);\n\t}\n\tconst termWeight = new Map(\n\t\tterms.map((t) => [t, inverseDocumentFrequency(documentFrequency.get(t) ?? 0, candidates.length)]),\n\t);\n\tconst totalTermWeight = terms.reduce((sum, t) => sum + (termWeight.get(t) ?? 0), 0);\n\n\t// Fused prior normalized by score, not by position: a candidate both\n\t// retrievers agreed on should outrank one that squeaked in, and a uniform\n\t// 1 - index/length ramp throws that magnitude away.\n\tconst maxRrfScore = Math.max(...candidates.map((c) => c.rrfScore), Number.MIN_VALUE);\n\n\tconst scored = candidates.map((candidate, index) => {\n\t\tconst fusedPrior = candidate.rrfScore / maxRrfScore;\n\n\t\tconst window = windows[index];\n\t\tlet termCoverage = 0;\n\t\tlet declaresAnyTerm = false;\n\t\tif (window && terms.length > 0) {\n\t\t\tconst present = terms.filter((t) => window.includes(t));\n\t\t\ttermCoverage =\n\t\t\t\ttotalTermWeight > 0\n\t\t\t\t\t? present.reduce((sum, t) => sum + (termWeight.get(t) ?? 0), 0) / totalTermWeight\n\t\t\t\t\t: present.length / terms.length;\n\t\t\tdeclaresAnyTerm = present.some((t) => declaresTerm(window, t));\n\t\t}\n\n\t\tconst lowerPath = candidate.path.toLowerCase();\n\t\t// A quoted phrase rarely names a file; split it into path-ish tokens so\n\t\t// `\"token budget exceeded\"` still gets partial path credit.\n\t\tconst pathTerms = terms.length === 1 ? terms[0].split(/[^a-z0-9_$]+/).filter((t) => t.length >= 3) : terms;\n\t\tconst pathAffinity =\n\t\t\tpathTerms.length > 0 ? pathTerms.filter((t) => lowerPath.includes(t)).length / pathTerms.length : 0;\n\n\t\tconst exactPath =\n\t\t\tqueryPath.length >= 3 && (lowerPath === queryPath || lowerPath.endsWith(`/${queryPath}`)) ? 1 : 0;\n\n\t\tconst score =\n\t\t\tWEIGHT_FUSED_PRIOR * fusedPrior +\n\t\t\tWEIGHT_TERM_COVERAGE * termCoverage +\n\t\t\tWEIGHT_PATH_AFFINITY * pathAffinity +\n\t\t\tEXACT_PATH_BONUS * exactPath +\n\t\t\t(declaresAnyTerm ? DECLARATION_BONUS : 0);\n\t\treturn { candidate, index, score };\n\t});\n\n\t// Stable, deterministic: score desc, fused order as tie-break.\n\tscored.sort((a, b) => b.score - a.score || a.index - b.index);\n\treturn { candidates: scored.map((s) => s.candidate), latencyMs: Date.now() - startedMs };\n}\n"]}
@@ -8,13 +8,26 @@
8
8
  */
9
9
  import type { FusedHit, RankedHit } from "./types.js";
10
10
  /**
11
- * Default RRF constant. The literature folklore default is 60; the eval gate
12
- * (scripts/search-eval.mjs, 12-query gold set) measured k ∈ {0, 2} beating
13
- * k = 60 on every differing query, twice, with reranking on top of either —
14
- * small k keeps fusion top-heavy toward each retriever's best hits, and the
15
- * reranker corrects the tail. Small sample: re-sweep when the gold set grows.
11
+ * Default RRF constant, re-swept on the 62-query harness.
12
+ *
13
+ * History matters here, because the answer changed twice. The original
14
+ * 12-query gold set picked k = 2 over k = 60; that measurement ran on an
15
+ * unpinned corpus with a file-level metric and was never reproducible. The
16
+ * 62-query re-sweep then found the two *indistinguishable* — a 3pp R@10 gap
17
+ * carried by two queries (p = 0.50), with MRR worse on more queries than it
18
+ * was better.
19
+ *
20
+ * What made k decidable was fixing the reranker. Once it could tell a
21
+ * declaration from a call site, the deeper, flatter candidate mix that k = 60
22
+ * produces became worth having: MRR 0.403 -> 0.464, 20 queries better against
23
+ * 7 worse (p <= 0.05), with R@10 unchanged. Small k keeps fusion top-heavy
24
+ * toward each retriever's best hits, which only pays when the reranker cannot
25
+ * exploit the tail — and it now can.
26
+ *
27
+ * Re-sweep again (`bun run search-eval:compare`) after any reranker change,
28
+ * since that is what this value trades against.
16
29
  */
17
- export declare const DEFAULT_RRF_K = 2;
30
+ export declare const DEFAULT_RRF_K = 60;
18
31
  /**
19
32
  * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.
20
33
  *
@@ -1 +1 @@
1
- {"version":3,"file":"rrf.d.ts","sourceRoot":"","sources":["../../../src/core/search/rrf.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,SAAgB,GAAG,QAAQ,EAAE,CA6C/F","sourcesContent":["/**\n * Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid\n * search (docs/hybrid-retrieval-design.md, Decision 2).\n *\n * Rank-only on purpose: BM25 and cosine scores are not comparable across\n * retrievers, so raw scores are carried through as diagnostics but never\n * enter the fused score.\n */\n\nimport type { FusedHit, RankedHit } from \"./types.js\";\n\n/**\n * Default RRF constant. The literature folklore default is 60; the eval gate\n * (scripts/search-eval.mjs, 12-query gold set) measured k {0, 2} beating\n * k = 60 on every differing query, twice, with reranking on top of either —\n * small k keeps fusion top-heavy toward each retriever's best hits, and the\n * reranker corrects the tail. Small sample: re-sweep when the gold set grows.\n */\nexport const DEFAULT_RRF_K = 2;\n\n/**\n * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.\n *\n * Ties break by number of agreeing retrievers, then lexicographic id, so the\n * same inputs always produce the same context.\n *\n * Duplicate `source:id` pairs within one list are counted once (best rank\n * wins). The adapter dedupes upstream, so this guard should never fire in\n * practice — it exists so a misbehaving retriever cannot inflate its vote.\n */\nexport function rrfFuse(lists: readonly (readonly RankedHit[])[], k = DEFAULT_RRF_K): FusedHit[] {\n\tif (!Number.isFinite(k) || k < 0) {\n\t\tthrow new Error(`RRF k must be a finite non-negative number; got ${k}`);\n\t}\n\n\tconst acc = new Map<string, FusedHit>();\n\n\tfor (const list of lists) {\n\t\t// Collapse duplicates to their best rank first, so the single vote a\n\t\t// duplicated id gets is cast at the best rank regardless of emit order.\n\t\tconst collapsed = new Map<string, RankedHit>();\n\t\tfor (const hit of list) {\n\t\t\tif (!Number.isInteger(hit.rank) || hit.rank < 1) {\n\t\t\t\tthrow new Error(`RRF rank must be a positive integer; got ${hit.rank}`);\n\t\t\t}\n\t\t\tconst dedupeKey = `${hit.source}:${hit.id}`;\n\t\t\tconst existing = collapsed.get(dedupeKey);\n\t\t\tif (!existing || hit.rank < existing.rank) collapsed.set(dedupeKey, hit);\n\t\t}\n\n\t\tfor (const hit of collapsed.values()) {\n\t\t\tlet current = acc.get(hit.id);\n\t\t\tif (!current) {\n\t\t\t\tcurrent = { id: hit.id, rrfScore: 0, ranks: {}, rawScores: {} };\n\t\t\t\tacc.set(hit.id, current);\n\t\t\t}\n\n\t\t\tcurrent.rrfScore += 1 / (k + hit.rank);\n\n\t\t\tconst oldRank = current.ranks[hit.source];\n\t\t\tif (oldRank === undefined || hit.rank < oldRank) {\n\t\t\t\tcurrent.ranks[hit.source] = hit.rank;\n\t\t\t\t// rawScores follows the best rank; a best-ranked hit without a\n\t\t\t\t// score leaves any earlier score in place rather than erasing it.\n\t\t\t\tif (hit.score !== undefined) current.rawScores[hit.source] = hit.score;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()].sort(\n\t\t(a, b) =>\n\t\t\tb.rrfScore - a.rrfScore ||\n\t\t\tObject.keys(b.ranks).length - Object.keys(a.ranks).length ||\n\t\t\ta.id.localeCompare(b.id),\n\t);\n}\n"]}
1
+ {"version":3,"file":"rrf.d.ts","sourceRoot":"","sources":["../../../src/core/search/rrf.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,SAAgB,GAAG,QAAQ,EAAE,CA6C/F","sourcesContent":["/**\n * Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid\n * search (docs/hybrid-retrieval-design.md, Decision 2).\n *\n * Rank-only on purpose: BM25 and cosine scores are not comparable across\n * retrievers, so raw scores are carried through as diagnostics but never\n * enter the fused score.\n */\n\nimport type { FusedHit, RankedHit } from \"./types.js\";\n\n/**\n * Default RRF constant, re-swept on the 62-query harness.\n *\n * History matters here, because the answer changed twice. The original\n * 12-query gold set picked k = 2 over k = 60; that measurement ran on an\n * unpinned corpus with a file-level metric and was never reproducible. The\n * 62-query re-sweep then found the two *indistinguishable* — a 3pp R@10 gap\n * carried by two queries (p = 0.50), with MRR worse on more queries than it\n * was better.\n *\n * What made k decidable was fixing the reranker. Once it could tell a\n * declaration from a call site, the deeper, flatter candidate mix that k = 60\n * produces became worth having: MRR 0.403 -> 0.464, 20 queries better against\n * 7 worse (p <= 0.05), with R@10 unchanged. Small k keeps fusion top-heavy\n * toward each retriever's best hits, which only pays when the reranker cannot\n * exploit the tail and it now can.\n *\n * Re-sweep again (`bun run search-eval:compare`) after any reranker change,\n * since that is what this value trades against.\n */\nexport const DEFAULT_RRF_K = 60;\n\n/**\n * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.\n *\n * Ties break by number of agreeing retrievers, then lexicographic id, so the\n * same inputs always produce the same context.\n *\n * Duplicate `source:id` pairs within one list are counted once (best rank\n * wins). The adapter dedupes upstream, so this guard should never fire in\n * practice — it exists so a misbehaving retriever cannot inflate its vote.\n */\nexport function rrfFuse(lists: readonly (readonly RankedHit[])[], k = DEFAULT_RRF_K): FusedHit[] {\n\tif (!Number.isFinite(k) || k < 0) {\n\t\tthrow new Error(`RRF k must be a finite non-negative number; got ${k}`);\n\t}\n\n\tconst acc = new Map<string, FusedHit>();\n\n\tfor (const list of lists) {\n\t\t// Collapse duplicates to their best rank first, so the single vote a\n\t\t// duplicated id gets is cast at the best rank regardless of emit order.\n\t\tconst collapsed = new Map<string, RankedHit>();\n\t\tfor (const hit of list) {\n\t\t\tif (!Number.isInteger(hit.rank) || hit.rank < 1) {\n\t\t\t\tthrow new Error(`RRF rank must be a positive integer; got ${hit.rank}`);\n\t\t\t}\n\t\t\tconst dedupeKey = `${hit.source}:${hit.id}`;\n\t\t\tconst existing = collapsed.get(dedupeKey);\n\t\t\tif (!existing || hit.rank < existing.rank) collapsed.set(dedupeKey, hit);\n\t\t}\n\n\t\tfor (const hit of collapsed.values()) {\n\t\t\tlet current = acc.get(hit.id);\n\t\t\tif (!current) {\n\t\t\t\tcurrent = { id: hit.id, rrfScore: 0, ranks: {}, rawScores: {} };\n\t\t\t\tacc.set(hit.id, current);\n\t\t\t}\n\n\t\t\tcurrent.rrfScore += 1 / (k + hit.rank);\n\n\t\t\tconst oldRank = current.ranks[hit.source];\n\t\t\tif (oldRank === undefined || hit.rank < oldRank) {\n\t\t\t\tcurrent.ranks[hit.source] = hit.rank;\n\t\t\t\t// rawScores follows the best rank; a best-ranked hit without a\n\t\t\t\t// score leaves any earlier score in place rather than erasing it.\n\t\t\t\tif (hit.score !== undefined) current.rawScores[hit.source] = hit.score;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()].sort(\n\t\t(a, b) =>\n\t\t\tb.rrfScore - a.rrfScore ||\n\t\t\tObject.keys(b.ranks).length - Object.keys(a.ranks).length ||\n\t\t\ta.id.localeCompare(b.id),\n\t);\n}\n"]}
@@ -7,13 +7,26 @@
7
7
  * enter the fused score.
8
8
  */
9
9
  /**
10
- * Default RRF constant. The literature folklore default is 60; the eval gate
11
- * (scripts/search-eval.mjs, 12-query gold set) measured k ∈ {0, 2} beating
12
- * k = 60 on every differing query, twice, with reranking on top of either —
13
- * small k keeps fusion top-heavy toward each retriever's best hits, and the
14
- * reranker corrects the tail. Small sample: re-sweep when the gold set grows.
10
+ * Default RRF constant, re-swept on the 62-query harness.
11
+ *
12
+ * History matters here, because the answer changed twice. The original
13
+ * 12-query gold set picked k = 2 over k = 60; that measurement ran on an
14
+ * unpinned corpus with a file-level metric and was never reproducible. The
15
+ * 62-query re-sweep then found the two *indistinguishable* — a 3pp R@10 gap
16
+ * carried by two queries (p = 0.50), with MRR worse on more queries than it
17
+ * was better.
18
+ *
19
+ * What made k decidable was fixing the reranker. Once it could tell a
20
+ * declaration from a call site, the deeper, flatter candidate mix that k = 60
21
+ * produces became worth having: MRR 0.403 -> 0.464, 20 queries better against
22
+ * 7 worse (p <= 0.05), with R@10 unchanged. Small k keeps fusion top-heavy
23
+ * toward each retriever's best hits, which only pays when the reranker cannot
24
+ * exploit the tail — and it now can.
25
+ *
26
+ * Re-sweep again (`bun run search-eval:compare`) after any reranker change,
27
+ * since that is what this value trades against.
15
28
  */
16
- export const DEFAULT_RRF_K = 2;
29
+ export const DEFAULT_RRF_K = 60;
17
30
  /**
18
31
  * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.
19
32
  *
@@ -1 +1 @@
1
- {"version":3,"file":"rrf.js","sourceRoot":"","sources":["../../../src/core/search/rrf.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid\n * search (docs/hybrid-retrieval-design.md, Decision 2).\n *\n * Rank-only on purpose: BM25 and cosine scores are not comparable across\n * retrievers, so raw scores are carried through as diagnostics but never\n * enter the fused score.\n */\n\nimport type { FusedHit, RankedHit } from \"./types.js\";\n\n/**\n * Default RRF constant. The literature folklore default is 60; the eval gate\n * (scripts/search-eval.mjs, 12-query gold set) measured k {0, 2} beating\n * k = 60 on every differing query, twice, with reranking on top of either —\n * small k keeps fusion top-heavy toward each retriever's best hits, and the\n * reranker corrects the tail. Small sample: re-sweep when the gold set grows.\n */\nexport const DEFAULT_RRF_K = 2;\n\n/**\n * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.\n *\n * Ties break by number of agreeing retrievers, then lexicographic id, so the\n * same inputs always produce the same context.\n *\n * Duplicate `source:id` pairs within one list are counted once (best rank\n * wins). The adapter dedupes upstream, so this guard should never fire in\n * practice — it exists so a misbehaving retriever cannot inflate its vote.\n */\nexport function rrfFuse(lists: readonly (readonly RankedHit[])[], k = DEFAULT_RRF_K): FusedHit[] {\n\tif (!Number.isFinite(k) || k < 0) {\n\t\tthrow new Error(`RRF k must be a finite non-negative number; got ${k}`);\n\t}\n\n\tconst acc = new Map<string, FusedHit>();\n\n\tfor (const list of lists) {\n\t\t// Collapse duplicates to their best rank first, so the single vote a\n\t\t// duplicated id gets is cast at the best rank regardless of emit order.\n\t\tconst collapsed = new Map<string, RankedHit>();\n\t\tfor (const hit of list) {\n\t\t\tif (!Number.isInteger(hit.rank) || hit.rank < 1) {\n\t\t\t\tthrow new Error(`RRF rank must be a positive integer; got ${hit.rank}`);\n\t\t\t}\n\t\t\tconst dedupeKey = `${hit.source}:${hit.id}`;\n\t\t\tconst existing = collapsed.get(dedupeKey);\n\t\t\tif (!existing || hit.rank < existing.rank) collapsed.set(dedupeKey, hit);\n\t\t}\n\n\t\tfor (const hit of collapsed.values()) {\n\t\t\tlet current = acc.get(hit.id);\n\t\t\tif (!current) {\n\t\t\t\tcurrent = { id: hit.id, rrfScore: 0, ranks: {}, rawScores: {} };\n\t\t\t\tacc.set(hit.id, current);\n\t\t\t}\n\n\t\t\tcurrent.rrfScore += 1 / (k + hit.rank);\n\n\t\t\tconst oldRank = current.ranks[hit.source];\n\t\t\tif (oldRank === undefined || hit.rank < oldRank) {\n\t\t\t\tcurrent.ranks[hit.source] = hit.rank;\n\t\t\t\t// rawScores follows the best rank; a best-ranked hit without a\n\t\t\t\t// score leaves any earlier score in place rather than erasing it.\n\t\t\t\tif (hit.score !== undefined) current.rawScores[hit.source] = hit.score;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()].sort(\n\t\t(a, b) =>\n\t\t\tb.rrfScore - a.rrfScore ||\n\t\t\tObject.keys(b.ranks).length - Object.keys(a.ranks).length ||\n\t\t\ta.id.localeCompare(b.id),\n\t);\n}\n"]}
1
+ {"version":3,"file":"rrf.js","sourceRoot":"","sources":["../../../src/core/search/rrf.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reciprocal Rank Fusion — the transparent, rank-only recall layer of hybrid\n * search (docs/hybrid-retrieval-design.md, Decision 2).\n *\n * Rank-only on purpose: BM25 and cosine scores are not comparable across\n * retrievers, so raw scores are carried through as diagnostics but never\n * enter the fused score.\n */\n\nimport type { FusedHit, RankedHit } from \"./types.js\";\n\n/**\n * Default RRF constant, re-swept on the 62-query harness.\n *\n * History matters here, because the answer changed twice. The original\n * 12-query gold set picked k = 2 over k = 60; that measurement ran on an\n * unpinned corpus with a file-level metric and was never reproducible. The\n * 62-query re-sweep then found the two *indistinguishable* — a 3pp R@10 gap\n * carried by two queries (p = 0.50), with MRR worse on more queries than it\n * was better.\n *\n * What made k decidable was fixing the reranker. Once it could tell a\n * declaration from a call site, the deeper, flatter candidate mix that k = 60\n * produces became worth having: MRR 0.403 -> 0.464, 20 queries better against\n * 7 worse (p <= 0.05), with R@10 unchanged. Small k keeps fusion top-heavy\n * toward each retriever's best hits, which only pays when the reranker cannot\n * exploit the tail and it now can.\n *\n * Re-sweep again (`bun run search-eval:compare`) after any reranker change,\n * since that is what this value trades against.\n */\nexport const DEFAULT_RRF_K = 60;\n\n/**\n * Fuse ranked lists into one deterministic ordering by summed `1/(k + rank)`.\n *\n * Ties break by number of agreeing retrievers, then lexicographic id, so the\n * same inputs always produce the same context.\n *\n * Duplicate `source:id` pairs within one list are counted once (best rank\n * wins). The adapter dedupes upstream, so this guard should never fire in\n * practice — it exists so a misbehaving retriever cannot inflate its vote.\n */\nexport function rrfFuse(lists: readonly (readonly RankedHit[])[], k = DEFAULT_RRF_K): FusedHit[] {\n\tif (!Number.isFinite(k) || k < 0) {\n\t\tthrow new Error(`RRF k must be a finite non-negative number; got ${k}`);\n\t}\n\n\tconst acc = new Map<string, FusedHit>();\n\n\tfor (const list of lists) {\n\t\t// Collapse duplicates to their best rank first, so the single vote a\n\t\t// duplicated id gets is cast at the best rank regardless of emit order.\n\t\tconst collapsed = new Map<string, RankedHit>();\n\t\tfor (const hit of list) {\n\t\t\tif (!Number.isInteger(hit.rank) || hit.rank < 1) {\n\t\t\t\tthrow new Error(`RRF rank must be a positive integer; got ${hit.rank}`);\n\t\t\t}\n\t\t\tconst dedupeKey = `${hit.source}:${hit.id}`;\n\t\t\tconst existing = collapsed.get(dedupeKey);\n\t\t\tif (!existing || hit.rank < existing.rank) collapsed.set(dedupeKey, hit);\n\t\t}\n\n\t\tfor (const hit of collapsed.values()) {\n\t\t\tlet current = acc.get(hit.id);\n\t\t\tif (!current) {\n\t\t\t\tcurrent = { id: hit.id, rrfScore: 0, ranks: {}, rawScores: {} };\n\t\t\t\tacc.set(hit.id, current);\n\t\t\t}\n\n\t\t\tcurrent.rrfScore += 1 / (k + hit.rank);\n\n\t\t\tconst oldRank = current.ranks[hit.source];\n\t\t\tif (oldRank === undefined || hit.rank < oldRank) {\n\t\t\t\tcurrent.ranks[hit.source] = hit.rank;\n\t\t\t\t// rawScores follows the best rank; a best-ranked hit without a\n\t\t\t\t// score leaves any earlier score in place rather than erasing it.\n\t\t\t\tif (hit.score !== undefined) current.rawScores[hit.source] = hit.score;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...acc.values()].sort(\n\t\t(a, b) =>\n\t\t\tb.rrfScore - a.rrfScore ||\n\t\t\tObject.keys(b.ranks).length - Object.keys(a.ranks).length ||\n\t\t\ta.id.localeCompare(b.id),\n\t);\n}\n"]}
@@ -7,7 +7,17 @@
7
7
  * query (fusion and span expansion consult the same sidecar snapshot) but not
8
8
  * across edits or rebuilds — never persist them as durable references.
9
9
  */
10
- type RetrieverSource = "grep" | "embed";
10
+ /**
11
+ * A retriever whose ranked list can enter fusion.
12
+ *
13
+ * `bm25` is the daemon's Okapi lexical index, fetched as its own leg via the
14
+ * `retriever: "lexical"` op rather than pre-fused by `query_hybrid`. Keeping
15
+ * it separate from `grep` matters: they are both "lexical" but they fail
16
+ * differently — BM25 has IDF and misses identifiers written in another naming
17
+ * convention, ripgrep has neither IDF nor an index but sees the working tree,
18
+ * including edits made this session.
19
+ */
20
+ type RetrieverSource = "grep" | "embed" | "bm25";
11
21
  export type SearchMode = "auto" | "lexical" | "semantic" | "hybrid";
12
22
  export type ResolvedSearchMode = Exclude<SearchMode, "auto">;
13
23
  export interface RankedHit {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,KAAK,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC;AAExC,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AACpE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAE7D,MAAM,WAAW,SAAS;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,uFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,EAAE,aAAa;CAAG;AAElE;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,UAAU,CAAC;IAC1B,YAAY,EAAE,kBAAkB,CAAC;IACjC,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACtF,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE","sourcesContent":["/**\n * Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).\n *\n * Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)\n * from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for\n * grep hits in files the index does not cover. They are stable within one\n * query (fusion and span expansion consult the same sidecar snapshot) but not\n * across edits or rebuilds — never persist them as durable references.\n */\n\ntype RetrieverSource = \"grep\" | \"embed\";\n\nexport type SearchMode = \"auto\" | \"lexical\" | \"semantic\" | \"hybrid\";\nexport type ResolvedSearchMode = Exclude<SearchMode, \"auto\">;\n\nexport interface RankedHit {\n\tid: string;\n\t/** 1-indexed, gap-free rank within its retriever's list. */\n\trank: number;\n\t/** Retriever-local score (BM25-ish, cosine, …). Diagnostics only — never fused. */\n\tscore?: number;\n\tsource: RetrieverSource;\n}\n\nexport interface FusedHit {\n\tid: string;\n\trrfScore: number;\n\t/** Best rank per contributing retriever. */\n\tranks: Partial<Record<RetrieverSource, number>>;\n\t/** Raw score at the best rank per retriever. Diagnostics only. */\n\trawScores: Partial<Record<RetrieverSource, number>>;\n}\n\n/** Line-span identity a candidate id resolves to, for post-fusion expansion. */\nexport interface CandidateSpan {\n\tpath: string;\n\t/** 1-based inclusive. */\n\tstartLine: number;\n\t/** 1-based inclusive. May exceed the file's length; readers clamp. */\n\tendLine: number;\n}\n\nexport interface FusedCandidate extends FusedHit, CandidateSpan {}\n\n/** Per-call diagnostic record, written to the store-dir trace jsonl — never\n * into model context. */\nexport interface SearchTrace {\n\ttimestampMs: number;\n\tquery: string;\n\trequestedMode: SearchMode;\n\tresolvedMode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a degradation of the requested one. */\n\tdegradedReason?: string;\n\tindexPhase: \"ready\" | \"indexing\" | \"unavailable\";\n\trrfK?: number;\n\tretrievers: Partial<Record<RetrieverSource, { latencyMs: number; hitCount: number }>>;\n\tfused: FusedHit[];\n\trerank?: { applied: boolean; candidateCount: number; latencyMs: number };\n}\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;GASG;AACH,KAAK,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AACpE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAE7D,MAAM,WAAW,SAAS;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,uFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,EAAE,aAAa;CAAG;AAElE;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,UAAU,CAAC;IAC1B,YAAY,EAAE,kBAAkB,CAAC;IACjC,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACtF,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE","sourcesContent":["/**\n * Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).\n *\n * Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)\n * from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for\n * grep hits in files the index does not cover. They are stable within one\n * query (fusion and span expansion consult the same sidecar snapshot) but not\n * across edits or rebuilds — never persist them as durable references.\n */\n\n/**\n * A retriever whose ranked list can enter fusion.\n *\n * `bm25` is the daemon's Okapi lexical index, fetched as its own leg via the\n * `retriever: \"lexical\"` op rather than pre-fused by `query_hybrid`. Keeping\n * it separate from `grep` matters: they are both \"lexical\" but they fail\n * differently — BM25 has IDF and misses identifiers written in another naming\n * convention, ripgrep has neither IDF nor an index but sees the working tree,\n * including edits made this session.\n */\ntype RetrieverSource = \"grep\" | \"embed\" | \"bm25\";\n\nexport type SearchMode = \"auto\" | \"lexical\" | \"semantic\" | \"hybrid\";\nexport type ResolvedSearchMode = Exclude<SearchMode, \"auto\">;\n\nexport interface RankedHit {\n\tid: string;\n\t/** 1-indexed, gap-free rank within its retriever's list. */\n\trank: number;\n\t/** Retriever-local score (BM25-ish, cosine, …). Diagnostics only — never fused. */\n\tscore?: number;\n\tsource: RetrieverSource;\n}\n\nexport interface FusedHit {\n\tid: string;\n\trrfScore: number;\n\t/** Best rank per contributing retriever. */\n\tranks: Partial<Record<RetrieverSource, number>>;\n\t/** Raw score at the best rank per retriever. Diagnostics only. */\n\trawScores: Partial<Record<RetrieverSource, number>>;\n}\n\n/** Line-span identity a candidate id resolves to, for post-fusion expansion. */\nexport interface CandidateSpan {\n\tpath: string;\n\t/** 1-based inclusive. */\n\tstartLine: number;\n\t/** 1-based inclusive. May exceed the file's length; readers clamp. */\n\tendLine: number;\n}\n\nexport interface FusedCandidate extends FusedHit, CandidateSpan {}\n\n/** Per-call diagnostic record, written to the store-dir trace jsonl — never\n * into model context. */\nexport interface SearchTrace {\n\ttimestampMs: number;\n\tquery: string;\n\trequestedMode: SearchMode;\n\tresolvedMode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a degradation of the requested one. */\n\tdegradedReason?: string;\n\tindexPhase: \"ready\" | \"indexing\" | \"unavailable\";\n\trrfK?: number;\n\tretrievers: Partial<Record<RetrieverSource, { latencyMs: number; hitCount: number }>>;\n\tfused: FusedHit[];\n\trerank?: { applied: boolean; candidateCount: number; latencyMs: number };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).\n *\n * Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)\n * from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for\n * grep hits in files the index does not cover. They are stable within one\n * query (fusion and span expansion consult the same sidecar snapshot) but not\n * across edits or rebuilds — never persist them as durable references.\n */\n\ntype RetrieverSource = \"grep\" | \"embed\";\n\nexport type SearchMode = \"auto\" | \"lexical\" | \"semantic\" | \"hybrid\";\nexport type ResolvedSearchMode = Exclude<SearchMode, \"auto\">;\n\nexport interface RankedHit {\n\tid: string;\n\t/** 1-indexed, gap-free rank within its retriever's list. */\n\trank: number;\n\t/** Retriever-local score (BM25-ish, cosine, …). Diagnostics only — never fused. */\n\tscore?: number;\n\tsource: RetrieverSource;\n}\n\nexport interface FusedHit {\n\tid: string;\n\trrfScore: number;\n\t/** Best rank per contributing retriever. */\n\tranks: Partial<Record<RetrieverSource, number>>;\n\t/** Raw score at the best rank per retriever. Diagnostics only. */\n\trawScores: Partial<Record<RetrieverSource, number>>;\n}\n\n/** Line-span identity a candidate id resolves to, for post-fusion expansion. */\nexport interface CandidateSpan {\n\tpath: string;\n\t/** 1-based inclusive. */\n\tstartLine: number;\n\t/** 1-based inclusive. May exceed the file's length; readers clamp. */\n\tendLine: number;\n}\n\nexport interface FusedCandidate extends FusedHit, CandidateSpan {}\n\n/** Per-call diagnostic record, written to the store-dir trace jsonl — never\n * into model context. */\nexport interface SearchTrace {\n\ttimestampMs: number;\n\tquery: string;\n\trequestedMode: SearchMode;\n\tresolvedMode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a degradation of the requested one. */\n\tdegradedReason?: string;\n\tindexPhase: \"ready\" | \"indexing\" | \"unavailable\";\n\trrfK?: number;\n\tretrievers: Partial<Record<RetrieverSource, { latencyMs: number; hitCount: number }>>;\n\tfused: FusedHit[];\n\trerank?: { applied: boolean; candidateCount: number; latencyMs: number };\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * Shared types for hybrid code retrieval (see docs/hybrid-retrieval-design.md).\n *\n * Identity note: candidate ids are per-index-build chunk ids (`relpath#index`)\n * from the embedding sidecar, or synthetic `relpath#L<line>` fallback ids for\n * grep hits in files the index does not cover. They are stable within one\n * query (fusion and span expansion consult the same sidecar snapshot) but not\n * across edits or rebuilds — never persist them as durable references.\n */\n\n/**\n * A retriever whose ranked list can enter fusion.\n *\n * `bm25` is the daemon's Okapi lexical index, fetched as its own leg via the\n * `retriever: \"lexical\"` op rather than pre-fused by `query_hybrid`. Keeping\n * it separate from `grep` matters: they are both \"lexical\" but they fail\n * differently — BM25 has IDF and misses identifiers written in another naming\n * convention, ripgrep has neither IDF nor an index but sees the working tree,\n * including edits made this session.\n */\ntype RetrieverSource = \"grep\" | \"embed\" | \"bm25\";\n\nexport type SearchMode = \"auto\" | \"lexical\" | \"semantic\" | \"hybrid\";\nexport type ResolvedSearchMode = Exclude<SearchMode, \"auto\">;\n\nexport interface RankedHit {\n\tid: string;\n\t/** 1-indexed, gap-free rank within its retriever's list. */\n\trank: number;\n\t/** Retriever-local score (BM25-ish, cosine, …). Diagnostics only — never fused. */\n\tscore?: number;\n\tsource: RetrieverSource;\n}\n\nexport interface FusedHit {\n\tid: string;\n\trrfScore: number;\n\t/** Best rank per contributing retriever. */\n\tranks: Partial<Record<RetrieverSource, number>>;\n\t/** Raw score at the best rank per retriever. Diagnostics only. */\n\trawScores: Partial<Record<RetrieverSource, number>>;\n}\n\n/** Line-span identity a candidate id resolves to, for post-fusion expansion. */\nexport interface CandidateSpan {\n\tpath: string;\n\t/** 1-based inclusive. */\n\tstartLine: number;\n\t/** 1-based inclusive. May exceed the file's length; readers clamp. */\n\tendLine: number;\n}\n\nexport interface FusedCandidate extends FusedHit, CandidateSpan {}\n\n/** Per-call diagnostic record, written to the store-dir trace jsonl — never\n * into model context. */\nexport interface SearchTrace {\n\ttimestampMs: number;\n\tquery: string;\n\trequestedMode: SearchMode;\n\tresolvedMode: ResolvedSearchMode;\n\t/** Set when the resolved mode is a degradation of the requested one. */\n\tdegradedReason?: string;\n\tindexPhase: \"ready\" | \"indexing\" | \"unavailable\";\n\trrfK?: number;\n\tretrievers: Partial<Record<RetrieverSource, { latencyMs: number; hitCount: number }>>;\n\tfused: FusedHit[];\n\trerank?: { applied: boolean; candidateCount: number; latencyMs: number };\n}\n"]}
@@ -48,9 +48,6 @@ export declare const DEFAULT_SETTINGS: {
48
48
  enablePluginTools: false;
49
49
  deferMcpSchemas: true;
50
50
  enableWebTools: false;
51
- enableBrowserTools: false;
52
- enableBrowserLivePreview: false;
53
- enableFileTools: false;
54
51
  enableEmbsearchTools: true;
55
52
  embsearchThresholdBytes: number;
56
53
  light: false;
@@ -1 +1 @@
1
- {"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
1
+ {"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiFT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
@@ -48,9 +48,6 @@ export const DEFAULT_SETTINGS = {
48
48
  enablePluginTools: false,
49
49
  deferMcpSchemas: true,
50
50
  enableWebTools: false,
51
- enableBrowserTools: false,
52
- enableBrowserLivePreview: false,
53
- enableFileTools: false,
54
51
  enableEmbsearchTools: true,
55
52
  embsearchThresholdBytes: 0,
56
53
  light: false,
@@ -1 +1 @@
1
- {"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,KAAK,EAAE;QACN,SAAS,EAAE,GAAG;KACd;IACD,QAAQ,EAAE;QACT,WAAW,EAAE,EAAE;KACf;IACD,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,kBAAkB,EAAE,KAAK;IACzB,wBAAwB,EAAE,KAAK;IAC/B,eAAe,EAAE,KAAK;IACtB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;QAC3B,mBAAmB,EAAE,KAAK;KAC1B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
1
+ {"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,KAAK,EAAE;QACN,SAAS,EAAE,GAAG;KACd;IACD,QAAQ,EAAE;QACT,WAAW,EAAE,EAAE;KACf;IACD,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;QAC3B,mBAAmB,EAAE,KAAK;KAC1B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tvoice: {\n\t\tsilenceMs: 800,\n\t},\n\twebtools: {\n\t\ttimeoutSecs: 15,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorBorder: \"box\",\n\teditorPaddingX: 1,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
@@ -169,12 +169,6 @@ export declare class SettingsManager {
169
169
  setEnableEmbsearchTools(enabled: boolean): void;
170
170
  getEmbsearchBinaryPath(): string | undefined;
171
171
  getEmbsearchThresholdBytes(): number;
172
- getEnableBrowserTools(): boolean;
173
- setEnableBrowserTools(enabled: boolean): void;
174
- getEnableBrowserLivePreview(): boolean;
175
- setEnableBrowserLivePreview(enabled: boolean): void;
176
- getEnableFileTools(): boolean;
177
- setEnableFileTools(enabled: boolean): void;
178
172
  getLight(): boolean;
179
173
  setLight(enabled: boolean): void;
180
174
  getThinkingBudgets(): ThinkingBudgetsSettings | undefined;