From c87c01a9a4266a4369ce8df12c16a3b12c5bd381 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 25 Aug 2026 07:44:07 -0500 Subject: [PATCH 1/8] ci(coverage): give Ready the baseline file so it exercises the comparison path Ready's purpose is to be a fully functional repo that works with every Workflows capability while having no application use of its own. It already runs the coverage soft gate (enable-soft-gate: true, coverage-min: 80) -- but with no config/coverage-baseline.json it never exercised the BASELINE half of that pipeline: tools/coverage_trend.py reported baseline_status=absent, computed no delta, and Maint Coverage Guard had nothing to compare. So the one repo whose job is to surface fleet CI defects was silently skipping the check. That is not hypothetical -- the guard produced exactly ONE breach issue across all thirteen repos in ten months, and Ready is precisely where that silence should have been visible. Keyed `line` on purpose: coverage_trend.py and coverage_guard.py both accept `line` or `coverage` with `line` taking precedence, and exercising the precedence path is what a conformance repo is for. Set to 80 to match this repo's coverage-min and pyproject fail_under. Measured coverage is 100% of the src/my_project scaffold -- the only code Ready owns, since everything in scripts/ and tools/ is synced fleet tooling tested upstream. Co-Authored-By: Claude Opus 5 --- config/coverage-baseline.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 config/coverage-baseline.json diff --git a/config/coverage-baseline.json b/config/coverage-baseline.json new file mode 100644 index 00000000..a9ca89a8 --- /dev/null +++ b/config/coverage-baseline.json @@ -0,0 +1,7 @@ +{ + "line": 80.0, + "warn_drop": 1.0, + "recovery_days": 3, + "updated": "2026-08-25", + "notes": "Ready exists to exercise every Workflows capability end-to-end, so it must exercise the coverage BASELINE path too, not just coverage measurement. Without this file tools/coverage_trend.py reports baseline_status=absent and computes no delta, and Maint Coverage Guard has nothing to compare against -- meaning the one repo whose job is to catch fleet CI defects was silently skipping the comparison. That mattered: the guard produced exactly one breach issue fleet-wide in ten months, and this is where that should have surfaced first. Keyed `line` deliberately: both tools/coverage_trend.py and tools/coverage_guard.py accept `line` or `coverage`, with `line` taking precedence, and exercising the precedence path is the point of a conformance repo. Set to 80 to match this repo's coverage-min and pyproject fail_under; measured coverage is 100% of the src/my_project scaffold, which is the only code Ready owns -- everything in scripts/ and tools/ is synced fleet tooling tested upstream in stranske/Workflows." +} From 615552cab24cbb9a027063ad0c21dedfdc26cf03 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 25 Aug 2026 20:34:21 +0000 Subject: [PATCH 2/8] docs(coverage): document coverage baseline fields and update procedure Ready's config/coverage-baseline.json already set line: 80.0 with warn_drop and recovery_days, matching coverage-min and pyproject fail_under -- but nothing explained what those fields do or how to update the baseline when coverage intentionally changes. README.md is repo-specific (excluded from Workflows sync) and is where this belongs. Co-Authored-By: Claude Sonnet 5 --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index b19680c4..ae22b746 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,21 @@ This repository uses reusable workflows from [stranske/Workflows](https://github **Note:** `agents-orchestrator.yml` is legacy and can be removed. The current architecture uses `agents-keepalive-loop.yml` which integrates with the Gate workflow for event-driven triggering. +### Coverage Baseline + +`pr-00-gate.yml` runs the coverage soft gate (`coverage-min: "80"`, `enable-soft-gate: true`), which compares each run's coverage against `config/coverage-baseline.json`: + +| Field | Meaning | +|-------|---------| +| `line` | Baseline coverage percentage. `tools/coverage_trend.py` and `tools/coverage_guard.py` both accept `line` or `coverage`, with `line` taking precedence — use `line`. | +| `warn_drop` | Coverage-point drop from baseline that triggers a warning before a breach issue is opened. | +| `recovery_days` | Consecutive days of passing coverage required before a breach issue is closed automatically. | +| `updated` | Date `line` was last changed. | + +This file is excluded from Workflows sync — each repo owns its own baseline. Measured coverage covers `src/my_project` only (`[tool.coverage.run] source = ["src"]` in `pyproject.toml`); everything in `scripts/` and `tools/` is fleet tooling tested upstream in `stranske/Workflows`, not by this repo's own suite. + +**Updating the baseline:** when coverage intentionally changes, set `line` to the new measured percentage and `updated` to the date of the change. Otherwise the next run compares against a stale number and reports a drop (or improvement) that was already accepted. + ## Agent Automation This template uses the **Gate-triggered keepalive** architecture: From 983dd54ba04640e1a4aa4b41e48bc1d7d5780730 Mon Sep 17 00:00:00 2001 From: stranske Date: Tue, 25 Aug 2026 20:38:58 +0000 Subject: [PATCH 3/8] test(coverage): guard coverage-baseline.json against regressing its own design config/coverage-baseline.json's `line` key, 80.0 value, and warn_drop/recovery_days fields were set by hand with no test enforcing them -- a future edit could silently key `coverage` again (losing the precedence-path exercise) or let `line` drift from pr-00-gate.yml's coverage-min or pyproject's fail_under without anything catching it. Adds tests/test_coverage_baseline.py to assert all three agree and stay keyed `line`. Co-Authored-By: Claude Sonnet 5 --- tests/test_coverage_baseline.py | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_coverage_baseline.py diff --git a/tests/test_coverage_baseline.py b/tests/test_coverage_baseline.py new file mode 100644 index 00000000..c46ded20 --- /dev/null +++ b/tests/test_coverage_baseline.py @@ -0,0 +1,74 @@ +"""Guard config/coverage-baseline.json against silently regressing the choices this repo made. + +Ready's whole purpose is to exercise the fleet coverage pipeline end-to-end, so the baseline +file's fields aren't cosmetic: `line` (not `coverage`) exercises the precedence path both +tools/coverage_trend.py and tools/coverage_guard.py implement, and its value must agree with +coverage-min in pr-00-gate.yml and fail_under in pyproject.toml or the three drift apart. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +BASELINE_PATH = REPO_ROOT / "config" / "coverage-baseline.json" +GATE_WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "pr-00-gate.yml" +PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" + + +def _load_baseline() -> dict: + return json.loads(BASELINE_PATH.read_text()) + + +def test_baseline_file_exists() -> None: + assert BASELINE_PATH.is_file(), ( + "config/coverage-baseline.json must exist so Maint Coverage Guard has something to " + "compare against instead of reporting baseline_status=absent." + ) + + +def test_baseline_keys_line_not_coverage() -> None: + baseline = _load_baseline() + assert "line" in baseline, ( + "Baseline must key coverage as `line`: coverage_trend.py and coverage_guard.py both " + "accept `line` or `coverage`, with `line` taking precedence, and exercising that " + "precedence path is the point of a conformance repo." + ) + assert "coverage" not in baseline, ( + "Do not also set `coverage` -- that reintroduces the ambiguity `line` precedence is " + "meant to resolve." + ) + + +def test_baseline_matches_gate_and_pyproject() -> None: + baseline = _load_baseline() + gate_text = GATE_WORKFLOW_PATH.read_text() + gate_match = re.search(r'coverage-min:\s*"(\d+)"', gate_text) + assert gate_match, "pr-00-gate.yml must set a numeric coverage-min." + gate_min = float(gate_match.group(1)) + + pyproject_text = PYPROJECT_PATH.read_text() + fail_under_match = re.search(r"fail_under\s*=\s*(\d+)", pyproject_text) + assert fail_under_match, "pyproject.toml [tool.coverage.report] must set fail_under." + fail_under = float(fail_under_match.group(1)) + + assert baseline["line"] == gate_min == fail_under == 80.0, ( + "config/coverage-baseline.json `line`, pr-00-gate.yml `coverage-min`, and " + "pyproject.toml `fail_under` must all agree (80) so the three don't drift apart: " + f"line={baseline['line']!r}, coverage-min={gate_min!r}, fail_under={fail_under!r}" + ) + + +def test_baseline_has_warn_drop_and_recovery_days() -> None: + baseline = _load_baseline() + assert isinstance(baseline.get("warn_drop"), (int, float)), ( + "warn_drop must be numeric -- it's the coverage-point drop that triggers a warning " + "before a breach issue is opened." + ) + assert isinstance(baseline.get("recovery_days"), int), ( + "recovery_days must be an int -- consecutive passing days required before a breach " + "issue auto-closes." + ) + assert baseline["recovery_days"] > 0 From bfa7587962a43938582e3b7f0371cee182f1cfaf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:39:30 +0000 Subject: [PATCH 4/8] Claude (keepalive) automated update for PR #542 --- src/my_project.egg-info/PKG-INFO | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/my_project.egg-info/PKG-INFO b/src/my_project.egg-info/PKG-INFO index 82bcabe4..7b619765 100644 --- a/src/my_project.egg-info/PKG-INFO +++ b/src/my_project.egg-info/PKG-INFO @@ -100,6 +100,21 @@ This repository uses reusable workflows from [stranske/Workflows](https://github **Note:** `agents-orchestrator.yml` is legacy and can be removed. The current architecture uses `agents-keepalive-loop.yml` which integrates with the Gate workflow for event-driven triggering. +### Coverage Baseline + +`pr-00-gate.yml` runs the coverage soft gate (`coverage-min: "80"`, `enable-soft-gate: true`), which compares each run's coverage against `config/coverage-baseline.json`: + +| Field | Meaning | +|-------|---------| +| `line` | Baseline coverage percentage. `tools/coverage_trend.py` and `tools/coverage_guard.py` both accept `line` or `coverage`, with `line` taking precedence — use `line`. | +| `warn_drop` | Coverage-point drop from baseline that triggers a warning before a breach issue is opened. | +| `recovery_days` | Consecutive days of passing coverage required before a breach issue is closed automatically. | +| `updated` | Date `line` was last changed. | + +This file is excluded from Workflows sync — each repo owns its own baseline. Measured coverage covers `src/my_project` only (`[tool.coverage.run] source = ["src"]` in `pyproject.toml`); everything in `scripts/` and `tools/` is fleet tooling tested upstream in `stranske/Workflows`, not by this repo's own suite. + +**Updating the baseline:** when coverage intentionally changes, set `line` to the new measured percentage and `updated` to the date of the change. Otherwise the next run compares against a stale number and reports a drop (or improvement) that was already accepted. + ## Agent Automation This template uses the **Gate-triggered keepalive** architecture: From 389ec88b73fd4d9deceeb8cd2528c54d4ed8a6d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:45:31 +0000 Subject: [PATCH 5/8] Claude (keepalive) automated update for PR #542 --- src/my_project.egg-info/SOURCES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/my_project.egg-info/SOURCES.txt b/src/my_project.egg-info/SOURCES.txt index bfd7f470..a1c9ec51 100644 --- a/src/my_project.egg-info/SOURCES.txt +++ b/src/my_project.egg-info/SOURCES.txt @@ -7,6 +7,7 @@ src/my_project.egg-info/SOURCES.txt src/my_project.egg-info/dependency_links.txt src/my_project.egg-info/requires.txt src/my_project.egg-info/top_level.txt +tests/test_coverage_baseline.py tests/test_dependency_version_alignment.py tests/test_main.py tests/test_repo_hygiene.py \ No newline at end of file From 79aadb72184cd2d78aa30960da7ea8740120ee7a Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Tue, 25 Aug 2026 16:28:25 -0500 Subject: [PATCH 6/8] test(coverage): validate active threshold configuration --- tests/test_coverage_baseline.py | 38 ++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/tests/test_coverage_baseline.py b/tests/test_coverage_baseline.py index c46ded20..140f9fe4 100644 --- a/tests/test_coverage_baseline.py +++ b/tests/test_coverage_baseline.py @@ -9,12 +9,14 @@ from __future__ import annotations import json +import math import re from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent BASELINE_PATH = REPO_ROOT / "config" / "coverage-baseline.json" -GATE_WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "pr-00-gate.yml" +CI_WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "ci.yml" +PR_GATE_WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "pr-00-gate.yml" PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" @@ -42,33 +44,39 @@ def test_baseline_keys_line_not_coverage() -> None: ) -def test_baseline_matches_gate_and_pyproject() -> None: +def _coverage_min(path: Path) -> float: + match = re.search(r"coverage-min:\s*[\"']?(\d+(?:\.\d+)?)[\"']?", path.read_text()) + assert match, f"{path.name} must set a numeric coverage-min." + return float(match.group(1)) + + +def test_baseline_matches_ci_gate_and_pyproject() -> None: baseline = _load_baseline() - gate_text = GATE_WORKFLOW_PATH.read_text() - gate_match = re.search(r'coverage-min:\s*"(\d+)"', gate_text) - assert gate_match, "pr-00-gate.yml must set a numeric coverage-min." - gate_min = float(gate_match.group(1)) + ci_min = _coverage_min(CI_WORKFLOW_PATH) + gate_min = _coverage_min(PR_GATE_WORKFLOW_PATH) pyproject_text = PYPROJECT_PATH.read_text() fail_under_match = re.search(r"fail_under\s*=\s*(\d+)", pyproject_text) assert fail_under_match, "pyproject.toml [tool.coverage.report] must set fail_under." fail_under = float(fail_under_match.group(1)) - assert baseline["line"] == gate_min == fail_under == 80.0, ( - "config/coverage-baseline.json `line`, pr-00-gate.yml `coverage-min`, and " - "pyproject.toml `fail_under` must all agree (80) so the three don't drift apart: " - f"line={baseline['line']!r}, coverage-min={gate_min!r}, fail_under={fail_under!r}" + assert baseline["line"] == ci_min == gate_min == fail_under == 80.0, ( + "config/coverage-baseline.json `line`, ci.yml and pr-00-gate.yml `coverage-min`, " + "and pyproject.toml `fail_under` must all agree (80) so the four don't drift apart: " + f"line={baseline['line']!r}, ci={ci_min!r}, gate={gate_min!r}, " + f"fail_under={fail_under!r}" ) def test_baseline_has_warn_drop_and_recovery_days() -> None: baseline = _load_baseline() - assert isinstance(baseline.get("warn_drop"), (int, float)), ( - "warn_drop must be numeric -- it's the coverage-point drop that triggers a warning " - "before a breach issue is opened." + warn_drop = baseline.get("warn_drop") + assert isinstance(warn_drop, (int, float)) and not isinstance(warn_drop, bool) and math.isfinite(warn_drop), ( + "warn_drop must be a finite numeric value excluding booleans -- it's the coverage-point " + "drop that triggers a warning before a breach issue is opened." ) - assert isinstance(baseline.get("recovery_days"), int), ( - "recovery_days must be an int -- consecutive passing days required before a breach " + assert type(baseline.get("recovery_days")) is int, ( + "recovery_days must be an exact int excluding booleans -- consecutive passing days required before a breach " "issue auto-closes." ) assert baseline["recovery_days"] > 0 From 033e86eab320cac5a21330547cd4cbf985b4dab2 Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Tue, 25 Aug 2026 21:27:43 -0500 Subject: [PATCH 7/8] test(coverage): parse active configuration thresholds --- tests/test_coverage_baseline.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_coverage_baseline.py b/tests/test_coverage_baseline.py index 140f9fe4..728313f3 100644 --- a/tests/test_coverage_baseline.py +++ b/tests/test_coverage_baseline.py @@ -45,9 +45,12 @@ def test_baseline_keys_line_not_coverage() -> None: def _coverage_min(path: Path) -> float: - match = re.search(r"coverage-min:\s*[\"']?(\d+(?:\.\d+)?)[\"']?", path.read_text()) + match = re.search( + r"(?m)^[ \t]*coverage-min[ \t]*:[ \t]*([\"']?)(\d+(?:\.\d+)?)\1[ \t]*(?:#.*)?$", + path.read_text(), + ) assert match, f"{path.name} must set a numeric coverage-min." - return float(match.group(1)) + return float(match.group(2)) def test_baseline_matches_ci_gate_and_pyproject() -> None: @@ -56,7 +59,14 @@ def test_baseline_matches_ci_gate_and_pyproject() -> None: gate_min = _coverage_min(PR_GATE_WORKFLOW_PATH) pyproject_text = PYPROJECT_PATH.read_text() - fail_under_match = re.search(r"fail_under\s*=\s*(\d+)", pyproject_text) + coverage_report = re.search( + r"(?ms)^\[tool\.coverage\.report\][ \t]*\n(.*?)(?=^\[|\Z)", pyproject_text + ) + assert coverage_report, "pyproject.toml must define [tool.coverage.report]." + fail_under_match = re.search( + r"(?m)^[ \t]*fail_under[ \t]*=[ \t]*(\d+(?:\.\d+)?)[ \t]*(?:#.*)?$", + coverage_report.group(1), + ) assert fail_under_match, "pyproject.toml [tool.coverage.report] must set fail_under." fail_under = float(fail_under_match.group(1)) From 3c6152f034fbbf1e920f910abb15b101b9b0a197 Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Tue, 25 Aug 2026 21:45:21 -0500 Subject: [PATCH 8/8] chore(sync): apply Workflows warn_drop breach threshold in coverage_guard Pull tools/coverage_guard.py from Workflows main (post #3262 merge) so Maint coverage guard honors configured warn_drop when opening breach issues. Addresses CodeRabbit review thread on PR #542. --- tools/coverage_guard.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/coverage_guard.py b/tools/coverage_guard.py index e5dbf4a2..04359bea 100755 --- a/tools/coverage_guard.py +++ b/tools/coverage_guard.py @@ -148,6 +148,11 @@ def load_baseline(path: Path) -> BaselineConfig: ) +def is_coverage_breach(current: float, baseline: float, warn_drop: float) -> bool: + """Return whether coverage exceeds the configured drop allowance.""" + return current < baseline - warn_drop + + def compute_top_files(coverage_data: dict[str, Any], limit: int = 15) -> list[FileCoverage]: """Return the most useful file-level coverage rows for issue comments.""" files = coverage_data.get("files", {}) @@ -680,6 +685,7 @@ def main(args: list[str] | None = None) -> int: ) return 0 delta = current - baseline + warn_drop = load_baseline(parsed.baseline_path).warn_drop configured_recovery_window = max( 1, _to_int( @@ -714,7 +720,7 @@ def main(args: list[str] | None = None) -> int: return 0 # Create or update issue - if current < baseline: + if is_coverage_breach(current, baseline, warn_drop): try: _find_or_create_issue( repo=parsed.repo, @@ -726,6 +732,12 @@ def main(args: list[str] | None = None) -> int: print(f"Failed to create or update coverage issue: {exc}", file=sys.stderr) return 1 else: + if current < baseline: + print( + f"Coverage {current:.2f}% is within the configured {warn_drop:.2f}-point " + f"drop allowance below baseline {baseline:.2f}% - no new issue needed" + ) + return 0 print(f"Coverage {current:.2f}% meets baseline {baseline:.2f}% - no open issue needed") if not _recovery_window_satisfied( trend_data,