Files
gbrain/test/search-compiled-truth-boost-scope.test.ts
T
85286a556c fix(search): stop boosting compiled_truth at default detail (#3430) (#3514)
* fix(search): stop boosting compiled_truth at default detail (#3430)

COMPILED_TRUTH_BOOST = 2.0 is applied AFTER RRF normalization, and RRF's whole
dynamic range over a 100-deep pool is 1/60 -> 1/160 (a factor of 2.67). So a
2.0x multiplier consumes roughly three quarters of the range: break-even is
`2/(60+r) >= 1/60`, i.e. r <= 60, which means ANY boosted chunk inside the
first 60 ranks outranks an unboosted rank-1 chunk. That is a categorical
filter, not a tilt.

Measured against master's own rrfFusion, with the correct answer in a
fenced_code chunk at vector rank 0:

  compiled_truth chunks in pool | final rank | in top-20
  10                            | 10         | yes
  20                            | 20         | NO
  40                            | 40         | NO
  80                            | 59         | NO

With the boost off the answer stays at rank 0 in every case.

The gate was spelled `detail !== 'high'` -- written as though `high` were the
special case. The documented contract in src/core/operations.ts is
"low (compiled truth only), medium (default, all with dedup), high (all
chunks)", which makes LOW the special one: `low` already restricts to
compiled_truth, so a boost there is a no-op among equals, while `medium` and
`high` are both meant to see everything. So the default detail was silently
compiled-truth-only, contradicting the op's own description.

Three changes:

1. The three fusion call sites now route through a named predicate,
   `shouldBoostCompiledTruth(detail)`, returning true only for 'low'.
   Extracted rather than left inline precisely because an inline expression is
   only reachable through a full hybridSearch round trip -- which is why the
   inversion went unnoticed. The predicate is directly unit-testable.

2. KNOBS_HASH_VERSION 13 -> 14. Results are cached AFTER fusion, so rows
   ranked under the old semantics would otherwise be served under the new ones
   for the whole TTL (3600s default). One-time miss spike on upgrade.

3. test/search-compiled-truth-boost-scope.test.ts pins both the mapping and
   the arithmetic, and documents the displacement it prevents.

Verified the tests discriminate: stubbing the OLD predicate body into master
(so the failure is behavioral rather than a missing export) gives 4 fail /
3 pass; with the fix, 7 pass. typecheck clean, verify 32/32, and 144 pass /
0 fail across the search + fusion + cache suites.

* fix(test): update the three remaining KNOBS_HASH_VERSION pins to 14 (#3430)

Missed in the first pass because I ran a targeted set of test files instead of
the full suite. CI shards 3, 8 and 10 caught them:

  test/search/knobs-hash-reranker.test.ts:67
  test/cross-modal-phase1.test.ts:139,149
  test/search-alias-resolved-boost.test.ts:93

Each carries the running history of why the version moved, so each gets the
13→14 rationale appended rather than just the number swapped. No pins at 13
remain anywhere in test/.

---------

Co-authored-by: Garry Tan <garrytan@gmail.com>
2026-07-28 19:27:46 -07:00

113 lines
5.5 KiB
TypeScript

/**
* #3430: the compiled_truth boost must not apply at `detail=medium`.
*
* `COMPILED_TRUTH_BOOST = 2.0` is applied AFTER RRF score normalization. RRF's
* entire dynamic range over a 100-deep pool is 1/60 → 1/160 (a factor of 2.67),
* so a 2.0x multiplier consumes roughly three quarters of it. Break-even is
* `2/(60+r) >= 1/60`, i.e. r <= 60 — so ANY boosted chunk in the first 60 ranks
* outranks an unboosted rank-1 chunk. That is a categorical filter, not a tilt:
* a page whose actual answer is in a `fenced_code` chunk returns the prose
* chunk instead, and the code chunk leaves the result window entirely.
*
* The gate was written as `detail !== 'high'` — "high is special" — but the
* documented contract in `src/core/operations.ts` is:
*
* low (compiled truth only), medium (default, all with dedup), high (all chunks)
*
* which makes LOW the special one. `low` already restricts to compiled_truth,
* so a boost there is a no-op among equals; `medium` and `high` are both
* supposed to see everything. Hence `detail === 'low'`.
*
* These tests pin the arithmetic, not the constant — they would still fail if
* someone reintroduced a boost at medium with a different multiplier or behind
* a score floor, which is why they assert final RANK rather than score.
*/
import { describe, test, expect } from 'bun:test';
import { rrfFusion, RRF_K, shouldBoostCompiledTruth } from '../src/core/search/hybrid.ts';
import { KNOBS_HASH_VERSION } from '../src/core/search/mode.ts';
import type { SearchResult } from '../src/core/types.ts';
function chunk(slug: string, chunkSource: string): SearchResult {
return { slug, chunk_source: chunkSource, chunk_text: 'x', title: slug, score: 0 } as unknown as SearchResult;
}
/** One vector arm: the correct answer at rank 0, then `n` compiled_truth chunks. */
function poolWithAnswerFirst(n: number): SearchResult[] {
const list = [chunk('code/answer', 'fenced_code')];
for (let i = 0; i < n; i++) list.push(chunk(`prose/p${i}`, 'compiled_truth'));
return list;
}
function rankOfAnswer(results: SearchResult[]): number {
return results.findIndex((r) => r.slug === 'code/answer');
}
describe('#3430: the detail→boost mapping itself', () => {
// These are the assertions that actually FAIL on master. The rrfFusion tests
// below pin the arithmetic but pass either way, because they pass the boost
// flag explicitly — they cannot see how hybridSearch decides it. This is the
// wiring.
test('ONLY detail=low boosts compiled_truth', () => {
expect(shouldBoostCompiledTruth('low')).toBe(true);
expect(shouldBoostCompiledTruth('medium')).toBe(false);
expect(shouldBoostCompiledTruth('high')).toBe(false);
});
test('an absent detail does not boost — medium is the documented default', () => {
// Callers that omit detail get medium semantics, so the unset case must
// match medium, not low. A `!== 'high'` spelling gets this backwards.
expect(shouldBoostCompiledTruth(undefined)).toBe(false);
expect(shouldBoostCompiledTruth(null)).toBe(false);
});
test('an unrecognized detail value does not boost', () => {
// Fail-open toward showing everything rather than silently filtering.
expect(shouldBoostCompiledTruth('')).toBe(false);
expect(shouldBoostCompiledTruth('LOW')).toBe(false);
expect(shouldBoostCompiledTruth('detailed')).toBe(false);
});
test('the cache version was bumped so pre-fix rankings are unreachable', () => {
// Results are cached AFTER fusion, so rows written under the old boost
// semantics would otherwise be served under the new ones for the whole TTL.
// 13 was the pre-fix value.
expect(KNOBS_HASH_VERSION).toBeGreaterThanOrEqual(14);
});
});
describe('#3430: compiled_truth boost scope', () => {
test('boost OFF (detail=medium/high) keeps the vector-ranked answer at rank 0', () => {
// The regression this file exists for. Pre-fix, medium passed applyBoost=true
// and the answer landed at rank n — outside a 20-result window for n >= 20.
for (const n of [10, 20, 40, 80]) {
const fused = rrfFusion([poolWithAnswerFirst(n)], RRF_K, false);
expect(rankOfAnswer(fused), `n=${n}: answer must stay first without the boost`).toBe(0);
}
});
test('boost ON demonstrates the categorical displacement it causes', () => {
// Documents WHY the boost cannot be on at medium. Not an endorsement of
// these numbers — a characterization of the mechanism, so a future reader
// sees the cost rather than re-deriving it.
const observed = [10, 20, 40].map((n) => ({
n,
rank: rankOfAnswer(rrfFusion([poolWithAnswerFirst(n)], RRF_K, true)),
}));
// Displacement scales with pool composition: the answer is pushed back by
// roughly one position per boosted chunk ahead of the break-even rank.
for (const { n, rank } of observed) {
expect(rank, `n=${n}: boosted chunks should displace the answer`).toBeGreaterThan(0);
}
// And past ~20 compiled_truth chunks it leaves a default-size window.
expect(observed.find((o) => o.n === 20)!.rank).toBeGreaterThanOrEqual(20);
});
test('with the boost off, compiled_truth still wins when the vector arm ranks it first', () => {
// Guard against over-correcting: removing the boost must not penalize
// compiled_truth, only stop privileging it.
const list = [chunk('prose/answer', 'compiled_truth'), chunk('code/other', 'fenced_code')];
const fused = rrfFusion([list], RRF_K, false);
expect(fused[0].slug).toBe('prose/answer');
});
});