Repository navigation
maint-79: grow verifier corpus from realized PR outcomes (#2819 move 2) #2832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| name: Maint 79 Verifier Corpus Harvest | ||
|
|
||
| # Grows the frozen verifier evaluation corpus from realized PR outcomes | ||
| # (stranske/Workflows#2819, move 2). High-confidence cases (stable merges, | ||
| # reverts, resolved verifier follow-ups) auto-promote via an auto-merging PR; | ||
| # ambiguous cases land in an FYI-only, auto-expiring staging file. No human | ||
| # action is required — the PR is the audit trail, not a gate. | ||
| # | ||
| # On pull_request touching the harvester/config it only runs the unit tests | ||
| # (the corpus is grown from live data, so there is no deterministic drift gate). | ||
|
|
||
| on: | ||
| schedule: | ||
| # Weekly, Monday 05:40 UTC (after maint-68 sync + maint-77 freshness). | ||
| - cron: "40 5 * * 1" | ||
| workflow_dispatch: | ||
| inputs: | ||
| write: | ||
| description: "Persist and open a PR (false = dry-run summary only)." | ||
| type: boolean | ||
| default: true | ||
| pull_request: | ||
| paths: | ||
| - tools/harvest_verifier_corpus.py | ||
| - tests/tools/test_harvest_verifier_corpus.py | ||
| - config/model_selection_policy.json | ||
| - config/model_eval_corpus_staging.json | ||
|
Comment on lines
+23
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Run pilot-corpus validation for harvest PRs.
Proposed fix pull_request:
paths:
- tools/harvest_verifier_corpus.py
- tests/tools/test_harvest_verifier_corpus.py
+ - tests/tools/test_model_eval_pilot.py
- config/model_selection_policy.json
- config/model_eval_corpus_staging.json
+ - config/model_eval_pilot.json
...
- name: Unit tests
- run: python -m pytest tests/tools/test_harvest_verifier_corpus.py -q
+ run: python -m pytest tests/tools/test_harvest_verifier_corpus.py tests/tools/test_model_eval_pilot.py -qAs per path instructions, “Flag new or changed behavior with no accompanying test.” Also applies to: 47-48 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| test: | ||
| if: github.event_name == 'pull_request' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
|
|
||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-python@v6 | ||
|
|
||
| with: | ||
| python-version: "3.14" | ||
| - name: Unit tests | ||
| run: python -m pytest tests/tools/test_harvest_verifier_corpus.py -q | ||
|
|
||
| harvest: | ||
| if: github.event_name != 'pull_request' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
|
|
||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-python@v6 | ||
|
|
||
| with: | ||
| python-version: "3.14" | ||
| - name: Harvest realized-outcome cases | ||
| env: | ||
| # Cross-repo read; github.token is scoped to Workflows only. | ||
| GH_TOKEN: ${{ secrets.OWNER_PR_PAT }} | ||
| run: | | ||
| mode="" | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.write }}" != "true" ]; then | ||
| mode="dry-run" | ||
| python -m tools.harvest_verifier_corpus | tee harvest.log | ||
| else | ||
| python -m tools.harvest_verifier_corpus --write | tee harvest.log | ||
| fi | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| { | ||
| echo '## Verifier corpus harvest' | ||
| echo '```' | ||
| cat harvest.log | ||
| echo '```' | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
Comment on lines
+48
to
+59
|
||
| - name: Open auto-merging corpus-growth PR | ||
| if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.write) | ||
| id: cpr | ||
| uses: peter-evans/create-pull-request@v8 | ||
|
|
||
| with: | ||
| token: ${{ secrets.OWNER_PR_PAT }} | ||
| branch: verifier-corpus-harvest/auto | ||
| base: main | ||
| title: "corpus: harvest realized-outcome verifier cases" | ||
| commit-message: "corpus: grow verifier eval corpus from realized PR outcomes" | ||
| labels: "automation,model-selection" | ||
| delete-branch: true | ||
| body: | | ||
| Automated corpus growth (stranske/Workflows#2819 move 2). | ||
|
|
||
| High-confidence cases derived from realized PR outcomes — see the run | ||
| summary for the promoted case list. Ambiguous cases were routed to the | ||
| auto-expiring staging file, not here. | ||
|
|
||
| Expected verdicts here come from what the world already adjudicated by | ||
| merging or reverting each PR. The semantic NON_PASS categories | ||
| (stale-verifier-claim, review-thread-debt, missing-acceptance-criterion) | ||
| remain owner-sourced and are never machine-added. | ||
| - name: Enable auto-merge | ||
| if: steps.cpr.outputs.pull-request-number | ||
| env: | ||
| GH_TOKEN: ${{ secrets.OWNER_PR_PAT }} | ||
| run: gh pr merge "${{ steps.cpr.outputs.pull-request-number }}" --squash --auto | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "schema": "verifier-corpus-staging/v1", | ||
| "note": "FYI-only. Auto-expiring candidate cases pending stability or adjudication; nothing here gates anything.", | ||
| "cases": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| """Tests for tools/harvest_verifier_corpus.py (realized-outcome corpus growth).""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| from datetime import UTC, datetime | ||
|
|
||
| from tools import harvest_verifier_corpus as hv | ||
|
|
||
| NOW = datetime(2026, 7, 25, tzinfo=UTC) | ||
|
|
||
|
|
||
| def _rec(pr, *, merged=True, days_ago=40, reverted=False, followup=False, resolved=False): | ||
| merged_at = None | ||
| if merged: | ||
| merged_at = datetime(2026, 7, 25, tzinfo=UTC).fromordinal(NOW.toordinal() - days_ago) | ||
| merged_at = merged_at.replace(tzinfo=UTC).isoformat() | ||
| return { | ||
| "repo": "stranske/Demo", | ||
| "pr": pr, | ||
| "merged": merged, | ||
| "merged_at": merged_at, | ||
| "reverted": reverted, | ||
| "verifier_followup": followup, | ||
| "followup_resolved": resolved, | ||
| } | ||
|
|
||
|
|
||
| def test_stable_merge_is_high_confidence_pass(): | ||
| label = hv.classify(_rec(1, days_ago=40), now=NOW, stability_days=30) | ||
| assert label == {"expected_verdict": "PASS", "category": "clean-pass", "confidence": "high"} | ||
|
|
||
|
|
||
| def test_recent_merge_is_low_confidence_pass(): | ||
| label = hv.classify(_rec(2, days_ago=5), now=NOW, stability_days=30) | ||
| assert label["expected_verdict"] == "PASS" | ||
| assert label["confidence"] == "low" # not yet stable -> staging | ||
|
|
||
|
|
||
| def test_reverted_is_high_confidence_non_pass(): | ||
| label = hv.classify(_rec(3, days_ago=10, reverted=True), now=NOW, stability_days=30) | ||
| assert label == { | ||
| "expected_verdict": "NON_PASS", | ||
| "category": "regression-after-merge", | ||
| "confidence": "high", | ||
| } | ||
|
|
||
|
|
||
| def test_resolved_followup_is_high_confidence_non_pass(): | ||
| label = hv.classify(_rec(4, followup=True, resolved=True), now=NOW, stability_days=30) | ||
| assert label["category"] == "follow-up-required" | ||
| assert label["confidence"] == "high" | ||
|
|
||
|
|
||
| def test_unresolved_followup_stages(): | ||
| label = hv.classify(_rec(5, followup=True, resolved=False), now=NOW, stability_days=30) | ||
| assert label["expected_verdict"] == "NON_PASS" | ||
| assert label["confidence"] == "low" | ||
|
|
||
|
|
||
| def test_unmerged_pr_yields_no_signal(): | ||
| assert hv.classify(_rec(6, merged=False), now=NOW, stability_days=30) is None | ||
|
|
||
|
|
||
| def test_partition_splits_promote_and_stage(): | ||
| recs = [_rec(1, days_ago=40), _rec(2, days_ago=3), _rec(3, reverted=True)] | ||
| promote, stage = hv.partition(recs, now=NOW, stability_days=30) | ||
| assert {c["pr"] for c in promote} == {1, 3} | ||
| assert {c["pr"] for c in stage} == {2} | ||
|
|
||
|
|
||
| def test_grow_corpus_dedups_and_caps_and_bumps_version(): | ||
| corpus = {"corpus_version": "v2026-07-12", "cases": [{"repo": "stranske/Demo", "pr": 1}]} | ||
| promote = [ | ||
| hv.to_case( | ||
| _rec(1, days_ago=40), {"expected_verdict": "PASS", "category": "clean-pass"}, now=NOW | ||
| ), | ||
| hv.to_case( | ||
| _rec(9, days_ago=40), {"expected_verdict": "PASS", "category": "clean-pass"}, now=NOW | ||
| ), | ||
| ] | ||
| grown, added = hv.grow_corpus(corpus, promote, max_size=150) | ||
| assert [c["pr"] for c in added] == [9] # pr 1 already present -> deduped | ||
| assert grown["corpus_version"] == "v2026-07-12+harvest1" | ||
|
|
||
| capped, added2 = hv.grow_corpus(corpus, promote, max_size=1) # already 1 case -> cap hit | ||
| assert added2 == [] and capped is corpus | ||
|
|
||
|
|
||
| def test_grow_corpus_respects_per_category_caps(): | ||
| corpus = {"cases": []} | ||
| promote = [ | ||
| hv.to_case( | ||
| _rec(n, days_ago=40), {"expected_verdict": "PASS", "category": "clean-pass"}, now=NOW | ||
| ) | ||
| for n in range(10) | ||
| ] | ||
| grown, added = hv.grow_corpus(corpus, promote, max_size=150, category_caps={"clean-pass": 3}) | ||
| assert len(added) == 3 # capped at 3 clean-pass despite 10 offered | ||
| assert len(grown["cases"]) == 3 | ||
|
|
||
|
|
||
| def test_grow_corpus_noop_returns_original(): | ||
| corpus = {"cases": [{"repo": "stranske/Demo", "pr": 1}]} | ||
| grown, added = hv.grow_corpus(corpus, [], max_size=150) | ||
| assert added == [] and grown is corpus | ||
|
|
||
|
|
||
| def test_staging_auto_expires_old_cases(): | ||
| old = {"repo": "stranske/Demo", "pr": 100, "harvested_at": "2026-01-01"} # >60d ago | ||
| fresh = {"repo": "stranske/Demo", "pr": 101, "harvested_at": NOW.date().isoformat()} | ||
| out = hv.prune_staging({"cases": [old]}, [fresh], now=NOW, expiry_days=60) | ||
| prs = {c["pr"] for c in out["cases"]} | ||
| assert prs == {101} # old one expired out, fresh retained | ||
|
|
||
|
|
||
| def test_main_dry_run_and_write(tmp_path, capsys): | ||
| policy = { | ||
| "profiles": { | ||
| "verifier-balanced": { | ||
| "corpus_growth": { | ||
| "enabled": True, | ||
| "stability_days": 30, | ||
| "staging_expiry_days": 60, | ||
| "max_corpus_size": 150, | ||
| "source_repos": [], | ||
| } | ||
| } | ||
| } | ||
| } | ||
| corpus = {"corpus_version": "v1", "cases": []} | ||
| records = [_rec(1, days_ago=40), _rec(2, days_ago=2)] | ||
| pol_p = tmp_path / "policy.json" | ||
| cor_p = tmp_path / "corpus.json" | ||
| stg_p = tmp_path / "staging.json" | ||
| rec_p = tmp_path / "records.json" | ||
| pol_p.write_text(json.dumps(policy)) | ||
| cor_p.write_text(json.dumps(corpus)) | ||
| rec_p.write_text(json.dumps(records)) | ||
|
|
||
| argv = [ | ||
| "--policy", | ||
| str(pol_p), | ||
| "--corpus", | ||
| str(cor_p), | ||
| "--staging", | ||
| str(stg_p), | ||
| "--from-json", | ||
| str(rec_p), | ||
| "--write", | ||
| ] | ||
| assert hv.main(argv) == 0 | ||
| grown = json.loads(cor_p.read_text()) | ||
| assert [c["pr"] for c in grown["cases"]] == [1] # only the stable merge promoted | ||
| staged = json.loads(stg_p.read_text()) | ||
| assert [c["pr"] for c in staged["cases"]] == [2] # recent merge staged | ||
|
Comment on lines
+141
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Test the default dry-run path. This test always passes As per path instructions, “Flag new or changed behavior with no accompanying test.” 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
|
|
||
| def test_main_respects_disabled_flag(tmp_path): | ||
| pol_p = tmp_path / "policy.json" | ||
| pol_p.write_text( | ||
| json.dumps({"profiles": {"verifier-balanced": {"corpus_growth": {"enabled": False}}}}) | ||
| ) | ||
| assert hv.main(["--policy", str(pol_p), "--from-json", str(tmp_path / "none.json")]) == 0 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new workflow is absent from
docs/ci/WORKFLOWS.md,docs/ci/WORKFLOW_SYSTEM.md, andEXPECTED_NAMESintests/workflows/test_workflow_naming.py. The naming test asserts that every workflow has a display-name mapping, and the inventory test requires every workflow in both documents, so the repository's workflow validation will fail;scripts/check_docs_drift.pyalso reports this file as undocumented. Register the workflow in all three required surfaces.AGENTS.md reference: AGENTS.md:L81-L81
Useful? React with 👍 / 👎.