Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions config/coverage-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"line": 80.0,
"warn_drop": 1.0,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"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."
}
15 changes: 15 additions & 0 deletions src/my_project.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/my_project.egg-info/SOURCES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
92 changes: 92 additions & 0 deletions tests/test_coverage_baseline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""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 math
import re
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
BASELINE_PATH = REPO_ROOT / "config" / "coverage-baseline.json"
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"


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 _coverage_min(path: Path) -> float:
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(2))


def test_baseline_matches_ci_gate_and_pyproject() -> None:
baseline = _load_baseline()
ci_min = _coverage_min(CI_WORKFLOW_PATH)
gate_min = _coverage_min(PR_GATE_WORKFLOW_PATH)

pyproject_text = PYPROJECT_PATH.read_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))

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()
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 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
Comment thread
stranske marked this conversation as resolved.
14 changes: 13 additions & 1 deletion tools/coverage_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", {})
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading