Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENT_DIARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
Пункты «RU→EN словарь» и «concept→symbol словарь» сняты: их заменяет контракт.
- **Урок:** не учить сервер языкам, которые знает хост; отдавать `hints` и честно
просить их, когда в вопросе нет code-имени.
- **E68 (бенчмарк из истории git):** вопрос = сообщение коммита, gold = `git show`,
корпус на коммите `B` старше задач. Dev/71: tool 19, neighbours 37, search 24,
grep 2 (hit@5); sealed/37: 7 / 21 / 5 / 1. P1 и P2 провалены: дефолт **хуже**
обычного поиска. Причина — 36 из 52 промахов это abstain с нулём строк.
- **Урок:** «честный отказ» стоит recall, и это теперь измерено в задачах, а не
в мнении; решение (отказ или соседи с caveat) — за владельцем.
- **E66 (панель из индекса):** 281 задача, сид = sha256(commit). name 154/160,
qualified 38/40, negative 40/40; якоря воспроизвели E65 **точно** (6/16 и 11/17) —
инструмент откалиброван. Это детектор регрессий, а не «поиск на 96%»: вопросы
Expand Down
32 changes: 32 additions & 0 deletions EXPERIMENTS_LOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
# EXPERIMENTS_LOG.md — Audit Verification (2026-07-22)
## [2026-11-10] — E68: commit-localization benchmark — the default answers *less* than plain search

**Why:** every earlier panel was authored by us, so "tuned to the test" was a
fair objection. Here the question is a commit message we did not write, the gold
is `git show --name-only`, and the corpus is a worktree at `B = 7c2c7a3`
(2026-09-06) — the fixes are not in the index.

**Panel:** 174 commits in `(B, HEAD]`; 66 reference files created after `B` and
are unsolvable by construction (dropped, counted); 108 solvable, split 71 dev /
37 sealed by date. Sealed ran once (`--sealed`).

```
hit@1 hit@5 any@8 dev (n=71) sealed (n=37)
tool (default) 12 19 23 0.268 0.189
tool + neighbours 24 37 44 0.521 0.568
search channel alone 10 24 29 0.338 0.135
grep (floor) 0 2 6 0.028 0.027
```

**Verdict:** P1 FAIL (default 0.268 < 0.35), P2 FAIL (default is **5 below**
plain search on dev), P3 PASS, P4 PASS (dev/sealed within 0.10 for both arms).

**Cause, measured:** 36 of 52 default misses are `abstain` with zero rows — the
message names things the commit introduces, so the gate finds no occurrence and
withholds the eight neighbours it already computed. Turning them on recovers
+18 of 71 (dev) and +16 of 37 (sealed). This is a policy decision (honesty vs
recall), not a ranking defect.

**Instrumentation:** the sealed process died twice (16 and 22 tasks) with no
traceback; the runner now resumes from `results_*.partial.jsonl`. Arms were
frozen before the run; no code or metric changed afterwards.

## [2026-11-10] — E66: generated 281-task panel — the instrument is calibrated

**Design** (frozen before the run): tasks generated from the index, seed =
Expand Down
103 changes: 103 additions & 0 deletions experiments/bench_2026_11/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Benchmark design — a test we cannot tune to

Date: 2026-11-10. Problem statement (owner): every panel so far was authored by
us, so "we tuned it ourselves" is a fair objection. This is the design that
removes the author from the loop.

## The three ways a benchmark can be gamed, and the fix for each

| Hole | How we exploited it (E66) | Fix |
|---|---|---|
| **We write the questions** | generated `name` tasks contain the identifier; grep solves 91.9% | take the query from **real text we did not write** (commit messages, issue bodies) |
| **We pick the gold** | gold = symbol definition, chosen by the generator's filters | gold is **computed by a rule** from git (`git show --name-only`), no human choice |
| **We can look and retune** | full panel visible while iterating | **temporal split + sealed eval**: tuning set and eval set are different commits, eval runs once and its hash is committed first |

## B1 — commit localization (the main benchmark)

For every non-merge commit `C` in a pinned range:

- **query** = the commit message (subject + body), with the diff removed. The
text is written by whoever fixed the bug — not by us.
- **gold** = the set of files changed in `C`, minus merge commits, minus commits
touching more than 20 files, minus commits whose message is a changelog/typo
stub. The filter is one rule, applied to all commits, before any result is
seen.
- **corpus** = the repository at commit `B`, where `B` is *older* than every task
commit. One index build serves the whole task set, and the fix is not in the
corpus. This is the leak that makes naive "ask about HEAD" benchmarks
worthless: the fixed code is right there in the index.
- **metric** = file-level hit@1 / hit@5 / any@8 and MRR. Exact, no judge, no
rubric. Per task first, rates with Wilson intervals second.

Why this resists tuning: the questions are the developers' own sentences
(including typos, "wip", "fix"), the gold is what git says changed, and the
corpus predates the fix. We do not get to pick a flattering subset without
saying so in the rule.

### Split

- Sort tasks by commit date.
- First 70% = **dev set** (visible, tuning allowed).
- Last 30% = **sealed set**. Its task list and gold are hashed and committed
*before* the first sealed run. The runner refuses to print per-task sealed
results unless `--sealed` is passed, and that flag is used once per release.
- Report dev and sealed separately. A change that helps dev and not sealed is
reported as such.

### Baselines that must be printed next to us

- `git grep` on the message's identifiers (the E66 control, measured: it wins on
easy tasks).
- BM25 over the same corpus with no graph, no symbols, no gate.

If we do not beat both, the benchmark says so — that is the point.

## B2 — external repositories (the strongest check)

The same generator over 2-3 pinned external repos where we have never written a
line: the commit messages and the gold are entirely independent of us. Repos and
commits are pinned in a `frozen/PINS.md` with the sha, and the runner verifies
the sha before running.

This is the only version where "they tuned to it" is impossible by construction;
its cost is cloning and indexing a few external trees.

## B3 — definition lookup (what E66 was)

Keep it, but label it **regression detector**, never a quality number: a plain
grep already reaches 91.9%, measured.

## Determinism, the part the owner asked about explicitly

"Same exact evaluation" means: same code + same commit + same task set + same
index state -> same numbers. Two things make that hard here:

1. **Index freshness.** E66's task 197 differed between sessions while being
stable within a session. Fix: the runner builds the index once into a
dedicated data dir, records its `build_head` and file count, and refuses to
report if the index changed mid-run.
2. **Concurrency.** Channels run in parallel; equal scores are ordered by the
deterministic tie-break we added in E65, so repeated runs agree (measured
24 tasks x 3 runs, 0 drift).

Any published number must come from `bench/run.py` — not from a hand-written
table. The metric lives in code, the task set lives in a hashed file.

## What this cannot fix

- **We can still see the sealed set** and re-tune afterwards. The only real
defenses are the temporal split (future commits are unseen by construction)
and external repos (B2).
- **A benchmark is a sample.** n and intervals decide whether a delta is real;
the rule "one task is one task" (paired counts, not rates) stays.
- **Corpus drift**: the repo changes, so old tasks may become trivial (the fix
file gets renamed). Re-pinning the range is a new version of the benchmark;
versions are never edited in place.

## Order of work

1. `bench/commit_tasks.py` — build tasks from git, one rule, hashed manifest.
2. `bench/run.py` — index at `B`, run tasks, exact metrics, paired deltas.
3. `bench/baseline.py` — grep and BM25 rows in the same table.
4. Sealed split + manifest hash committed before the first sealed run.
5. B2 on one external repo, then a second.
2 changes: 2 additions & 0 deletions experiments/bench_2026_11/MANIFEST.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
71395d4533eb12aaad83a420308af87916d0a31f7c2ee460d0734441e579a205 tasks.jsonl
base=7c2c7a3c6417539bc08f807d6790fe6e722d3a84
177 changes: 177 additions & 0 deletions experiments/bench_2026_11/REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# B1 — commit-localization benchmark: first result

Frozen before the run: `frozen/PLAN.md` (design + Amendments 1-3).
Design rationale: `DESIGN.md`. Owner objection this answers: *"we wrote the test
and tuned to it"*.

## What was measured, and why it cannot flatter us

- **Corpus**: worktree at `B = 7c2c7a3` (2026-09-06). Index built once there:
383 files, 6406 chunks, 0 errors, 218 s. Guarded: the runner refuses to report
if `build_head != B`.
- **Tasks**: 174 commits from `(B, HEAD]`. The question is the commit message
**verbatim**; the gold is `git show --name-only`, one rule for every commit.
- **Solvable subset**: 66 of 174 tasks reference files created after `B` and are
unsolvable by construction (Amendment 2); they are counted and printed, and
the metrics below are over the solvable subset.
- **Split**: 121 dev / 53 sealed by date; sealed needs `--sealed` and was run
once.

Arms: `tool` (`investigate` default), `neighbours` (same with
`include_neighbours=true`, Amendment 3), `search` (the product's BM25/hybrid
channel alone — the fair baseline), `grep` (message words, floor).

## Dev result (71 solvable tasks)

```
arm hit@1 hit@5 any@8 MRR hit@5 95% CI
tool 12 19 23 0.215 [0.179, 0.381]
neighbours 24 38 44 0.424 [0.420, 0.646]
search 11 22 29 0.239 [0.214, 0.425]
grep 0 2 6 0.051 [0.008, 0.097]
paired hit@5: neighbours-only 20, search-only 4 (delta +16)
```

Determinism guard: 10 tasks x 3 repeats, **0** unstable.

## Frozen predictions

| # | Prediction | Result |
|---|---|---|
| P1 | dev `hit@5` (tool) >= 0.35 | **FAIL** — 19/71 = 0.268 |
| P2 | tool >= search + 5 on `hit@5` | **FAIL** — 19 vs 22, i.e. **-3** |
| P3 | `any@8` >= `hit@5` everywhere | PASS |
| P4 | sealed within +/- 0.10 of dev | see below |

The default `tool` arm is **worse than the plain search channel** on prose
queries. The `neighbours` arm is +13 over search.

### Does the message give the answer away?

Half of real commit messages name the file ("fix parser.py"). Split:

```
n tool neighbours search (hit@5, dev)
names file 37 11 22 11
prose only 34 8 16 11
```

The tool's deficit is in **both** halves (11 vs 11 named, 8 vs 11 prose), so the
loss is not an artifact of messages that leak the filename. `neighbours` wins in
both.


## Why the default loses (measured, not argued)

52 tool misses at hit@5, classified:

```
36 abstain, zero rows -> identifiers in the message do not exist in the corpus
11 outside top-8
4 rank 6-8
1 outside top-8 (answer)
```

The messages describe changes, so they name things the commit *introduces*
(`iter_import_candidate_nodes`). The gate extracts those names, finds zero
occurrences, and — correctly by its contract — abstains and withholds the eight
neighbours it already computed.

On exactly those 52 tasks, with `include_neighbours=true`:

```
default hit@1=0 hit@5=0 any@8=4
neighbours hit@1=12 hit@5=18 any@8=25
```

So the loss is a **policy** choice (`abstain` = "do not trust anything"), not a
ranking defect. This is the most useful thing the benchmark has produced so far,
and it is a product decision, not a bug fix: returning unverified neighbours
instead of nothing gains 18 of 71 tasks and risks an agent taking a neighbour for
an answer.

## Sealed result (37 solvable tasks)

```
arm hit@1 hit@5 any@8 MRR hit@5 95% CI
tool 2 7 7 0.113 [0.095, 0.342]
neighbours 11 21 21 0.410 [0.409, 0.713]
search 0 5 7 0.062 [0.059, 0.280]
grep 0 1 1 0.036 [0.005, 0.138]
paired hit@5: neighbours-only 16, search-only 0 (delta +16)
```

P4: `tool` moves from 0.268 (dev) to 0.189 (sealed), a **0.079** drop — inside the
+/-0.10 band, so the overfit alarm does not fire. `neighbours` moves 0.521 ->
0.568 (+0.047).

The sealed split is harder for **every** arm (`search` collapses from 24/71 to
5/37), which is consistent with it being the most recent commits: their files
changed after the corpus snapshot. That is a property of the benchmark, not of
the tool.

Split by "does the message name the gold file" (sealed):

```
n tool neighbours search
names file 20 4 14 4
prose only 17 3 7 1
```

The policy gap reproduces out of sample: `neighbours` beats the default by 10/20
on named tasks and 4/17 on prose.

### Panel change after the first run (and why everything was re-run)

The third-party data gate blocked the first commit: one commit message quoted a
contributor's email address. Messages are now redacted (`<email>`) **inside the
generator**, before the manifest is hashed, which changed the manifest from
`89940dd8...` to `71395d45...`. A changed panel cannot inherit a measurement, so
both splits were re-run from scratch on the redacted panel; the numbers above are
the re-run. The conclusions did not move (sealed is identical; dev `search`
shifted by two tasks because the email split into searchable terms).

### Instrumentation note

The sealed process died twice (after 16 and after 22 tasks) with no traceback and
no Python exception — a hard process exit, cause not identified. The runner now
appends every row to `results_sealed.partial.jsonl` and resumes by id; the table
above is the union of three chunks. Per-task booleans for the first 22 tasks were
visible in the log before the crash; no code, arm or metric changed after that
(the arms were frozen in Amendments 1-3 before the sealed run), so this is
disclosed rather than "clean".

## Threats

- The corpus is one snapshot at `B`. A file renamed after `B` makes its task
unsolvable; those tasks are not filtered (only *absent* files are), so a
rename shows up as a miss and is named in the report rather than hidden.
- Many commits in the range are ours (agents), so the messages are "natural" but
not independent authors. External repos (`DESIGN.md`, B2) are the fix.
- One run per split: dev was re-run after Amendment 3, sealed was not.
- `grep` is a weak floor by construction; `search` is the baseline that matters.

## Decision this creates

The tool currently optimises for "never show an unverified answer". This
benchmark prices that policy: **-33 tasks of 108 solvable** (dev 19 vs 38,
sealed 7 vs 21 for the same tool with neighbours shown).

The options are:

1. keep `abstain` (honest, loses the neighbours);
2. return neighbours as `caveat` when nothing verifies (gains them, risks a
neighbour being read as the answer);
3. keep `abstain` but make the retry obvious to the caller (it already prints
"Pass include_neighbours=true", the measurement says that hint is not acted
on by a single-shot caller).

No option is implemented here — it changes the product's honesty contract.

## What would make this benchmark stronger (next version, new pin)

- External repos (B2): messages and gold written entirely outside this project.
- An issue-text arm: issue bodies are longer and further from the diff than
commit messages.
- The crash investigation: repeated `asyncio.run` + SQLite in one process dies
after ~20 tasks; a per-chunk subprocess would remove the resumption dance.
Loading
Loading