Skip to content

perf: establish objective-aware retrieval benchmark baseline - #161

Merged
Lucas-Bur merged 21 commits into
mainfrom
feat/158-retrieval-quality-bench
Aug 1, 2026
Merged

perf: establish objective-aware retrieval benchmark baseline#161
Lucas-Bur merged 21 commits into
mainfrom
feat/158-retrieval-quality-bench

Conversation

@Lucas-Bur

@Lucas-Bur Lucas-Bur commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add schema 16 with an explicit production-RRF holdout baseline, Recall@50, and objective-specific router candidates for direct, reranker-top20, and reranker-top50 retrieval.
  • Share Pareto/successive-halving search state across objectives and enforce a 1% development guardrail against production RRF.
  • Record the schema-16 baseline, extend the historical HTML visualization, and document the benchmark contract for the next sparse-embedder experiment.
  • Keep reranker implementation and sparse production integration out of this change.

Validation

  • vp check
  • vp test (415 passed)
  • vp test --config benchmarks/vite.config.ts --run benchmarks/tests/channels.test.ts (16 passed)
  • vp test --config benchmarks/vite.config.ts --run benchmarks/tests/retrieval.test.ts -t runs-the-smoke-retrieval-profile (passed)
  • Schema-16 validate artifact: benchmarks/results/retrieval-2026-07-31T23-54-20.351Z.json

Fallow still reports the repository's known dependency, duplication, and complexity health findings; dead exports are at 0.0%.

Summary by CodeRabbit

  • New Features

    • Added an opt-in retrieval-quality benchmark suite with multiple repositories, search strategies, fusion methods, validation profiles, caching, metrics, and report generation.
    • Added automatic embedding-device selection with fallback across available devices.
    • Improved query routing for short, medium, and long searches.
    • Added configurable text chunking support.
  • Documentation

    • Added benchmark guides, baseline findings, corpus manifests, visual results, and updated vector-search architecture records.
  • Tests

    • Added comprehensive coverage for benchmark workflows, routing, fusion, metrics, corpus validation, and deterministic validation splits.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added an opt-in retrieval-quality benchmark with pinned corpora, retrieval metrics, fusion and evidence-router optimization, embedding caching, SQLite-backed execution, reports, tests, and baseline visualization. Shared query routing and automatic embedder selection were added. The JavaScript dense scorer was removed.

Changes

Retrieval benchmark foundation

Layer / File(s) Summary
Benchmark contracts and corpora
benchmarks/retrieval/types.ts, benchmarks/corpus/*, benchmarks/README.md, benchmarks/BASELINE.md
Defines versioned benchmark artifacts, profiles, validation strategies, corpus manifests, and baseline procedures for three repositories.
Corpus preparation and runtime integration
benchmarks/retrieval/corpus.ts, benchmarks/retrieval/prepare.ts, benchmarks/retrieval/embedding-cache.ts, benchmarks/retrieval/sqlite-index.ts, src/services/chunker.ts, src/services/embedder.ts
Prepares pinned repositories, chunks source files, builds indexes, caches embeddings, runs SQLite-backed retrieval, and selects embedding devices automatically.
Ranking, metrics, fusion, and routing
benchmarks/retrieval/ranking.ts, benchmarks/retrieval/metrics.ts, benchmarks/retrieval/fusion.ts, benchmarks/retrieval/evidence-router.ts, benchmarks/retrieval/folds.ts, src/lib/retrieval/routing.ts
Adds retrieval variants, lexical channels, target resolution, quality metrics, score fusion, evidence-based routing, and deterministic grouped folds.
Optimization and orchestration
benchmarks/retrieval/weight-search.ts, benchmarks/retrieval/runner.ts, benchmarks/retrieval/report.ts
Runs benchmark profiles, evaluates retrieval variants, searches static and dynamic weights, evaluates holdouts, and writes JSON and Markdown artifacts.
Validation and visualization
benchmarks/tests/*, benchmarks/plot-baseline.html, package.json
Adds benchmark integration tests, determinism and holdout-isolation checks, profile scripts, and an interactive baseline dashboard.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • Lucas-Bur/pix#113 — Introduced the hybrid BM25 and dense retrieval architecture extended by this benchmark.
  • Lucas-Bur/pix#138 — Added the identifier channels evaluated and routed by this benchmark.
  • Lucas-Bur/pix#152 — Added the SQLite and sqlite-vector IndexStore path used for dense benchmark retrieval.

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant Corpus
  participant Cache
  participant SQLite
  participant Report
  Runner->>Corpus: Prepare pinned repository
  Corpus->>Cache: Load or write embeddings
  Runner->>SQLite: Persist indexes and execute searches
  SQLite-->>Runner: Return rankings and measurements
  Runner->>Report: Render benchmark artifacts
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's main change: establishing an objective-aware retrieval benchmark baseline.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/158-retrieval-quality-bench

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.68421% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/services/embedder.ts 38.46% 8 Missing ⚠️
src/lib/retrieval/routing.ts 92.30% 1 Missing ⚠️
src/services/chunker.ts 90.90% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
benchmarks/retrieval/weight-search.ts (2)

866-897: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Build the validation evidence once per call.

prepareEvidenceSamples(validation) runs inside the map callback, so it repeats for every objective in ROUTER_OBJECTIVES. buildRoutingEvidence sorts the dense score list per sample, so this triples the cost without changing the result. Hoist it above the map.

♻️ Proposed refactor
   const dynamicSelection = selectBestEvidenceRouter(development, fusion)
   const productionValidation = summarizeProductionRrf(validation)
+  const validationEvidence = prepareEvidenceSamples(validation)
   return dynamicSelection.selections.map((selection) => {
@@
-      validation: summarizeEvidenceRouter(
-        prepareEvidenceSamples(validation),
-        selection.config,
-        fusion,
-      ),
+      validation: summarizeEvidenceRouter(validationEvidence, selection.config, fusion),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/retrieval/weight-search.ts` around lines 866 - 897, Hoist the
prepareEvidenceSamples(validation) call out of the
dynamicSelection.selections.map callback and compute it once before mapping.
Reuse the resulting evidence collection in each summarizeEvidenceRouter call
while preserving the existing selection results and validation behavior.

45-48: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bind the Halton prime count to the parameter count.

coefficientParameters currently has exactly 28 entries (7 influences × 4 channels), and HALTON_PRIMES has exactly 28 primes. If a future influence is added, HALTON_PRIMES[parameterIndex] becomes undefined. The non-null assertion then hides the fault: radicalInverse(index, undefined) returns 0-based NaN arithmetic, valueIndex becomes NaN, and values[NaN]! yields undefined. The scout seeds are then silently corrupted instead of failing.

Add an explicit guard so the mismatch fails fast.

♻️ Proposed guard
   if (baseSeeds.length === 0) return []
   const coefficientParameters = parameters.slice(CHANNELS.length)
+  if (coefficientParameters.length > HALTON_PRIMES.length)
+    throw new Error(
+      `Halton sequence needs ${coefficientParameters.length} primes, got ${HALTON_PRIMES.length}`,
+    )

Also applies to: 563-578

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/retrieval/weight-search.ts` around lines 45 - 48, Add an explicit
length validation tying HALTON_PRIMES to coefficientParameters before the Halton
sampling loop uses HALTON_PRIMES[parameterIndex], and fail immediately with a
clear error when the counts differ. Remove or supersede the non-null assertion
so an out-of-range prime cannot silently produce invalid scout seeds; preserve
the existing sampling behavior when the lengths match.
benchmarks/retrieval/report.ts (1)

20-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Merge the three weighted average helpers.

weightedRouterAverage, weightedFusionAverage, and weightedProductionAverage have identical bodies. They differ only in the row type. Replace them with one generic helper over rows that expose validationQueries.

♻️ Proposed refactor
-const weightedRouterAverage = (
-  rows: readonly EvidenceRouterSearchResult[],
-  select: (row: EvidenceRouterSearchResult) => number,
-): number => {
-  const samples = rows.reduce((sum, row) => sum + row.validationQueries, 0)
-  return samples === 0
-    ? 0
-    : rows.reduce((sum, row) => sum + select(row) * row.validationQueries, 0) / samples
-}
-
-const weightedFusionAverage = (
-  rows: readonly FusionSearchResult[],
-  select: (row: FusionSearchResult) => number,
-): number => {
-  const samples = rows.reduce((sum, row) => sum + row.validationQueries, 0)
-  return samples === 0
-    ? 0
-    : rows.reduce((sum, row) => sum + select(row) * row.validationQueries, 0) / samples
-}
-
-const weightedProductionAverage = (
-  rows: readonly ProductionRrfSearchResult[],
-  select: (row: ProductionRrfSearchResult) => number,
-): number => {
-  const samples = rows.reduce((sum, row) => sum + row.validationQueries, 0)
-  return samples === 0
-    ? 0
-    : rows.reduce((sum, row) => sum + select(row) * row.validationQueries, 0) / samples
-}
+const weightedAverage = <T extends { readonly validationQueries: number }>(
+  rows: readonly T[],
+  select: (row: T) => number,
+): number => {
+  const samples = rows.reduce((sum, row) => sum + row.validationQueries, 0)
+  return samples === 0
+    ? 0
+    : rows.reduce((sum, row) => sum + select(row) * row.validationQueries, 0) / samples
+}

Then replace each call site with weightedAverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/retrieval/report.ts` around lines 20 - 48, Merge
weightedRouterAverage, weightedFusionAverage, and weightedProductionAverage into
a single generic weightedAverage helper constrained to row objects exposing
validationQueries and accepting a row selector. Update every call site to use
weightedAverage while preserving the existing zero-sample handling and weighted
calculation.
benchmarks/retrieval/runner.ts (1)

384-388: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the fold key builder from folds.ts.

This line rebuilds the grouped-fold key with the literal separator \0. benchmarks/retrieval/folds.ts owns the same format in its private foldKey helper. If either side changes, the lookup fails at runtime and only the groupedFold === undefined guard reports it. Export foldKey from folds.ts and call it here.

♻️ Proposed refactor
-import { assignGroupedFolds } from "./folds.js"
+import { assignGroupedFolds, foldKey } from "./folds.js"
@@
-              const groupedFold = groupedFoldAssignments.get(`${manifest.id}\0${question.id}`)
+              const groupedFold = groupedFoldAssignments.get(foldKey(manifest.id, question.id))

In benchmarks/retrieval/folds.ts:

-const foldKey = (repositoryId: string, questionId: string): string =>
+/** Key identifying one question inside one repository manifest. */
+export const foldKey = (repositoryId: string, questionId: string): string =>
   `${repositoryId}\0${questionId}`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/retrieval/runner.ts` around lines 384 - 388, Export the existing
foldKey helper from folds.ts, then update the groupedFoldAssignments lookup in
the runner flow to call foldKey(manifest.id, question.id) instead of rebuilding
the key with a literal separator. Keep the existing undefined guard and failure
behavior unchanged.
benchmarks/retrieval/sqlite-index.ts (1)

30-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider flattening the nested Layer.provideMerge calls with .pipe(...).

The two nested Layer.provideMerge calls are harder to read than a .pipe(Layer.provideMerge(...), Layer.provideMerge(...)) chain, which is the idiomatic Effect composition style shown in Effect's own layer-composition examples.

♻️ Proposed readability refactor
-const sqliteBenchmarkIndexLayer = (model: string, dtype: EmbeddingDtype) =>
-  Layer.provideMerge(
-    Layer.provideMerge(
-      SqliteIndexStoreBase,
-      Layer.merge(
-        Layer.succeed(ConfigStore, benchmarkConfigStore(benchmarkConfig(model, dtype))),
-        sqliteIndexDatabaseLayer(":memory:"),
-      ),
-    ),
-    layerNoop({}),
-  )
+const sqliteBenchmarkIndexLayer = (model: string, dtype: EmbeddingDtype) =>
+  SqliteIndexStoreBase.pipe(
+    Layer.provideMerge(
+      Layer.merge(
+        Layer.succeed(ConfigStore, benchmarkConfigStore(benchmarkConfig(model, dtype))),
+        sqliteIndexDatabaseLayer(":memory:"),
+      ),
+    ),
+    Layer.provideMerge(layerNoop({})),
+  )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/retrieval/sqlite-index.ts` around lines 30 - 40, Refactor
sqliteBenchmarkIndexLayer to flatten the nested Layer.provideMerge calls into a
.pipe(...) composition chain. Preserve the existing merge order and all layers,
including SqliteIndexStoreBase, the benchmark configuration/database layer, and
layerNoop({}).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/plot-baseline.html`:
- Around line 244-297: Move the r50 object out of the mini.r20 object so it
becomes a sibling of r20, r10, and ctx under mini, matching the structure used
by bge. Preserve the existing r50 grouped, loro, and fitall data so
renderModelPanel can access DATA.mini.r50 directly.

In `@benchmarks/retrieval/runner.ts`:
- Around line 131-138: Update selectManifests to validate every identifier from
PIX_BENCH_REPOS against manifests, matching the unknown-value validation
behavior already implemented by selectModels. Reject unknown identifiers before
filtering, while preserving the existing smoke-profile default and
valid-selection behavior.
- Around line 307-320: Update the unresolved-target validation in the
manifest.questions mapping to return an Effect.fail with the existing Error
message instead of throwing directly. Preserve the current question ID and
unresolved target details, matching the typed failure pattern used by
runRetrievalBenchmark’s neighboring failure paths.

---

Nitpick comments:
In `@benchmarks/retrieval/report.ts`:
- Around line 20-48: Merge weightedRouterAverage, weightedFusionAverage, and
weightedProductionAverage into a single generic weightedAverage helper
constrained to row objects exposing validationQueries and accepting a row
selector. Update every call site to use weightedAverage while preserving the
existing zero-sample handling and weighted calculation.

In `@benchmarks/retrieval/runner.ts`:
- Around line 384-388: Export the existing foldKey helper from folds.ts, then
update the groupedFoldAssignments lookup in the runner flow to call
foldKey(manifest.id, question.id) instead of rebuilding the key with a literal
separator. Keep the existing undefined guard and failure behavior unchanged.

In `@benchmarks/retrieval/sqlite-index.ts`:
- Around line 30-40: Refactor sqliteBenchmarkIndexLayer to flatten the nested
Layer.provideMerge calls into a .pipe(...) composition chain. Preserve the
existing merge order and all layers, including SqliteIndexStoreBase, the
benchmark configuration/database layer, and layerNoop({}).

In `@benchmarks/retrieval/weight-search.ts`:
- Around line 866-897: Hoist the prepareEvidenceSamples(validation) call out of
the dynamicSelection.selections.map callback and compute it once before mapping.
Reuse the resulting evidence collection in each summarizeEvidenceRouter call
while preserving the existing selection results and validation behavior.
- Around line 45-48: Add an explicit length validation tying HALTON_PRIMES to
coefficientParameters before the Halton sampling loop uses
HALTON_PRIMES[parameterIndex], and fail immediately with a clear error when the
counts differ. Remove or supersede the non-null assertion so an out-of-range
prime cannot silently produce invalid scout seeds; preserve the existing
sampling behavior when the lengths match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b12a35b5-4c5c-44d5-9649-157c0e5312fc

📥 Commits

Reviewing files that changed from the base of the PR and between cacfdeb and 0c05bc2.

⛔ Files ignored due to path filters (1)
  • .gitignore is excluded by !.gitignore
📒 Files selected for processing (38)
  • CONTEXT.md
  • benchmarks/BASELINE.md
  • benchmarks/README.md
  • benchmarks/corpus/effect-v4.json
  • benchmarks/corpus/fastapi.json
  • benchmarks/corpus/fd.json
  • benchmarks/plot-baseline.html
  • benchmarks/retrieval/corpus.ts
  • benchmarks/retrieval/embedding-cache.ts
  • benchmarks/retrieval/evidence-router.ts
  • benchmarks/retrieval/folds.ts
  • benchmarks/retrieval/fusion.ts
  • benchmarks/retrieval/metrics.ts
  • benchmarks/retrieval/prepare.ts
  • benchmarks/retrieval/ranking.ts
  • benchmarks/retrieval/report.ts
  • benchmarks/retrieval/runner.ts
  • benchmarks/retrieval/sqlite-index.ts
  • benchmarks/retrieval/types.ts
  • benchmarks/retrieval/weight-search.ts
  • benchmarks/tests/channels.test.ts
  • benchmarks/tests/corpus.test.ts
  • benchmarks/tests/retrieval.test.ts
  • benchmarks/tsconfig.json
  • benchmarks/vite.config.ts
  • docs/adr/0008-embedding-internal-representation.md
  • docs/adr/0018-sqlite-index-and-vector-search.md
  • package.json
  • src/application/query-project.ts
  • src/lib/retrieval/dense.test.ts
  • src/lib/retrieval/dense.ts
  • src/lib/retrieval/routing.ts
  • src/lib/vectors/cosine.test.ts
  • src/lib/vectors/cosine.ts
  • src/services/chunker.ts
  • src/services/device-detect.ts
  • src/services/embedder.ts
  • src/services/index-store.test.ts
💤 Files with no reviewable changes (5)
  • src/lib/vectors/cosine.ts
  • src/lib/retrieval/dense.test.ts
  • src/lib/retrieval/dense.ts
  • src/lib/vectors/cosine.test.ts
  • src/services/index-store.test.ts

Comment on lines +244 to +297
r20: {
grouped: [
{ s: 2, v: 78.9, m: "Ø der 4 Query-Formen (per-Form Gewichte)" },
{ s: 3, v: 77.5, m: "Evidence-Router (gegruppt)" },
{ s: 4, v: 78.0, m: "dynamischer Router (gegruppt, Vergleich im BGE-Run)" },
{ s: 5, v: 78.1, m: "positive Basen (gegruppt)" },
{ s: 6, v: 76.4, m: "Log2-Kernel (gegruppt)" },
{ s: 7, v: 82.8, m: "Relative-Score-Fusion (kein dynamischer Router)" },
{ s: 8, v: 80.8, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 9, v: 82.2, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 10, v: 82.2, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 11, v: 83.3, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 12, v: 82.8, m: "Relative-Score dynamisch (gegruppt, Vergleich)" },
{ s: 14, v: 78.1, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 15, v: 78.1, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 16, v: 80.3, m: "Objective reranker-top50 (gegruppt)" },
],
loro: [
{ s: 2, v: 79.7, m: "Ø der 4 Query-Formen (per-Form Gewichte)" },
{ s: 3, v: 78.6, m: "Evidence-Router (LORO)" },
{ s: 4, v: 80.3, m: "dynamischer Router (LORO, Vergleich im BGE-Run)" },
{ s: 5, v: 80.3, m: "positive Basen (LORO)" },
{ s: 6, v: 81.1, m: "Log2-Kernel (LORO)" },
{ s: 7, v: 83.3, m: "Relative-Score-Fusion (kein dynamischer Router)" },
{ s: 8, v: 80.3, m: "DBSF dynamisch (LORO)" },
{ s: 9, v: 82.8, m: "Relative-Score dynamisch (LORO)" },
{ s: 10, v: 83.3, m: "Relative-Score dynamisch (LORO)" },
{ s: 11, v: 82.8, m: "Relative-Score dynamisch (LORO)" },
{ s: 12, v: 83.6, m: "Relative-Score dynamisch (LORO, Vergleich)" },
{ s: 14, v: 80.0, m: "DBSF dynamisch (LORO)" },
{ s: 15, v: 80.0, m: "DBSF dynamisch (LORO)" },
{ s: 16, v: 81.9, m: "Objective direct (LORO)" },
],
fitall: [
{ s: 3, v: 82.5, m: "Evidence-Router" },
{ s: 4, v: 83.1, m: "dynamischer Router" },
{ s: 5, v: 83.1, m: "positive Basen" },
{ s: 6, v: 83.1, m: "Log2-Kernel" },
{ s: 8, v: 83.9, m: "DBSF" },
{ s: 9, v: 85.3, m: "Relative-Score" },
{ s: 10, v: 85.3, m: "Relative-Score" },
{ s: 11, v: 85.3, m: "Relative-Score" },
{ s: 12, v: 85.8, m: "Relative-Score (Vergleich)" },
{ s: 14, v: 83.9, m: "DBSF" },
{ s: 15, v: 83.9, m: "DBSF" },
{ s: 16, v: 84.2, m: "Objective reranker-top20" },
],
r50: {
grouped: [{ s: 16, v: 89.2, m: "Objective reranker-top50 (gegruppt)" }],
loro: [{ s: 16, v: 89.4, m: "Objective reranker-top20 (LORO)" }],
fitall: [{ s: 16, v: 92.5, m: "Objective reranker-top50" }],
},
},
r10: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the misplaced r50 key: it breaks the dashboard.

r50 (Lines 291-295) is nested inside mini.r20 instead of being a sibling of r20, r10, and ctx under mini. Compare to bge (Line 407), where r50 is correctly a sibling.

renderModelPanel reads DATA.mini.r50 directly (Line 655, via modelData[meta.key]). Since that path is undefined, renderChart dereferences metricData[meta.key] (Line 539) on undefined and throws. This halts the synchronous rendering loop, so the MiniLM Recall@10/Context charts, the entire BGE panel, and the Schema 1 note never render.

🐛 Proposed fix: move `r50` out of `r20`
             fitall: [
               { s: 3, v: 82.5, m: "Evidence-Router" },
               { s: 4, v: 83.1, m: "dynamischer Router" },
               { s: 5, v: 83.1, m: "positive Basen" },
               { s: 6, v: 83.1, m: "Log2-Kernel" },
               { s: 8, v: 83.9, m: "DBSF" },
               { s: 9, v: 85.3, m: "Relative-Score" },
               { s: 10, v: 85.3, m: "Relative-Score" },
               { s: 11, v: 85.3, m: "Relative-Score" },
               { s: 12, v: 85.8, m: "Relative-Score (Vergleich)" },
               { s: 14, v: 83.9, m: "DBSF" },
               { s: 15, v: 83.9, m: "DBSF" },
               { s: 16, v: 84.2, m: "Objective reranker-top20" },
             ],
-            r50: {
-              grouped: [{ s: 16, v: 89.2, m: "Objective reranker-top50 (gegruppt)" }],
-              loro: [{ s: 16, v: 89.4, m: "Objective reranker-top20 (LORO)" }],
-              fitall: [{ s: 16, v: 92.5, m: "Objective reranker-top50" }],
-            },
           },
+          r50: {
+            grouped: [{ s: 16, v: 89.2, m: "Objective reranker-top50 (gegruppt)" }],
+            loro: [{ s: 16, v: 89.4, m: "Objective reranker-top20 (LORO)" }],
+            fitall: [{ s: 16, v: 92.5, m: "Objective reranker-top50" }],
+          },
           r10: {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
r20: {
grouped: [
{ s: 2, v: 78.9, m: "Ø der 4 Query-Formen (per-Form Gewichte)" },
{ s: 3, v: 77.5, m: "Evidence-Router (gegruppt)" },
{ s: 4, v: 78.0, m: "dynamischer Router (gegruppt, Vergleich im BGE-Run)" },
{ s: 5, v: 78.1, m: "positive Basen (gegruppt)" },
{ s: 6, v: 76.4, m: "Log2-Kernel (gegruppt)" },
{ s: 7, v: 82.8, m: "Relative-Score-Fusion (kein dynamischer Router)" },
{ s: 8, v: 80.8, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 9, v: 82.2, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 10, v: 82.2, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 11, v: 83.3, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 12, v: 82.8, m: "Relative-Score dynamisch (gegruppt, Vergleich)" },
{ s: 14, v: 78.1, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 15, v: 78.1, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 16, v: 80.3, m: "Objective reranker-top50 (gegruppt)" },
],
loro: [
{ s: 2, v: 79.7, m: "Ø der 4 Query-Formen (per-Form Gewichte)" },
{ s: 3, v: 78.6, m: "Evidence-Router (LORO)" },
{ s: 4, v: 80.3, m: "dynamischer Router (LORO, Vergleich im BGE-Run)" },
{ s: 5, v: 80.3, m: "positive Basen (LORO)" },
{ s: 6, v: 81.1, m: "Log2-Kernel (LORO)" },
{ s: 7, v: 83.3, m: "Relative-Score-Fusion (kein dynamischer Router)" },
{ s: 8, v: 80.3, m: "DBSF dynamisch (LORO)" },
{ s: 9, v: 82.8, m: "Relative-Score dynamisch (LORO)" },
{ s: 10, v: 83.3, m: "Relative-Score dynamisch (LORO)" },
{ s: 11, v: 82.8, m: "Relative-Score dynamisch (LORO)" },
{ s: 12, v: 83.6, m: "Relative-Score dynamisch (LORO, Vergleich)" },
{ s: 14, v: 80.0, m: "DBSF dynamisch (LORO)" },
{ s: 15, v: 80.0, m: "DBSF dynamisch (LORO)" },
{ s: 16, v: 81.9, m: "Objective direct (LORO)" },
],
fitall: [
{ s: 3, v: 82.5, m: "Evidence-Router" },
{ s: 4, v: 83.1, m: "dynamischer Router" },
{ s: 5, v: 83.1, m: "positive Basen" },
{ s: 6, v: 83.1, m: "Log2-Kernel" },
{ s: 8, v: 83.9, m: "DBSF" },
{ s: 9, v: 85.3, m: "Relative-Score" },
{ s: 10, v: 85.3, m: "Relative-Score" },
{ s: 11, v: 85.3, m: "Relative-Score" },
{ s: 12, v: 85.8, m: "Relative-Score (Vergleich)" },
{ s: 14, v: 83.9, m: "DBSF" },
{ s: 15, v: 83.9, m: "DBSF" },
{ s: 16, v: 84.2, m: "Objective reranker-top20" },
],
r50: {
grouped: [{ s: 16, v: 89.2, m: "Objective reranker-top50 (gegruppt)" }],
loro: [{ s: 16, v: 89.4, m: "Objective reranker-top20 (LORO)" }],
fitall: [{ s: 16, v: 92.5, m: "Objective reranker-top50" }],
},
},
r10: {
r20: {
grouped: [
{ s: 2, v: 78.9, m: "Ø der 4 Query-Formen (per-Form Gewichte)" },
{ s: 3, v: 77.5, m: "Evidence-Router (gegruppt)" },
{ s: 4, v: 78.0, m: "dynamischer Router (gegruppt, Vergleich im BGE-Run)" },
{ s: 5, v: 78.1, m: "positive Basen (gegruppt)" },
{ s: 6, v: 76.4, m: "Log2-Kernel (gegruppt)" },
{ s: 7, v: 82.8, m: "Relative-Score-Fusion (kein dynamischer Router)" },
{ s: 8, v: 80.8, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 9, v: 82.2, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 10, v: 82.2, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 11, v: 83.3, m: "Relative-Score dynamisch (gegruppt)" },
{ s: 12, v: 82.8, m: "Relative-Score dynamisch (gegruppt, Vergleich)" },
{ s: 14, v: 78.1, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 15, v: 78.1, m: "DBSF dynamisch (gegruppt 5-fold)" },
{ s: 16, v: 80.3, m: "Objective reranker-top50 (gegruppt)" },
],
loro: [
{ s: 2, v: 79.7, m: "Ø der 4 Query-Formen (per-Form Gewichte)" },
{ s: 3, v: 78.6, m: "Evidence-Router (LORO)" },
{ s: 4, v: 80.3, m: "dynamischer Router (LORO, Vergleich im BGE-Run)" },
{ s: 5, v: 80.3, m: "positive Basen (LORO)" },
{ s: 6, v: 81.1, m: "Log2-Kernel (LORO)" },
{ s: 7, v: 83.3, m: "Relative-Score-Fusion (kein dynamischer Router)" },
{ s: 8, v: 80.3, m: "DBSF dynamisch (LORO)" },
{ s: 9, v: 82.8, m: "Relative-Score dynamisch (LORO)" },
{ s: 10, v: 83.3, m: "Relative-Score dynamisch (LORO)" },
{ s: 11, v: 82.8, m: "Relative-Score dynamisch (LORO)" },
{ s: 12, v: 83.6, m: "Relative-Score dynamisch (LORO, Vergleich)" },
{ s: 14, v: 80.0, m: "DBSF dynamisch (LORO)" },
{ s: 15, v: 80.0, m: "DBSF dynamisch (LORO)" },
{ s: 16, v: 81.9, m: "Objective direct (LORO)" },
],
fitall: [
{ s: 3, v: 82.5, m: "Evidence-Router" },
{ s: 4, v: 83.1, m: "dynamischer Router" },
{ s: 5, v: 83.1, m: "positive Basen" },
{ s: 6, v: 83.1, m: "Log2-Kernel" },
{ s: 8, v: 83.9, m: "DBSF" },
{ s: 9, v: 85.3, m: "Relative-Score" },
{ s: 10, v: 85.3, m: "Relative-Score" },
{ s: 11, v: 85.3, m: "Relative-Score" },
{ s: 12, v: 85.8, m: "Relative-Score (Vergleich)" },
{ s: 14, v: 83.9, m: "DBSF" },
{ s: 15, v: 83.9, m: "DBSF" },
{ s: 16, v: 84.2, m: "Objective reranker-top20" },
],
},
r50: {
grouped: [{ s: 16, v: 89.2, m: "Objective reranker-top50 (gegruppt)" }],
loro: [{ s: 16, v: 89.4, m: "Objective reranker-top20 (LORO)" }],
fitall: [{ s: 16, v: 92.5, m: "Objective reranker-top50" }],
},
r10: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/plot-baseline.html` around lines 244 - 297, Move the r50 object
out of the mini.r20 object so it becomes a sibling of r20, r10, and ctx under
mini, matching the structure used by bge. Preserve the existing r50 grouped,
loro, and fitall data so renderModelPanel can access DATA.mini.r50 directly.

Comment on lines +131 to +138
const selectManifests = (
manifests: readonly CorpusManifest[],
profile: BenchmarkProfile,
): readonly CorpusManifest[] => {
const selected = selectValues(process.env.PIX_BENCH_REPOS)
if (selected) return manifests.filter((manifest) => selected.has(manifest.id))
return profile === "smoke" ? manifests.filter((manifest) => manifest.id === "fd") : manifests
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject unknown PIX_BENCH_REPOS values.

selectManifests silently drops identifiers that match no manifest. A typo then produces an empty manifest list, and the benchmark writes an artifact with zero repositories and zero measurements. selectModels already validates unknown values at Line 147. Apply the same check here.

🛡️ Proposed validation
   const selected = selectValues(process.env.PIX_BENCH_REPOS)
-  if (selected) return manifests.filter((manifest) => selected.has(manifest.id))
+  if (selected) {
+    const unknown = [...selected].filter((id) => !manifests.some((manifest) => manifest.id === id))
+    if (unknown.length > 0)
+      throw new Error(`Unknown PIX_BENCH_REPOS values: ${unknown.join(", ")}`)
+    return manifests.filter((manifest) => selected.has(manifest.id))
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selectManifests = (
manifests: readonly CorpusManifest[],
profile: BenchmarkProfile,
): readonly CorpusManifest[] => {
const selected = selectValues(process.env.PIX_BENCH_REPOS)
if (selected) return manifests.filter((manifest) => selected.has(manifest.id))
return profile === "smoke" ? manifests.filter((manifest) => manifest.id === "fd") : manifests
}
const selectManifests = (
manifests: readonly CorpusManifest[],
profile: BenchmarkProfile,
): readonly CorpusManifest[] => {
const selected = selectValues(process.env.PIX_BENCH_REPOS)
if (selected) {
const unknown = [...selected].filter((id) => !manifests.some((manifest) => manifest.id === id))
if (unknown.length > 0)
throw new Error(`Unknown PIX_BENCH_REPOS values: ${unknown.join(", ")}`)
return manifests.filter((manifest) => selected.has(manifest.id))
}
return profile === "smoke" ? manifests.filter((manifest) => manifest.id === "fd") : manifests
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/retrieval/runner.ts` around lines 131 - 138, Update
selectManifests to validate every identifier from PIX_BENCH_REPOS against
manifests, matching the unknown-value validation behavior already implemented by
selectModels. Reject unknown identifiers before filtering, while preserving the
existing smoke-profile default and valid-selection behavior.

Comment on lines +307 to +320
const targetsByQuestion = manifest.questions.map((question) => {
const targets = resolveGoldTargets(
question.groundTruth,
corpus.chunks,
corpus.identifiersByChunk,
)
const unresolved = question.groundTruth.filter((_, index) => targets[index].size === 0)
if (unresolved.length > 0) {
throw new Error(
`${question.id} has unresolved gold targets: ${unresolved.map((target) => `${target.file}::${target.symbol}`).join(", ")}`,
)
}
return targets
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Report unresolved gold targets through the typed error channel.

This throw runs inside the Effect.gen body, so Effect treats it as a defect and the fiber dies. The declared error type of runRetrievalBenchmark is Error, and the neighboring failures at Line 325 and Line 386 use Effect.fail. Convert this validation to the same typed failure so callers can handle it.

♻️ Proposed refactor
-      const targetsByQuestion = manifest.questions.map((question) => {
-        const targets = resolveGoldTargets(
-          question.groundTruth,
-          corpus.chunks,
-          corpus.identifiersByChunk,
-        )
-        const unresolved = question.groundTruth.filter((_, index) => targets[index].size === 0)
-        if (unresolved.length > 0) {
-          throw new Error(
-            `${question.id} has unresolved gold targets: ${unresolved.map((target) => `${target.file}::${target.symbol}`).join(", ")}`,
-          )
-        }
-        return targets
-      })
+      const targetsByQuestion: (readonly ReadonlySet<number>[])[] = []
+      for (const question of manifest.questions) {
+        const targets = resolveGoldTargets(
+          question.groundTruth,
+          corpus.chunks,
+          corpus.identifiersByChunk,
+        )
+        const unresolved = question.groundTruth.filter((_, index) => targets[index].size === 0)
+        if (unresolved.length > 0)
+          return yield* Effect.fail(
+            new Error(
+              `${question.id} has unresolved gold targets: ${unresolved.map((target) => `${target.file}::${target.symbol}`).join(", ")}`,
+            ),
+          )
+        targetsByQuestion.push(targets)
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const targetsByQuestion = manifest.questions.map((question) => {
const targets = resolveGoldTargets(
question.groundTruth,
corpus.chunks,
corpus.identifiersByChunk,
)
const unresolved = question.groundTruth.filter((_, index) => targets[index].size === 0)
if (unresolved.length > 0) {
throw new Error(
`${question.id} has unresolved gold targets: ${unresolved.map((target) => `${target.file}::${target.symbol}`).join(", ")}`,
)
}
return targets
})
const targetsByQuestion: (readonly ReadonlySet<number>[])[] = []
for (const question of manifest.questions) {
const targets = resolveGoldTargets(
question.groundTruth,
corpus.chunks,
corpus.identifiersByChunk,
)
const unresolved = question.groundTruth.filter((_, index) => targets[index].size === 0)
if (unresolved.length > 0)
return yield* Effect.fail(
new Error(
`${question.id} has unresolved gold targets: ${unresolved.map((target) => `${target.file}::${target.symbol}`).join(", ")}`,
),
)
targetsByQuestion.push(targets)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/retrieval/runner.ts` around lines 307 - 320, Update the
unresolved-target validation in the manifest.questions mapping to return an
Effect.fail with the existing Error message instead of throwing directly.
Preserve the current question ID and unresolved target details, matching the
typed failure pattern used by runRetrievalBenchmark’s neighboring failure paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant