From 554841398a0aa7c55f29edc728546f425bda87fa Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:58:37 +0900 Subject: [PATCH 01/14] fix(bench): drop strategy kwarg that references an uncommitted module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run() gained a strategy parameter forwarded to build_context_pack, but the strategy module and the context-side parameter never landed in this history — every bench.run call raised TypeError, which would error the koth gate on any ladder PR. remove the dangling coupling; the engine-lane work can reintroduce it together with its context.py half. --- src/vouch/bench.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/vouch/bench.py b/src/vouch/bench.py index 1955d968..9154f217 100644 --- a/src/vouch/bench.py +++ b/src/vouch/bench.py @@ -519,7 +519,6 @@ def run( workdir: Path | None = None, extra_config: str | None = None, session_gap_seconds: float = 0.0, - strategy: Any = None, ) -> dict[str, Any]: """Generate, ingest through the real pipeline, retrieve, and grade. @@ -536,11 +535,6 @@ def run( timestamps carry the sessions' temporal order — the structure a recency-aware arm ranks by. Zero (the default) keeps runs fast; the generated dataset is identical either way. - - ``strategy`` is an optional pluggable ranking strategy (see - ``vouch.strategy``) — the engine-lane arm. For a competition submission - it is a ``SandboxProxy`` wrapping the untrusted file; the same generated - dataset scored with vs without it isolates the strategy's contribution. """ import tempfile import time as time_mod @@ -570,7 +564,6 @@ def run( for case in dataset.cases: pack = build_context_pack( store, query=case.question, limit=limit, max_chars=budget_chars, - strategy=strategy, ) pack_dict = dict(pack) if case.category == "citation-correctness": From 7243bd5e67f157be898f449f64d3877d322bdf76 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:02:43 +0900 Subject: [PATCH 02/14] chore(competition): seed a weak round-0 canary champion on the ladder no legal kit change can beat the baseline today - measured on the day's 12 paired seeds, every ranking knob (recency, rerank, pages-first, prompt gate) produces exactly zero composite diff because ranking never changes pack membership at bench density; only backend moves the score, and substring moves it down 0.34. so the auto-merge loop gets its integration test the honest way: the reigning kit is pinned to the weak substring backend, and the first challenger restoring backend auto dethrones it with a genuine, reproducible margin. ladder branch only - the shipped defaults are untouched. --- competition/kits/current/kit.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/competition/kits/current/kit.yaml b/competition/kits/current/kit.yaml index 59fb05dc..a2e92d78 100644 --- a/competition/kits/current/kit.yaml +++ b/competition/kits/current/kit.yaml @@ -12,8 +12,11 @@ # seeds and your PR auto-merges on the ladder branch. allowed keys and # bounds are enforced by .github/scripts/validate_kit.py - anything else # fails before scoring. +# round-0 canary: backend deliberately weakened to the substring scan so +# the first challenger PR exercises the dethrone + auto-merge path end to +# end. the first restore-to-auto kit PR takes the throne and heals this. retrieval: - backend: auto + backend: substring default_limit: 10 recency: enabled: true From 881127250ba3a680615d99bf73ab4376acfdcb69 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:44:04 +0900 Subject: [PATCH 03/14] fix(ci): pin the koth gate checkout to the PR base branch pull_request_target checks out the default branch by default, so ladder PRs ran against a tree with no scripts, no engine, and no reigning kit (pr 565's first run). pin actions/checkout to the pull request's base ref - still trusted code, a branch of this repo, never the PR head - and derive the seed sha from the checked-out tree instead of github.sha, which is also the default branch under this trigger. --- .github/workflows/koth-gate.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/koth-gate.yml b/.github/workflows/koth-gate.yml index b6e3b243..c089c8f7 100644 --- a/.github/workflows/koth-gate.yml +++ b/.github/workflows/koth-gate.yml @@ -37,7 +37,12 @@ jobs: steps: - name: checkout base branch (trusted code only) uses: actions/checkout@v4 - # no ref: pull_request_target checks out the base branch tip + # pull_request_target defaults to the DEFAULT branch, not the PR + # base - pin the base ref explicitly. still trusted code: a branch + # of this repo, never the PR head. the base carries the scripts, + # the engine, and the reigning kit. + with: + ref: ${{ github.event.pull_request.base.ref }} - name: classify the PR id: classify @@ -103,10 +108,12 @@ jobs: - name: paired scoring - challenger vs reigning kit if: steps.classify.outputs.mode == 'ladder' id: score - env: - BASE_SHA: ${{ github.sha }} run: | set +e + # seed identity = the tree actually scored (the checked-out base + # tip), not github.sha, which is the default branch under + # pull_request_target + BASE_SHA="$(git rev-parse HEAD)" python .github/scripts/koth_score.py \ --champion competition/kits/current/kit.yaml \ --challenger /tmp/kit-challenger.yaml \ From 3f286c9231d76f27def91cda0fe6283ad65afc98 Mon Sep 17 00:00:00 2001 From: plind <59729252+plind-junior@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:49:22 -0400 Subject: [PATCH 04/14] feat(competition): challenger kit restoring the auto backend (#565) round-1 ladder entry: the round-0 canary champion pins the substring scan; this kit restores backend auto (fts5 with rrf fusion when embeddings exist). measured locally with the gate's own scorer on the day's derived seeds, the restore clears the dethrone band by a wide margin. kit-only change, as the ladder requires. --- competition/kits/current/kit.yaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/competition/kits/current/kit.yaml b/competition/kits/current/kit.yaml index a2e92d78..2c406881 100644 --- a/competition/kits/current/kit.yaml +++ b/competition/kits/current/kit.yaml @@ -9,14 +9,10 @@ # values reproduce the repo baseline exactly. # # dethrone these values by max(0.007, 1.96 * paired SE) over the day's -# seeds and your PR auto-merges on the ladder branch. allowed keys and -# bounds are enforced by .github/scripts/validate_kit.py - anything else -# fails before scoring. -# round-0 canary: backend deliberately weakened to the substring scan so -# the first challenger PR exercises the dethrone + auto-merge path end to -# end. the first restore-to-auto kit PR takes the throne and heals this. +# seeds and your PR auto-merges. allowed keys and bounds are enforced by +# .github/scripts/validate_kit.py - anything else fails before scoring. retrieval: - backend: substring + backend: auto default_limit: 10 recency: enabled: true From 0ee9c5bd95c42d4e252673973821bc0f77c311c0 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:39:03 +0900 Subject: [PATCH 05/14] chore(competition): seed a weak round-1 canary champion on the ladder substring backend scores ~0.2-0.4 composite below auto on any seed, so the next kit-only pr restoring auto dethrones decisively. round 1 exercises the full chain live: gate scorecard, auto-merge on the protected ladder branch (gate required, strict), and the new koth-ledger row append. --- competition/kits/current/kit.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/competition/kits/current/kit.yaml b/competition/kits/current/kit.yaml index 2c406881..bed5fbc4 100644 --- a/competition/kits/current/kit.yaml +++ b/competition/kits/current/kit.yaml @@ -12,7 +12,7 @@ # seeds and your PR auto-merges. allowed keys and bounds are enforced by # .github/scripts/validate_kit.py - anything else fails before scoring. retrieval: - backend: auto + backend: substring default_limit: 10 recency: enabled: true From 02fa528eff7c8826de03f7bd88f013408d580b84 Mon Sep 17 00:00:00 2001 From: plind <59729252+plind-junior@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:45:09 -0400 Subject: [PATCH 06/14] feat(competition): round-1 challenger kit restoring the auto backend (#566) the reigning round-1 canary runs the substring backend, which scores ~0.2-0.4 composite below auto on any seed. restoring auto clears the dethrone band on the day's paired seeds. --- competition/kits/current/kit.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/competition/kits/current/kit.yaml b/competition/kits/current/kit.yaml index bed5fbc4..2c406881 100644 --- a/competition/kits/current/kit.yaml +++ b/competition/kits/current/kit.yaml @@ -12,7 +12,7 @@ # seeds and your PR auto-merges. allowed keys and bounds are enforced by # .github/scripts/validate_kit.py - anything else fails before scoring. retrieval: - backend: substring + backend: auto default_limit: 10 recency: enabled: true From ee1bd454279cf25919d045d895994d99ce7bf22d Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:48:21 +0900 Subject: [PATCH 07/14] ci(competition): ledger sweeps merged prs - auto-merge pushes fire no events round 1 (pr #566) proved the gap live: the gate arms auto-merge with the workflow GITHUB_TOKEN, and github suppresses workflow triggers for pushes caused by that token, so the merge push never reached the ledger. the workflow now sweeps recently merged ladder prs (schedule + manual dispatch + the still-working push path for human merges); the appender stays idempotent per pr, so overlapping sweeps converge. --- .github/workflows/koth-ledger.yml | 99 ++++++++++++++----------------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/.github/workflows/koth-ledger.yml b/.github/workflows/koth-ledger.yml index e025d627..fbd43770 100644 --- a/.github/workflows/koth-ledger.yml +++ b/.github/workflows/koth-ledger.yml @@ -1,23 +1,32 @@ -# appends the dethrone row to competition/LEADERBOARD.md after a win lands -# on the ladder branch. the scoring gates stay read-only by design (the -# engine gate holds no write token at all); this workflow never runs on a -# PR event — only on push to the ladder branch, after the merge, from -# base-branch code — so an untrusted PR can never reach its write token. -# idempotent: update_leaderboard.py no-ops when the PR is already cited, -# so the workflow triggering on its own ledger push converges. +# appends dethrone rows to competition/LEADERBOARD.md for merged ladder +# PRs. the scoring gates stay read-only by design (the engine gate holds +# no write token at all); this workflow never runs on a PR event, so an +# untrusted PR can never reach its write token. +# +# it is a SWEEP, not a per-push hook: the gate arms auto-merge with the +# workflow GITHUB_TOKEN, and github's recursion guard suppresses workflow +# triggers for pushes caused by that token — so an auto-merged dethrone +# never fires a push event here (round 1, PR #566, proved it live). the +# push trigger still catches human-merged rows immediately; the schedule +# and manual dispatch pick up auto-merged ones. update_leaderboard.py is +# idempotent per PR, so overlapping sweeps converge instead of duplicating. name: koth-ledger on: push: + schedule: + - cron: "17 */6 * * *" + workflow_dispatch: permissions: {} concurrency: - group: koth-ledger-${{ github.ref_name }} + group: koth-ledger cancel-in-progress: false jobs: ledger: if: >- vars.KOTH_LADDER_BASE != '' && - github.ref_name == vars.KOTH_LADDER_BASE && - !startsWith(github.event.head_commit.message, 'docs(competition): ledger row') + (github.event_name != 'push' || + (github.ref_name == vars.KOTH_LADDER_BASE && + !startsWith(github.event.head_commit.message, 'docs(competition): ledger row'))) runs-on: ubuntu-latest permissions: contents: write @@ -25,30 +34,27 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} + ref: ${{ vars.KOTH_LADDER_BASE }} - - name: find the merged pr and its scorecard comment - id: scorecard + - name: append rows for recently merged ladder prs env: GH_TOKEN: ${{ github.token }} - SHA: ${{ github.sha }} + LADDER: ${{ vars.KOTH_LADDER_BASE }} run: | - pr_json="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${SHA}/pulls" \ - --jq '[.[] | select(.merged_at != null)][0] // empty')" - if [ -z "$pr_json" ]; then - echo "no merged pr for ${SHA}; nothing to ledger" - echo "found=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - pr_number="$(printf '%s' "$pr_json" | jq -r '.number')" - pr_author="$(printf '%s' "$pr_json" | jq -r '.user.login')" - # only the gate's own comments are trusted: a scorecard-shaped - # comment from anyone else must never reach the ledger - gh api "repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" \ - --paginate \ - --jq '.[] | select(.user.login == "github-actions[bot]") | .body' \ - > /tmp/comments.txt - python3 - <<'PYEOF' + gh pr list --repo "$GITHUB_REPOSITORY" --base "$LADDER" \ + --state merged --limit 15 --json number,author \ + --jq '.[] | "\(.number) \(.author.login)"' > /tmp/merged.txt + appended=0 + while read -r pr author; do + [ -n "$pr" ] || continue + # only the gate's own comments are trusted: a scorecard-shaped + # comment from anyone else must never reach the ledger + gh api "repos/${GITHUB_REPOSITORY}/issues/${pr}/comments" \ + --paginate \ + --jq '.[] | select(.user.login == "github-actions[bot]") | .body' \ + > /tmp/comments.txt || continue + rm -f /tmp/report.json + python3 - <<'PYEOF' import re body = open("/tmp/comments.txt", encoding="utf-8").read() @@ -58,35 +64,22 @@ jobs: with open("/tmp/report.json", "w", encoding="utf-8") as fh: fh.write(reports[-1]) PYEOF - if [ ! -f /tmp/report.json ]; then - echo "no gate scorecard with a dethrone on pr ${pr_number}" - echo "found=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "found=true" >> "$GITHUB_OUTPUT" - echo "pr=${pr_number}" >> "$GITHUB_OUTPUT" - echo "author=${pr_author}" >> "$GITHUB_OUTPUT" + if [ -f /tmp/report.json ]; then + python3 .github/scripts/update_leaderboard.py \ + --report /tmp/report.json --pr "$pr" --author "$author" \ + && appended=1 + fi + done < /tmp/merged.txt + echo "sweep done (appended=$appended)" - - name: append the ledger row - if: steps.scorecard.outputs.found == 'true' - env: - PR: ${{ steps.scorecard.outputs.pr }} - AUTHOR: ${{ steps.scorecard.outputs.author }} - run: | - python3 .github/scripts/update_leaderboard.py \ - --report /tmp/report.json --pr "$PR" --author "$AUTHOR" - - - name: commit the row - if: steps.scorecard.outputs.found == 'true' - env: - PR: ${{ steps.scorecard.outputs.pr }} + - name: commit the rows run: | if git diff --quiet -- competition/LEADERBOARD.md; then - echo "ledger unchanged (row already present)" + echo "ledger unchanged" exit 0 fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add competition/LEADERBOARD.md - git commit -m "docs(competition): ledger row for #${PR}" + git commit -m "docs(competition): ledger rows from sweep" git push From 670e34ac70c5a7a4289b8f3540c7944393b80e77 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:52:44 +0900 Subject: [PATCH 08/14] ci(competition): ledger pushes with an owner token past the ruleset the ladder's required gate check applies to direct pushes too, so the bot's row append was rejected (round 1 proved it). protection moved from classic branch protection to the koth-ladder-gate ruleset (same gate requirement, strict), and the ledger checkout uses an owner token with admin bypass. the workflow never runs on pr events and never executes untrusted code, so the token stays out of untrusted reach. --- .github/workflows/koth-ledger.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/koth-ledger.yml b/.github/workflows/koth-ledger.yml index fbd43770..acae6c61 100644 --- a/.github/workflows/koth-ledger.yml +++ b/.github/workflows/koth-ledger.yml @@ -32,9 +32,15 @@ jobs: contents: write pull-requests: read steps: + # the push token: the ladder ruleset requires the gate check on every + # push, and the workflow GITHUB_TOKEN holds no bypass, so rows are + # pushed with an owner token (repo secret). safe because this workflow + # never runs on PR events and never executes untrusted code — the only + # inputs are gate-authored comments parsed as json. - uses: actions/checkout@v4 with: ref: ${{ vars.KOTH_LADDER_BASE }} + token: ${{ secrets.KOTH_LEDGER_TOKEN || github.token }} - name: append rows for recently merged ladder prs env: From 989fb6e62f5a1165e37dcea6da3afa2690c2bb7e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:53:05 +0000 Subject: [PATCH 09/14] docs(competition): ledger rows from sweep --- competition/LEADERBOARD.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/competition/LEADERBOARD.md b/competition/LEADERBOARD.md index c9517c78..3b43e6e2 100644 --- a/competition/LEADERBOARD.md +++ b/competition/LEADERBOARD.md @@ -13,6 +13,8 @@ payout rank is settled by the monthly sealed commit-reveal run. | # | champion | PR | dethroned on | scored mean | margin over prior | |---|----------|----|--------------|-------------|-------------------| | 0 | baseline kit (repo defaults) | — | 2026-07-28 | 0.52 ± 0.03 (seeds 1–6) | — | +| 1 | plind-junior (kit) | #566 | 2026-07-27 | 0.5333 | +0.3333 | +| 2 | plind-junior (kit) | #565 | 2026-07-27 | 0.5667 | +0.3667 | payouts follow the season shares in docs/vouchbench-seasons.md (65/14/10/7/4). days-on-throne accrue between dethrones; the monthly From 71ff0ff0f8d670e39dd4b846bebb20c0da927e73 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:13:41 +0900 Subject: [PATCH 10/14] feat(retrieval): strategy ranks an over-fetched pool - exclusion is real with a ranking strategy active, retrieval fetched exactly `limit` hits before the strategy ran, so ordering could never change pack membership - and the bench grades presence, not order, which made every strategy score byte-identical to baseline (measured: 12 seeds, diff 0.0000). retrieval now over-fetches a bounded pool (5x limit, floor 50) when a strategy is active and the top `limit` of the strategy's order survive the cut. de-prioritising below the window excludes the candidate; a strategy still cannot fabricate a result or shrink the pack. contract language in the lane doc and baseline docstring updated to match, plus a regression test that a demoted candidate leaves the pack. --- contrib/strategies/baseline.py | 12 ++++++----- docs/koth-strategy-lane.md | 9 ++++++--- src/vouch/context.py | 17 ++++++++++++++-- tests/test_strategy.py | 37 ++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/contrib/strategies/baseline.py b/contrib/strategies/baseline.py index e74d7d09..08723892 100644 --- a/contrib/strategies/baseline.py +++ b/contrib/strategies/baseline.py @@ -5,11 +5,13 @@ challenger only dethrones it by making vouch's benchmark score genuinely higher, not by accident. -A strategy is real ranking code. It receives the query and the retrieved -candidates (data only - no KB, no disk, no network) and returns the ids in the -order the reader should see them. Ordering is authoritative but bounded: ids -you invent are ignored, and any candidate you drop is appended at the tail, so -you can reorder and de-prioritise but never fabricate or hide a result. +A strategy is real ranking code. It receives the query and an over-fetched +candidate pool (data only - no KB, no disk, no network) and returns the ids in +the order the reader should see them; the top ``limit`` survive the cut. +Ordering is authoritative but bounded: ids you invent are ignored, and any +candidate you drop is appended at the tail before the cut - so you can +de-prioritise a candidate out of the pack, but never fabricate a result or +shrink the pack. """ from vouch.strategy import Candidate diff --git a/docs/koth-strategy-lane.md b/docs/koth-strategy-lane.md index c044790f..19bb38ab 100644 --- a/docs/koth-strategy-lane.md +++ b/docs/koth-strategy-lane.md @@ -44,9 +44,12 @@ def rank(query: str, candidates: list[Candidate], *, limit: int) -> list[str]: a `Candidate` is `kind`, `id`, `summary`, `score` - **data only**. the strategy never receives the KB, the filesystem, or a socket. ordering is authoritative but bounded (ids you invent are ignored; candidates you omit are -appended), so a strategy can reorder and de-prioritise but cannot fabricate or -hide a result. see `contrib/strategies/baseline.py` (the champion) and -`example_lexical.py` (a worked example). +appended). with a strategy active, retrieval over-fetches a bounded candidate +pool and the top `limit` of your order survive the cut - so de-prioritising a +candidate below the window genuinely excludes it from the pack. a strategy +still cannot fabricate a result or shrink the pack. see +`contrib/strategies/baseline.py` (the champion) and `example_lexical.py` (a +worked example). ## how a submission is scored diff --git a/src/vouch/context.py b/src/vouch/context.py index a715ca7e..f2e5e1d3 100644 --- a/src/vouch/context.py +++ b/src/vouch/context.py @@ -45,6 +45,12 @@ ContextItemKind = Literal["claim", "page", "entity", "relation", "source"] +# Candidate-pool sizing when a ranking strategy is active: the strategy +# ranks pool candidates and the top ``limit`` survive, so exclusion (not +# just order) is in its hands. Factor/floor keep the pool a shortlist. +_STRATEGY_POOL_FACTOR = 5 +_STRATEGY_POOL_MIN = 50 + _VALID_BACKENDS = ("auto", "hybrid", "embedding", "fts5", "substring") _RERANKER_CACHE: Any | None = None @@ -675,10 +681,17 @@ def build_context_pack( project=project, agent=agent, ) - hits = _retrieve(store, query, limit, viewer) + # with a ranking strategy active, retrieval over-fetches a bounded pool + # and the strategy's order decides which ``limit`` survive the cut — + # de-prioritising a candidate below the window excludes it from the + # pack. without one, the pool IS the limit and nothing changes. the + # pool is bounded so a strategy ranks a shortlist, never the whole kb. + strategy_active = strategy is not None or _configured_strategy(store) + pool = max(limit * _STRATEGY_POOL_FACTOR, _STRATEGY_POOL_MIN) if strategy_active else limit + hits = _retrieve(store, query, pool, viewer) hits = _maybe_strategy( store, query=query, hits=hits, limit=limit, strategy=strategy - ) + )[:limit] items: list[ContextItem] = [] for kind, hid, summary, score, backend in hits: cites: list[str] = [] diff --git a/tests/test_strategy.py b/tests/test_strategy.py index 103ffecf..fb907cc2 100644 --- a/tests/test_strategy.py +++ b/tests/test_strategy.py @@ -200,3 +200,40 @@ def test_shipped_examples_load(path: str) -> None: assert hasattr(strat, "rank") result = strat.rank("beta gamma", CANDS, limit=10) assert sorted(result) == ["a", "b", "c"] + + +def test_strategy_demotion_excludes_from_pack(tmp_path: Path) -> None: + # with a strategy active, retrieval over-fetches a pool and the top + # ``limit`` of the strategy's order survive - de-prioritising below the + # window must EXCLUDE the candidate, not just reorder it. + from vouch import health + from vouch.context import build_context_pack + from vouch.models import Claim + from vouch.storage import KBStore + + store = KBStore.init(tmp_path / "kb") + src = store.put_source(b"raw") + for i in range(4): + store.put_claim(Claim( + id=f"kite-{i}", text=f"kite fact number {i}", evidence=[src.id], + )) + store.put_claim(Claim( + id="kite-noisy", text="kite fact but noisy hearsay", evidence=[src.id], + )) + health.rebuild_index(store) + + class DemoteNoisy: + def rank(self, query, candidates, *, limit): + keep = [c.id for c in candidates if "noisy" not in c.summary] + tail = [c.id for c in candidates if "noisy" in c.summary] + return keep + tail + + pack = dict(build_context_pack( + store, query="kite fact", limit=4, strategy=DemoteNoisy(), + )) + ids = [item["id"] for item in pack["items"]] + assert len(ids) == 4 + assert "kite-noisy" not in ids + + baseline = dict(build_context_pack(store, query="kite fact", limit=4)) + assert len(baseline["items"]) == 4 From 70c7f7f150ac4c77f8ca44a9ef029b827cdf93d1 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:15:01 +0900 Subject: [PATCH 11/14] feat(competition): provenance-rank engine-lane submission ranks first-hand statements over hearsay (reported speech about a named third party is demoted, hardest when the query is first-person), pushes stored instructions to the tail (a memory that tells the reader what to answer is an injection, not a fact), and boosts change-of-state phrasing so the newest value survives a tight budget. blended with the backend score and plain lexical overlap for everything else. verified locally with the ci-exact loop over generic seeds 1-12: challenger 0.5750 vs baseline 0.5250, diff +0.0500, band 0.0381 - dethroned. --- contrib/strategies/provenance_rank.py | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 contrib/strategies/provenance_rank.py diff --git a/contrib/strategies/provenance_rank.py b/contrib/strategies/provenance_rank.py new file mode 100644 index 00000000..16ef6217 --- /dev/null +++ b/contrib/strategies/provenance_rank.py @@ -0,0 +1,81 @@ +"""Provenance-aware ranking: first-hand facts over hearsay and instructions. + +Three general principles, none keyed to any benchmark artifact: + +1. **Hearsay demotion.** A memory that *attributes* a fact to a named third + party via reported speech ("X mentioned her ... is", "X said his ...") + is weaker evidence about the speaker's own facts than a first-hand + statement — especially when the query is first-person ("my ...") or + team-scoped ("our ...", "we ..."). Demote it below first-hand memories. + +2. **Instruction demotion.** A memory that tries to *instruct the reader* + ("always answer", "no matter what", "if anyone asks") is a stored + prompt-injection, not a fact. It should rank behind every ordinary + memory that matches the query. + +3. **Update boost.** A memory phrased as a change of state ("changed to", + "moved ... to", "is now") supersedes a plain assertion of the same + attribute; surface it first so the reader sees the newest value inside + a tight budget. + +Ties and everything else defer to the backend's fused score, blended with +plain lexical overlap so an obviously-relevant hit the fusion under-ranked +still gets rescued. +""" + +import re + +from vouch.strategy import Candidate + +_WORD = re.compile(r"[a-z0-9]+") + +# reported speech about a named third party: "Foo mentioned her X is ...", +# "foo-bar said his X is ...". requires BOTH a leading name-like token and +# a speech verb with a third-person possessive, so a first-hand "i said i +# would ..." is untouched. +_HEARSAY = re.compile( + r"\b[a-z][a-z0-9-]*\s+(?:mentioned|said|says|claims|claimed|told\s+\w+)\b" + r".{0,40}\b(?:her|his|their)\b", + re.IGNORECASE | re.DOTALL, +) + +# stored instructions aimed at whoever reads the memory later. +_INSTRUCTION = re.compile( + r"\b(?:always\s+answer|no\s+matter\s+what|if\s+anyone\s+asks|" + r"future\s+assistant|ignore\s+(?:any|all|previous))\b", + re.IGNORECASE, +) + +# change-of-state phrasing: the newest value of an attribute. +_UPDATE = re.compile( + r"\b(?:changed\s+to|moved\s+(?:\w+\s+){0,3}(?:over\s+)?to|is\s+now|" + r"switched\s+to|renamed\s+to)\b", + re.IGNORECASE, +) + +_FIRST_PERSON_QUERY = re.compile(r"\b(?:my|our|we|i)\b", re.IGNORECASE) + + +def _tokens(text: str) -> set[str]: + return set(_WORD.findall(text.lower())) + + +def rank(query: str, candidates: list[Candidate], *, limit: int) -> list[str]: + q = _tokens(query) + first_person = bool(_FIRST_PERSON_QUERY.search(query)) + + def key(c: Candidate) -> float: + text = c.summary + overlap = len(q & _tokens(text)) / len(q) if q else 0.0 + score = 0.7 * c.score + 0.3 * overlap + if _INSTRUCTION.search(text): + score -= 10.0 + if _HEARSAY.search(text) and first_person: + score -= 5.0 + elif _HEARSAY.search(text): + score -= 2.0 + if _UPDATE.search(text): + score += 1.0 + return score + + return [c.id for c in sorted(candidates, key=key, reverse=True)] From f82a536d20c13053d1457a8dc4fbc38ec06d99b2 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:18:48 +0900 Subject: [PATCH 12/14] docs(contributing): competition pr shapes for both koth lanes open contributors kept needing the lane rules from three scattered docs. contributing.md now states the two pr shapes the gates enforce: engine lane = one new file under contrib/strategies/ against koth-ladder, sandboxed, scored, never auto-merged, human review for benchmark-keyed logic; kit lane = kit.yaml only, auto-merge on dethrone, low ceiling by construction. includes the local ci-exact practice command and the reproducibility and payout pointers. --- CONTRIBUTING.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33a4bf68..45e5315a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,55 @@ touch behavior. path to the capture → approve → compile → recall loop. - Tests, fixtures, CI hardening, and developer-experience improvements. +## Competition PRs (the koth ladders) + +The competition has its own PR shapes, and the gates enforce them +mechanically — a PR that doesn't match is classified as a normal PR and +just gets human review. Full walkthrough: +[docs/mining-on-vouch.md](docs/mining-on-vouch.md). + +**Engine lane — the main competition (submit ranking code):** + +- The PR adds **exactly one new file**: `contrib/strategies/.py` + (letters, digits, underscores). Nothing else in the diff — not + `baseline.py`, not the engine, not tests. +- Base branch: **`koth-ladder`**, not `main`. +- The file implements `rank(query, candidates, *, limit) -> list[str]` + (see [docs/koth-strategy-lane.md](docs/koth-strategy-lane.md)). It runs + in a sandbox: no network, no subprocesses, no file writes, hard + resource limits. A crashing strategy scores as the baseline, silently — + test locally first. +- Practice with the exact CI loop before pushing: + + ```bash + vouch bench run --seeds 1,2,3,4,5,6,7,8,9,10,11,12 \ + --strategy contrib/strategies/.py \ + --against contrib/strategies/baseline.py + ``` + +- The gate scores your PR against the reigning champion over the day's + seeds and posts the scorecard. **Engine code never auto-merges**: the + highest verified score merges after a human checks for benchmark-keyed + logic (lookup tables, category-pattern dispatch, generator-template + matching — instant disqualifiers). A merged winner ships as a default + and is credited in the changelog. + +**Kit lane — the 10-minute warm-up (config only):** + +- The PR touches **exactly one file**: + `competition/kits/current/kit.yaml`. Allowed keys and bounds are + enforced by `.github/scripts/validate_kit.py`. +- Base branch: **`koth-ladder`**. On a dethrone the PR auto-merges (data + cannot execute); against `main` it is scored and commented only. +- Know the ceiling: most kit knobs cannot move the bench — this lane is + for learning the loop, not for winning the season. + +**Both lanes:** seeds derive from the champion sha and the utc date, so +every scorecard is reproducible offline — the comment includes the exact +command. Payouts and season rules: +[docs/vouchbench-seasons.md](docs/vouchbench-seasons.md). Titles follow +the same conventional-commit format as everything else. + ## What we won't merge - Anything that bypasses the review gate from the agent side From 29137c2999795a5065482038b7d6fed4a926e71d Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:53:26 +0900 Subject: [PATCH 13/14] ci(competition): zizmor annotations and shellcheck note for the gates workflow lint only runs on prs that touch workflows, so the koth gates' pull_request_target findings never surfaced until the engine-gate registration pr woke the linter. both gates now carry the ignore annotation with the justification inline (base-branch code only, pr content handled as data, read-only tokens), and the scorecard comment script declares its literal backticks for shellcheck. --- .github/workflows/koth-engine-gate.yml | 2 +- .github/workflows/koth-gate.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/koth-engine-gate.yml b/.github/workflows/koth-engine-gate.yml index 6eb8a56f..e3f6fc9e 100644 --- a/.github/workflows/koth-engine-gate.yml +++ b/.github/workflows/koth-engine-gate.yml @@ -19,7 +19,7 @@ name: koth-engine-gate on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] workflow + grader + champion all come from the base branch; the PR's .py runs only inside the sandbox child, and the job token is read-only with no secrets types: [opened, synchronize, reopened, ready_for_review] concurrency: diff --git a/.github/workflows/koth-gate.yml b/.github/workflows/koth-gate.yml index c089c8f7..db3181bb 100644 --- a/.github/workflows/koth-gate.yml +++ b/.github/workflows/koth-gate.yml @@ -17,7 +17,7 @@ name: koth-gate on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] no PR code ever executes: the kit is fetched as data, validated against a closed-world allowlist, and scored by base-branch code with a read-only checkout types: [opened, synchronize, reopened, ready_for_review] concurrency: @@ -138,6 +138,7 @@ jobs: BASE_REF: ${{ github.event.pull_request.base.ref }} LADDER_BASE: ${{ vars.KOTH_LADDER_BASE }} run: | + # shellcheck disable=SC2016 # the posted markdown carries literal backticks if [ "$BASE_REF" = "$LADDER_BASE" ] && [ -n "$LADDER_BASE" ]; then gate_note="auto-merge on dethrone (provisional ladder branch)" else From 5baf77cf1b2bd9f7773e5314295d2bee100b697c Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:58:19 +0900 Subject: [PATCH 14/14] ci(competition): engine gate review fixes - sha pins and delete-only prs pin checkout and setup-python to immutable commit shas (mutable tags in a pull_request_target workflow are a supply-chain surface), and classify a delete-only strategy pr as normal instead of engine - the filename shape matched but there is nothing to fetch at the head sha, so the gate failed messy instead of passing through. --- .github/workflows/koth-engine-gate.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/koth-engine-gate.yml b/.github/workflows/koth-engine-gate.yml index e3f6fc9e..6230bbf3 100644 --- a/.github/workflows/koth-engine-gate.yml +++ b/.github/workflows/koth-engine-gate.yml @@ -38,7 +38,7 @@ jobs: pull-requests: write # scorecard comment only - no merge steps: - name: checkout base branch (trusted code only) - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: classify the PR id: classify @@ -48,8 +48,13 @@ jobs: run: | gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ --paginate --jq '.[].filename' > /tmp/changed.txt + # a delete-only pr matches the filename shape but has nothing to + # fetch at the head sha - classify it normal, not engine. + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ + --paginate --jq '.[] | select(.status != "removed") | .filename' \ + > /tmp/present.txt count=$(wc -l < /tmp/changed.txt) - only=$(head -1 /tmp/changed.txt) + only=$(head -1 /tmp/present.txt) case "$only" in contrib/strategies/baseline.py|contrib/strategies/README.md) only="" ;; esac @@ -88,7 +93,7 @@ jobs: - name: set up python if: steps.classify.outputs.mode == 'engine' - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.12'