From 448724e2d86ecf3bdf08aa38d4914d11fe4d8a48 Mon Sep 17 00:00:00 2001 From: Jin Pan Date: Wed, 22 Jul 2026 23:05:39 +0000 Subject: [PATCH 1/3] fix(evolution): account for every generated file --- scripts/evolve/refresh.py | 22 +++++++++++++++++++++- tests/test_evolution.py | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/scripts/evolve/refresh.py b/scripts/evolve/refresh.py index 2d0a5147..d125d0b1 100644 --- a/scripts/evolve/refresh.py +++ b/scripts/evolve/refresh.py @@ -67,7 +67,10 @@ def _run(command: list[str], root: Path) -> str: def _git_changes(root: Path) -> tuple[list[str], int]: - output = _run(["git", "status", "--porcelain=v1"], root) + output = _run( + ["git", "status", "--porcelain=v1", "--untracked-files=all"], + root, + ) files = [] for line in output.splitlines(): if not line: @@ -203,6 +206,23 @@ def run_refresh(args: argparse.Namespace, root: Path = WIKI_ROOT) -> dict[str, A } if not args.dry_run: _write_summary(root, run_date, summary) + # Include the generated summary itself and expand every untracked file + # before enforcing/reported budgets. A second measurement is needed + # because adding the summary path grows the summary by one list entry. + changed_files, changed_lines = _git_changes(root) + summary["changed_files"] = changed_files + summary["changed_lines"] = changed_lines + _write_summary(root, run_date, summary) + changed_files, changed_lines = _git_changes(root) + summary["changed_files"] = changed_files + summary["changed_lines"] = changed_lines + _write_summary(root, run_date, summary) + enforce_change_budget( + changed_files, + changed_lines=changed_lines, + max_files=args.max_files, + max_lines=args.max_lines, + ) _run([sys.executable, "scripts/validate.py"], root) _run([sys.executable, "tests/test_evolution.py"], root) return summary diff --git a/tests/test_evolution.py b/tests/test_evolution.py index d4524f96..9248355e 100644 --- a/tests/test_evolution.py +++ b/tests/test_evolution.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import subprocess import sys import tempfile from pathlib import Path @@ -387,6 +388,24 @@ def test_refresh_budget_rejects_unreviewable_change_sets(): raise AssertionError("oversized refresh was accepted") +def test_refresh_expands_untracked_directories_for_budgeting(): + from evolve.refresh import _git_changes + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "init", "-q", str(root)], check=True) + nested = root / "candidates" / "runs" / "today" + nested.mkdir(parents=True) + (nested / "one.yaml").write_text("one: true\n", encoding="utf-8") + (nested / "two.yaml").write_text("two: true\n", encoding="utf-8") + changed_files, changed_lines = _git_changes(root) + assert changed_files == [ + "candidates/runs/today/one.yaml", + "candidates/runs/today/two.yaml", + ] + assert changed_lines == 2 + + def test_query_marks_upstream_pr_snippets_as_untrusted(): import query From a8c6225b73654097e9b4bee052e7f6403b58b5af Mon Sep 17 00:00:00 2001 From: Jin Pan Date: Wed, 22 Jul 2026 23:17:25 +0000 Subject: [PATCH 2/3] fix(evolution): enforce converged summary budgets --- scripts/evolve/refresh.py | 54 ++++++++++++++++++++++++++++----------- tests/test_evolution.py | 44 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 15 deletions(-) diff --git a/scripts/evolve/refresh.py b/scripts/evolve/refresh.py index d125d0b1..4f44260a 100644 --- a/scripts/evolve/refresh.py +++ b/scripts/evolve/refresh.py @@ -109,6 +109,41 @@ def _write_summary(root: Path, run_date: str, summary: dict[str, Any]) -> Path: return path +def _finalize_summary( + root: Path, + run_date: str, + summary: dict[str, Any], + *, + max_files: int, + max_lines: int, + max_passes: int = 5, +) -> dict[str, Any]: + """Write and measure until summary metadata matches the on-disk diff. + + The summary includes its own path and line contribution, so one measurement + cannot be final. A bounded fixed-point loop makes the invariant explicit: + budgets are enforced only when the values stored in the summary equal a + fresh measurement of the exact state that contains that summary. + """ + for _ in range(max_passes): + _write_summary(root, run_date, summary) + changed_files, changed_lines = _git_changes(root) + if ( + summary.get("changed_files") == changed_files + and summary.get("changed_lines") == changed_lines + ): + enforce_change_budget( + changed_files, + changed_lines=changed_lines, + max_files=max_files, + max_lines=max_lines, + ) + return summary + summary["changed_files"] = changed_files + summary["changed_lines"] = changed_lines + raise RuntimeError("refresh summary accounting did not converge") + + def run_refresh(args: argparse.Namespace, root: Path = WIKI_ROOT) -> dict[str, Any]: run_date = args.captured_at or date.today().isoformat() initial_files, _ = _git_changes(root) @@ -205,21 +240,10 @@ def run_refresh(args: argparse.Namespace, root: Path = WIKI_ROOT) -> dict[str, A "dry_run": bool(args.dry_run), } if not args.dry_run: - _write_summary(root, run_date, summary) - # Include the generated summary itself and expand every untracked file - # before enforcing/reported budgets. A second measurement is needed - # because adding the summary path grows the summary by one list entry. - changed_files, changed_lines = _git_changes(root) - summary["changed_files"] = changed_files - summary["changed_lines"] = changed_lines - _write_summary(root, run_date, summary) - changed_files, changed_lines = _git_changes(root) - summary["changed_files"] = changed_files - summary["changed_lines"] = changed_lines - _write_summary(root, run_date, summary) - enforce_change_budget( - changed_files, - changed_lines=changed_lines, + summary = _finalize_summary( + root, + run_date, + summary, max_files=args.max_files, max_lines=args.max_lines, ) diff --git a/tests/test_evolution.py b/tests/test_evolution.py index 9248355e..6b82ed9e 100644 --- a/tests/test_evolution.py +++ b/tests/test_evolution.py @@ -406,6 +406,50 @@ def test_refresh_expands_untracked_directories_for_budgeting(): assert changed_lines == 2 +def test_final_summary_is_inside_the_enforced_budget(): + from evolve.refresh import _finalize_summary, _git_changes + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "init", "-q", str(root)], check=True) + summary = { + "schema_version": 1, + "run_date": "2026-07-22", + "discovery": {"total": 0}, + "gap_proposals": 0, + "machine_changes": 0, + "changed_files": [], + "changed_lines": 0, + "dry_run": False, + } + try: + _finalize_summary( + root, + "2026-07-22", + summary, + max_files=0, + max_lines=100, + ) + except ValueError as error: + assert "file budget" in str(error) + else: + raise AssertionError("summary file escaped the final file budget") + + finalized = _finalize_summary( + root, + "2026-07-22", + summary, + max_files=1, + max_lines=100, + ) + changed_files, changed_lines = _git_changes(root) + assert finalized["changed_files"] == changed_files + assert finalized["changed_lines"] == changed_lines + assert changed_files == [ + "candidates/runs/2026-07-22/refresh-summary.yaml" + ] + + def test_query_marks_upstream_pr_snippets_as_untrusted(): import query From ae5485ee5522dc005963164e484cecc4fbfc3f11 Mon Sep 17 00:00:00 2001 From: Jin Pan Date: Wed, 22 Jul 2026 23:21:33 +0000 Subject: [PATCH 3/3] fix(evolution): allow corpus inventory to grow --- tests/test_evolution.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_evolution.py b/tests/test_evolution.py index 6b82ed9e..e0b2f7f5 100644 --- a/tests/test_evolution.py +++ b/tests/test_evolution.py @@ -260,9 +260,10 @@ def test_corpus_manifest_is_generated_from_the_checkout(): (ROOT / "data" / "corpus-manifest.yaml").read_text(encoding="utf-8") ) assert committed == expected - assert expected["counts"]["source_prs"] == 7454 - assert expected["counts"]["active_wiki_pages"] == 54 - assert expected["counts"]["artifact_bundles"] == 959 + counts = expected["counts"] + assert counts["source_prs"] > 0 + assert 0 < counts["active_wiki_pages"] <= counts["wiki_pages"] + assert counts["artifact_bundles"] <= counts["source_prs"] def test_candidate_schema_and_gap_detection():