Skip to content

Add retrieval evaluation, fix the bugs it found, add TypeScript - #10

Merged
rahulr85r merged 1 commit into
mainfrom
feat/eval-harness
Aug 8, 2026
Merged

rahulr85r merged 1 commit into
mainfrom
feat/eval-harness

Conversation

@rahulr85r

Copy link
Copy Markdown
Owner

TokenHack had ~20 hand-tuned scoring constants and no way to tell whether any of them helped. This adds the measurement, then acts on what it found.

The harness

tests/ — 72 questions across 6 OSS repos, each paired with the file that answers it. Built blind: questions and answers were derived by reading the repos only, with the router and its index explicitly off-limits, so the set isn't shaped around what this tool is good at. 45 of 72 are deliberately vocabulary-mismatch questions, where the question's words don't appear in the answer's name.

python3 tests/fetch_repos.py   # clones + indexes the pinned public repos
python3 tests/eval.py

Public repos are pinned to the commit their gold set was written against, so the numbers are reproducible. Every scoring constant now reads a TOKENHACK_<NAME> env override, so a change can be ablated instead of argued about.

First run: hit@5 = 0.19

Four questions in five got nothing useful into the top five. What that found:

1. graph_prop was 94% of the winning score

winner:  testsuite/.../SocketReadPendingTest.java        score 354.76
         bm25 14.72 | filename 2.0 | path 2.0 | def 4.0 | graph_prop 333.01
gold:    transport/.../AdaptiveRecvByteBufAllocator.java score 8.16  (rank 1,716)

Every real lexical signal summed to 22 and got shouted down 15-to-1. The filename gate meant to bound this is trivially cleared by a natural-language question — enough words that any well-connected file matches two. Propagation is evidence a neighbour matched, which is weaker than matching yourself: now always log-compressed and capped at the file's own query-derived score.

2. The highest-weighted channel in the formula was empty

ETA is the largest coefficient in the scorer, and only the Python adapter ever populated the prose channel:

netty               0 / 3,512 files with prose
spring-framework    0 / 9,487 files with prose
Signal-Android      2 / 5,534 files with prose

Java, Kotlin, Swift and JS returned nothing — and an empty channel doesn't error, it just contributes zero forever. On repos that are famously javadoc'd. Doc-comment extraction now ships for all adapters (80% coverage on Spring, 60% on netty), and with the channel actually populated ETA=1.2 was too low: 0.181 at ETA=0, 0.208 at 1.2, 0.264 at 2.4, flat above. Default raised to 2.4.

3. Tests outranked the code they test

Tests restate a concept's vocabulary more densely than implementations do, which is exactly what BM25 rewards. netty's pipeline question returned two testsuite files and a microbenchmark with DefaultChannelPipeline.java absent — and when the pairing did surface it, it printed the implementation as (test for ...) its own test. Test paths are now demoted unless the question is about tests.

4. Structural matches ignored IDF

filename_match and path_affinity counted 1.0 per token regardless of the token, so read + socket scored the same as idempotency + backoff. Now IDF-weighted (worth ~1.4 pts of hit@5 alone), and sub-threshold query tokens are dropped as corpus filler.

Result

Before After
hit@5 0.19 0.26
recall@5 0.16 0.19
MRR 0.13 0.17
hit@5, vocabulary mismatch 0.13 0.22

Baseline committed so --check catches regressions.

These are in-sample numbers on six repos, and most questions still miss. That's now a fact with a number attached rather than an unknown.

Also in this PR

  • TypeScript/TSX adapter — the JS walk moved to walk_source(); typescript.py reuses it and adds interface/type-alias/enum declarations, dispatching .tsx to the JSX grammar. Biggest adoption blocker removed.
  • Coverage warning — the router now says so when >25% of a repo's source files had no adapter. The indexer always recorded this and nothing read it, so an unsupported-language repo got confident rankings over a fragment of itself.
  • Light plural folding on structural matches (bytes → ByteBuf, credentials → Credential).
  • CI job (workflow_dispatch + on PRs touching the ranker) that clones the pinned repos and fails on regression.

TokenHack had ~20 hand-tuned scoring constants and no way to tell whether any
of them helped. This adds the measurement and then acts on it.

## tests/ — the harness

72 questions across 6 OSS repos (spring-framework, netty, Signal-Android,
DuckDuckGo iOS, ownCloud Android, a small JS app), each paired with the file
that answers it. Built blind: the questions and answers were derived by reading
the repos only, with the router and its index off-limits, so the set is not
shaped around what this tool happens to be good at. 45 of 72 are deliberately
vocabulary-mismatch questions. Public repos are pinned in tests/repos.json and
tests/fetch_repos.py reproduces the whole thing from scratch.

Every scoring constant now reads an optional TOKENHACK_<NAME> environment
override so a change can be ablated instead of argued about.

## What the first run found

hit@5 = 0.19. Four questions in five got nothing useful into the top five.

1. graph_prop was 94% of the winning score. On a netty query the winner scored
   354.76, of which 333.01 was import-graph propagation against a lexical score
   of 22 — and the file that answered the question ranked 1,716th of 3,522. The
   filename gate meant to bound this is trivially cleared by a natural-language
   question, which contains enough words that any well-connected file matches
   two of them. Propagation is evidence that a *neighbour* matched, which is
   weaker than matching yourself: it is now always log-compressed and capped at
   the file's own query-derived score.

2. The prose channel was empty. ETA is the highest weight in the scoring
   function and only the Python adapter ever populated it — Java, Kotlin, Swift
   and JavaScript returned nothing, so the highest-weighted term multiplied
   zero across 0/3512 netty files, 0/9487 Spring files, 2/5534 Signal files.
   Doc-comment extraction (javadoc/KDoc/Swift doc/JSDoc) now ships in _base.py
   and all adapters use it; coverage is 80% on Spring, 60% on netty. With the
   channel actually populated, ETA 1.2 was too low — measured 0.181 at ETA=0,
   0.208 at 1.2, 0.264 at 2.4, flat above that. Default raised to 2.4.

3. Tests outranked implementations. Tests restate a concept's vocabulary more
   densely than the code does, which is what BM25 rewards; netty's pipeline
   question returned two testsuite files and a microbenchmark, with
   DefaultChannelPipeline.java absent. Test/benchmark/fixture paths are now
   demoted unless the question is about tests. Paired results were also labelled
   backwards — the implementation was printed as "test for" its own test.

4. Structural matches ignored IDF. filename_match and path_affinity counted 1.0
   per token regardless of the token, so `read` and `socket` scored the same as
   `idempotency` and `backoff`. Both are now IDF-weighted; worth ~1.4 points of
   hit@5 on its own. Query tokens below MIN_QUERY_TOKEN_IDF are dropped as
   corpus-wide filler.

## Result

hit@5 0.19 -> 0.26, recall@5 0.16 -> 0.19, MRR 0.13 -> 0.17. On the
vocabulary-mismatch slice, where lexical retrieval is expected to lose,
0.13 -> 0.22. Baseline committed so --check can catch regressions.

These are in-sample numbers on six repos and most questions still miss. That is
now a fact with a number attached rather than an unknown.

## Also

- TypeScript/TSX adapter. The JS walk moved into walk_source() and
  typescript.py reuses it, adding interface/type-alias/enum declarations and
  dispatching .tsx to the JSX grammar. Removes the single biggest adoption
  blocker.
- The router now warns when more than a quarter of a repo's source files had no
  language adapter. The indexer had always recorded this and nothing read it,
  so an unsupported-language repo got confident rankings over a fragment of
  itself.
- Light plural folding on structural matches, so "bytes" reaches ByteBuf and
  "credentials" reaches Credential.
- A workflow_dispatch/PR-triggered CI job that clones the pinned repos and fails
  on regression against the baseline.
@rahulr85r
rahulr85r merged commit 695bf92 into main Aug 8, 2026
1 check failed
@rahulr85r
rahulr85r deleted the feat/eval-harness branch August 8, 2026 04:11
rahulr85r added a commit that referenced this pull request Aug 8, 2026
The README led with "~95% of the tokens your AI assistant spends finding
files" and put "~25% session-level token savings" as the honest single number.
Both predate any measurement of whether retrieval returns the right file, and
the evaluation added in #10 shows it does so 26% of the time — so a savings
model that implicitly assumes retrieval works cannot be right.

Reworked to the actual expected value, with the hit rate carried through:

  on a hit  (26%):  495 staged + ~545 to read the top result's spans
  on a miss (74%):  495 staged, and Claude explores anyway

  E[saving] = 0.26 x (B - 1040) - 0.74 x 495

which breaks even at B ~= 2,400 tokens of unaided exploration, reaches ~14% at
B = 5,000 and ~20% at B = 10,000, and is NEGATIVE below break-even. Netting off
the ~1,150-token skill body and the share of a session that is actually
navigation, that is single-digit percent session-wide for a typical engineer.

B — what the unaided grep-and-read expedition costs — is the one input never
measured end-to-end, so this ships as a range with the assumption named rather
than a headline number.

Also removes the n=1 DuckDuckGo trace that produced the 95% figure. It was one
query on one repo with the baseline adjudicated after the fact by the author,
and the hero image drawn from it shows the correct file ranked 5th of 5.
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