feat(bench): derivation categories — consolidation, multi-hop, temporal-depth, aggregation - #634
Conversation
…al, aggregation CATEGORIES had no passive-consolidation, multi-hop-relational, temporal-depth or aggregation, so win condition W3 was unmeasured and the pages-first work had no number attached to it. these four differ from every existing category in one way that decides the design: the graded fact is stated in no single claim. it only follows from several. so an expected-answer substring check is impossible — the answer string appears nowhere in the corpus, and a generator that wrote it somewhere would degenerate the category into single-session recall, which is the failure mode the issue warns about. they are graded instead on a new MemoryCase.required: every supporting part must reach the pack inside the budget. all-or-nothing like every other category, with the shortfall named in the failure reason. the dump-guard still outranks it, so a forbidden leak zeroes the case before parts are counted. the generators draw from a derived rng (seed ^ salt) and their own pools, never advancing the main stream. that keeps the ten existing categories byte-identical — same attributes, values, questions, placements — so their recorded per-category scores stay comparable across this change. pinned in test_existing_categories_are_unchanged_by_derivation_cases against the pre-change values for seed 1. the session documents do grow, because new facts have to be planted somewhere, so the composite moves 0.58 -> 0.64 purely from four rows joining the mean. measured on stock config over seeds 1-6: consolidation, temporal-depth and aggregation all 1.00, and the ten originals reproduce their 2026-07-28 values exactly. W3 asked for > 0.5 on consolidation against ditto's 0.00, so the bar is cleared without the pages-first lever — the parts are short and co-topical, so ordinary retrieval assembles them under 2000 chars. the number does not say whether the pack answered from a compiled page or from n raw claims; that needs a compile step in run() and a grader that inspects which item carried the parts, which is left out of scope. multi-hop-relational is where these bite today: 0.17, because a three-link chain shares no term with the question at any hop. the reference baseline table in the module docstring is refreshed, with the new lever called out. Closes vouchdev#617
There was a problem hiding this comment.
Pull request overview
Adds four new vouchbench “derivation” categories where the graded fact is not present in any single claim, extending the benchmark to measure multi-claim synthesis (and specifically to cover the missing axes called out in #617) while keeping existing per-category scores reproducible and comparable.
Changes:
- Introduces four new benchmark categories:
passive-consolidation,multi-hop-relational,temporal-depth, andaggregation. - Adds
MemoryCase.requiredand grading logic that scores these cases by whether all required parts reached the context pack under budget (with detailed shortfall reasons). - Expands
tests/test_bench.pyto pin the original 10 categories’ seed-1 outputs and to cover required-part grading and end-to-end scoring for the new categories; updatesCHANGELOG.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/vouch/bench.py |
Adds derivation-category generators, MemoryCase.required, and required-part grading to extend benchmark coverage without perturbing the original categories’ RNG stream. |
tests/test_bench.py |
Adds tests for the new categories’ invariants, required-part grading behavior, and ensures the original ten categories remain byte-identical for pinned seeds. |
CHANGELOG.md |
Documents the new benchmark categories, grading approach, and reported baseline score impact. |
Comments suppressed due to low confidence (1)
src/vouch/bench.py:524
- This comment claims each passive-consolidation part is stated “in its own session”, but sub_spread will reuse session indices when sessions < number of parts (e.g., sessions=4). Consider rewording to match the behavior: parts are spread across sessions as distinct as possible, and the whole is never stated.
# 11. passive-consolidation: each part of a whole is stated in its own
# session and the whole is never stated. Answering needs all of them at
# once, which is what a compiled page can do and a raw-claim pack cannot
# inside the same budget. The direct W3 measurement.
| * **Derivation axes.** Four categories ask for a fact stated in no single | ||
| claim: passive-consolidation (parts of a whole, each in its own session), | ||
| multi-hop-relational (a three-link chain the question names no link of), | ||
| temporal-depth (a value's whole history, not its current value), and | ||
| aggregation (list-all over separately stated members). Because the answer |
| assert len(set(case.required)) == len(case.required), case.category | ||
|
|
||
|
|
||
| def test_derivation_parts_are_planted_in_separate_sessions() -> None: |
| # 13. temporal-depth: three values in chronological order. Unlike | ||
| # point-in-time (one prior value) the question is about the history, so a | ||
| # pack that keeps only the current value scores zero. |
three comments overstated what the generators guarantee. passive-consolidation was described as planting each part "in its own session". sub_spread only keeps parts distinct while the session count allows — five parts under sessions=4 (which tests/test_bench.py passes) land in two or three sessions. the invariant the category actually rests on is the weaker one: no single session carries every part. temporal-depth was described as "three values"; it plants one per _HISTORY_TEMPLATES entry, which is four. test_derivation_parts_are_planted_in_separate_sessions asserted the correct weaker property under a name promising the stronger one, so it is renamed to what it checks and now runs at sessions=3 and 4 as well as the default — the crowded case the old wording denied could happen. comments only; no generator behaviour changes and no score moves.
|
addressed all three review comments — they were right, and one of them was right for a reason worth writing down. passive-consolidation's "each in its own session" was wrong, not just loosely worded. what the category actually rests on is the weaker property — no single session carries every part, so the question is never answerable from one document. the module docstring and the block comment now say that instead. temporal-depth said "three values"; it plants one per the test name promised the strong invariant while asserting the weak one, so it is renamed to comments and one test name; no generator behaviour changed, so every recorded score stands. also merged the design question from the description is still open and still the one worth your attention: these grade on |
|
diff coverage: 100% — every python line this PR changes under |
Closes #617
adds
passive-consolidation,multi-hop-relational,temporal-depthandaggregation. W3 was unmeasured and the pages-first work had no number attached to it.the one design decision worth your eyes
these four differ from every existing category in a way that decides the grading: the graded fact is stated in no single claim. it only follows from several. so an expected-answer substring check is impossible — the answer string appears nowhere in the corpus, and a generator that wrote it somewhere would degenerate the category into single-session recall, which is exactly the failure mode the issue warns about.
so they are graded on a new
MemoryCase.required: every supporting part must reach the pack inside the budget. all-or-nothing like every other category, with the shortfall named in the failure reason (assembled 3/5 parts; missing 'x', 'y'). the dump-guard still outranks it — a forbidden leak zeroes the case before parts are counted.if you would rather these grade on something else, this is the decision to redirect. the alternative i considered and rejected: plant the derived sentence and check it directly, which is the degeneration the issue calls out.
existing scores stay comparable
the generators draw from a derived rng (
seed ^ salt) and their own pools, and never advance the main stream. so the ten existing categories generate byte-identically — same attribute assignments, coined values, question phrasings, session placements. verified across seeds 1/2/3/7 against the pre-change generator, and pinned intest_existing_categories_are_unchanged_by_derivation_casesagainst seed 1's actual pre-change values, so a future edit that moves the main stream fails loudly.the seeds-1-6 run confirms it end to end — all ten reproduce their recorded 2026-07-28 numbers exactly:
the composite moves only because four rows joined the mean. session documents do grow — new facts have to be planted somewhere — but that did not disturb a single existing category's score.
what the new numbers say
W3 is already cleared, and not for the reason the plan expected. it asks for > 0.5 on passive-consolidation against ditto's 0.00; stock config gives 1.00 without the pages-first lever, because the parts are short and co-topical so ordinary retrieval assembles all five inside 2000 chars.
i want to be straight that this is a weaker result than the number looks.
required-style grading proves the parts reached the pack; it cannot say whether the pack answered from a compiled page or from five raw claims. distinguishing those needs a compile step inrun()and a grader that inspects which item carried the parts — a bigger change with its own perf implications, so it is out of scope here and flagged rather than silently implied. if you would rather W3 be measured that way from the start, say so and i will do it instead.multi-hop-relationalat 0.17 is where these actually bite: a three-link chain (i work with X→X owns Y→Y stores in Z) loses a link, because no hop shares a term with the question. that is the new lever, and the one the ladder should be paying to move.i deliberately did not tune set sizes to manufacture low scores. the first draft used 3 parts and scored 1.00 across all four; i raised consolidation/aggregation to five members and multi-hop to three links because those are natural sizes for a checklist, a service list and a dependency chain — then reported whatever came out. three of four landing at 1.00 is a real finding about vouch, not a broken category.
scope note
#617 suggests landing this together with #616's multipliers under one bench version bump. this PR is deliberately only the categories: #616 changes the composite formula, which makes every recorded score and ladder entry incomparable and needs a season-boundary decision (season 1 is live in #573). that is a maintainer call, not something to bundle in. this change keeps per-category scores comparable, which is the whole reason for the derived rng.
tests
12 new cases in
tests/test_bench.py:requiredand noexpected— the never-stated invariantrequiredgrading: all-present passes, shortfall reportsn/m partsand names the missing, case-insensitive, and forbidden-outranks-requiredrun()produces a graded number in[0, 1]for each new axis through the real pipelineverification
no core paths touched —
bench.pyand its tests only, so no newkb.*method, no registration sites, no review-gate surface.