@kolisachint/hoocode-agent 0.4.164 → 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 (48) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/core/embsearch/client.d.ts +18 -2
  3. package/dist/core/embsearch/client.d.ts.map +1 -1
  4. package/dist/core/embsearch/client.js +20 -3
  5. package/dist/core/embsearch/client.js.map +1 -1
  6. package/dist/core/embsearch/embsearch-service.d.ts +18 -1
  7. package/dist/core/embsearch/embsearch-service.d.ts.map +1 -1
  8. package/dist/core/embsearch/embsearch-service.js +52 -4
  9. package/dist/core/embsearch/embsearch-service.js.map +1 -1
  10. package/dist/core/search/eval-compare.d.ts +57 -0
  11. package/dist/core/search/eval-compare.d.ts.map +1 -0
  12. package/dist/core/search/eval-compare.js +114 -0
  13. package/dist/core/search/eval-compare.js.map +1 -0
  14. package/dist/core/search/eval-gold.d.ts +47 -0
  15. package/dist/core/search/eval-gold.d.ts.map +1 -0
  16. package/dist/core/search/eval-gold.js +172 -0
  17. package/dist/core/search/eval-gold.js.map +1 -0
  18. package/dist/core/search/eval-harness.d.ts +140 -0
  19. package/dist/core/search/eval-harness.d.ts.map +1 -0
  20. package/dist/core/search/eval-harness.js +225 -0
  21. package/dist/core/search/eval-harness.js.map +1 -0
  22. package/dist/core/search/eval.d.ts +66 -8
  23. package/dist/core/search/eval.d.ts.map +1 -1
  24. package/dist/core/search/eval.js +67 -12
  25. package/dist/core/search/eval.js.map +1 -1
  26. package/dist/core/search/hybrid-search.d.ts +16 -0
  27. package/dist/core/search/hybrid-search.d.ts.map +1 -1
  28. package/dist/core/search/hybrid-search.js +54 -2
  29. package/dist/core/search/hybrid-search.js.map +1 -1
  30. package/dist/core/search/mode.d.ts +21 -5
  31. package/dist/core/search/mode.d.ts.map +1 -1
  32. package/dist/core/search/mode.js +23 -10
  33. package/dist/core/search/mode.js.map +1 -1
  34. package/dist/core/search/rerank.d.ts.map +1 -1
  35. package/dist/core/search/rerank.js +70 -11
  36. package/dist/core/search/rerank.js.map +1 -1
  37. package/dist/core/search/rrf.d.ts +19 -6
  38. package/dist/core/search/rrf.d.ts.map +1 -1
  39. package/dist/core/search/rrf.js +19 -6
  40. package/dist/core/search/rrf.js.map +1 -1
  41. package/dist/core/search/types.d.ts +11 -1
  42. package/dist/core/search/types.d.ts.map +1 -1
  43. package/dist/core/search/types.js.map +1 -1
  44. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  45. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  46. package/examples/extensions/sandbox/package.json +1 -1
  47. package/examples/extensions/with-deps/package.json +1 -1
  48. package/package.json +7 -4
@@ -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"]}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.2.161",
4
+ "version": "0.2.162",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.2.161",
4
+ "version": "0.2.162",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.2.161",
4
+ "version": "0.2.162",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.2.161",
4
+ "version": "0.2.162",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-agent",
3
- "version": "0.4.164",
3
+ "version": "0.4.165",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "hoocodeConfig": {
@@ -42,12 +42,15 @@
42
42
  "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx mkdir -p dist/core/extensions/plugins && shx cp -r src/core/extensions/plugins/default-marketplace dist/core/extensions/plugins/",
43
43
  "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
44
44
  "test": "vitest --run",
45
+ "search-eval": "bun scripts/search-eval.ts",
46
+ "search-eval:gold": "bun scripts/search-eval-gold.ts",
47
+ "search-eval:compare": "bun scripts/search-eval-compare.ts",
45
48
  "prepublishOnly": "npm run clean && npm run build"
46
49
  },
47
50
  "dependencies": {
48
- "@kolisachint/hoocode-agent-core": "^0.4.164",
49
- "@kolisachint/hoocode-ai": "^0.4.164",
50
- "@kolisachint/hoocode-tui": "^0.4.164",
51
+ "@kolisachint/hoocode-agent-core": "^0.4.165",
52
+ "@kolisachint/hoocode-ai": "^0.4.165",
53
+ "@kolisachint/hoocode-tui": "^0.4.165",
51
54
  "@silvia-odwyer/photon-node": "^0.3.4",
52
55
  "chalk": "^5.5.0",
53
56
  "cli-highlight": "^2.1.11",