From 8fc4ecbe526a08a055d57e73a4c11fd037bfe340 Mon Sep 17 00:00:00 2001 From: kgrizz-git <216068303+kgrizz-git@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:19:56 -0400 Subject: [PATCH 1/4] refactor(quality): add coverage, complexity, and module-size gates Add test-coverage, cyclomatic-complexity, and module-size enforcement in hooks and CI at thresholds just below current baselines. - scripts/coverage-check: scoped to src/github_usage; overall must stay >= 75% (current 76%); per-file baseline lock via coverage-baselines.json (grandfather existing low-coverage files, block them from dropping, require new files >= 82%); COVERAGE_TOLERANCE=1.0pp absorbs flaky runs. - scripts/coverage-baselines: regenerates the baseline lock file. - scripts/check-complexity: worst block CC <= 28 (current max); module size warns >= 575, blocks > 650 (current largest 562). - Wire both into scripts/check, pre-push hooks, and a new coverage CI job. - Add coverage/radon to dev extra; regenerate uv.lock. - Archive plan; drop completed TO_DO items; ignore coverage.json. --- .github/workflows/ci.yml | 13 ++ .gitignore | 1 + .pre-commit-config.yaml | 14 ++ CHANGELOG.md | 1 + TO_DO.md | 2 - coverage-baselines.json | 91 +++++++++ .../2026-08-07-coverage-complexity-gates.md | 49 +++++ pyproject.toml | 2 + scripts/check | 6 + scripts/check-complexity | 58 ++++++ scripts/coverage-baselines | 31 +++ scripts/coverage-check | 78 +++++++ uv.lock | 193 ++++++++++++++++++ 13 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 coverage-baselines.json create mode 100644 docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md create mode 100755 scripts/check-complexity create mode 100755 scripts/coverage-baselines create mode 100755 scripts/coverage-check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aa2135..b991834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,19 @@ jobs: - name: Run checks run: scripts/check + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.11" + cache: pip + - name: Install package + run: scripts/python -m pip install -e '.[dev,gui,test-xlsx,test-pdf]' + - name: Run coverage gate + run: scripts/coverage-check + typecheck: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 19996c9..d1cccda 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ .mypy_cache/ .basedpyright/ .coverage +coverage.json coverage.xml htmlcov/ build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adf4c0c..733957e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,3 +50,17 @@ repos: stages: [pre-push] always_run: true pass_filenames: false + - id: coverage-check + name: Coverage check (fail-under 82%) + entry: scripts/coverage-check + language: system + stages: [pre-push] + always_run: true + pass_filenames: false + - id: check-complexity + name: Complexity and module size guard + entry: scripts/check-complexity + language: system + stages: [pre-push] + always_run: true + pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 4074d36..db6cc2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project follows the structure from Keep a Changelog and intends to use Sema ### Added +- **Coverage, complexity, and module-size gates:** `scripts/coverage-check` runs the unittest suite under `coverage` (scoped to `src/github_usage`). Overall src coverage must stay ≥ `COVERAGE_TOTAL_MIN` (default 75; current ~76%). Each file is locked to its recorded baseline in `coverage-baselines.json` ("don't get worse" — a file may not drop below its baseline; existing files below the 82% target are grandfathered), while any **new** `src` file must meet `COVERAGE_FILE_MIN` (default 82%). A `COVERAGE_TOLERANCE` (default 1.0 percentage point) absorbs non-deterministic (flaky) test runs so only real drops fail. `scripts/coverage-baselines` regenerates the baseline file. `scripts/check-complexity` fails if the worst cyclomatic complexity exceeds `MAX_CC` (default 28; current worst) and warns at `WARN_LINES`=575 / blocks at `BLOCK_LINES`=650 for module size (current largest 562). Both scripts are wired into `scripts/check`, added as pre-push hooks in `.pre-commit-config.yaml`, and coverage runs as its own `coverage` job in `ci.yml`. `coverage` and `radon` added to the `dev` extra; `uv.lock` regenerated. - **Private-repo top consumers** ([plan](docs/superpowers/plans/archived/2026-07-31-private-top-consumers.md)): local full report (terminal + TUI), email text/HTML, and JSON exports gain private-only top lists for Actions minutes and billed storage alongside existing overall rankings; overall top-by-storage (billed avg MB) and artifact-scan private lists where they add signal. Shared `repo_consumers.py` helper with `private_list_is_redundant` guards; report cache version bumped to 3. HTML table helpers extracted to `_email_report_html_tables.py`. - **Minutes-by-workflow for top private consumer** ([plan](docs/superpowers/plans/archived/2026-07-31-private-top-consumers.md)): estimated per-workflow Actions minutes from completed run wall-clock times for the top private-repo minutes consumer. New `report_workflow_minutes.py` with shared `runs_cache`; terminal and email sections include an approximation caveat (not billable — will not match billed repo totals). Quota estimates bump by +10 when the workflow path is enabled. - **Private-usage emphasis for Actions free-tier reporting** ([plan](docs/superpowers/plans/archived/2026-07-30-private-usage-emphasis.md)): Limits Summary, utilization bars, and forecasts measure **private-repo** Actions minutes/storage against the free tier; public standard-runner usage is shown separately as free. Artifact storage section frames the 500 MB private allowance as GB-hrs accrual, splits artifacts vs release assets, and surfaces expiry/retention. Larger-runner SKUs are flagged `*`. Legacy reports include a Sources footer. Email `repo_consumers` gains `by_visibility`; report cache version bumped to 2 (stale v1 snapshots rejected). CSV/XLSX/PDF/JSON exports and TUI summary rows carry the same private-vs-public framing, storage analysis, and Sources section. diff --git a/TO_DO.md b/TO_DO.md index 99157df..bd4e946 100644 --- a/TO_DO.md +++ b/TO_DO.md @@ -8,8 +8,6 @@ - absolute local paths (e.g. `/Users/`, `C:\`, `/tmp/`, `/var/`) - unredacted report output in **email bodies (plain-text + HTML)**, **text**, and **PDF** exports — email is the main gap today (`redact.py` covers file exports only, not email bodies; PDF output needs verification), plus generated report artifacts in any format (`.json`, `.txt`, `.pdf`, `.xlsx`, `.csv`) - [ ] Add a dependency/vulnerability CI step: run `pip-audit` (already a `[dev]` dep, currently only invoked implicitly via `scripts/security`) as an explicit always-failing job in `.github/workflows/security.yml`. -- [ ] Add a code-complexity pre-commit hook (e.g. `xenon`/`radon` or ruff `C901`/`mccabe`) with a project-appropriate threshold, and fail CI on violations (watch `cli_runs.py` at 534 lines and `setup_config.py` at 507). -- [ ] Add a coverage CI job: a new `scripts/coverage` (or extend `scripts/check`) using `coverage.py`, enforce a baseline threshold that ratchets up, and gate PRs on it. - [ ] Add tests for the above: content-check unit tests with absolute-path/report-artifact fixtures, redaction coverage for email/PDF output, and the dependency/complexity/coverage gates' behavior. - [ ] Add an inventoried `scripts/sonarqube` helper that runs a local SonarQube scan (Docker `sonarqube` container + `sonar-scanner`, token via env var, output under `tmp/` or `reports/`), documented in the README scripts section and `docs/repo-harness-guidance.md` alongside `scripts/security`. diff --git a/coverage-baselines.json b/coverage-baselines.json new file mode 100644 index 0000000..a3807b9 --- /dev/null +++ b/coverage-baselines.json @@ -0,0 +1,91 @@ +{ + "src/github_usage/__init__.py": 100.0, + "src/github_usage/__main__.py": 0.0, + "src/github_usage/_email_report_common.py": 100.0, + "src/github_usage/_email_report_html_tables.py": 98.4, + "src/github_usage/api.py": 71.8, + "src/github_usage/auth.py": 93.0, + "src/github_usage/billing.py": 95.0, + "src/github_usage/cli.py": 79.6, + "src/github_usage/cli_email_report.py": 86.1, + "src/github_usage/cli_gui.py": 87.3, + "src/github_usage/cli_parsers.py": 100.0, + "src/github_usage/cli_runs.py": 91.7, + "src/github_usage/cli_runs_diff.py": 77.9, + "src/github_usage/email_report.py": 100.0, + "src/github_usage/email_report_html.py": 88.1, + "src/github_usage/email_report_send.py": 100.0, + "src/github_usage/email_report_text.py": 93.6, + "src/github_usage/export_csv.py": 93.8, + "src/github_usage/export_json.py": 94.1, + "src/github_usage/export_pdf.py": 94.6, + "src/github_usage/export_report.py": 83.1, + "src/github_usage/export_text.py": 100.0, + "src/github_usage/export_visibility.py": 81.8, + "src/github_usage/export_xlsx.py": 97.2, + "src/github_usage/forecast.py": 100.0, + "src/github_usage/gui/__init__.py": 100.0, + "src/github_usage/gui/app.py": 84.9, + "src/github_usage/gui/async_ops.py": 39.5, + "src/github_usage/gui/errors.py": 80.0, + "src/github_usage/gui/layout.py": 100.0, + "src/github_usage/gui/log_utils.py": 71.4, + "src/github_usage/gui/main_window.py": 69.2, + "src/github_usage/gui/modals.py": 45.5, + "src/github_usage/gui/prefs.py": 95.3, + "src/github_usage/gui/scroll_actions.py": 82.4, + "src/github_usage/gui/state.py": 67.1, + "src/github_usage/gui/views/__init__.py": 100.0, + "src/github_usage/gui/views/email_report_view.py": 51.1, + "src/github_usage/gui/views/report_view.py": 39.9, + "src/github_usage/gui/views/runs_view.py": 56.1, + "src/github_usage/gui/views/schedules_view.py": 62.9, + "src/github_usage/gui/views/setup_profiles_panel.py": 66.9, + "src/github_usage/gui/views/setup_secrets_panel.py": 95.0, + "src/github_usage/gui/views/setup_verify_panel.py": 63.6, + "src/github_usage/gui/views/setup_view.py": 61.7, + "src/github_usage/gui/widgets/__init__.py": 100.0, + "src/github_usage/gui/widgets/schedule_picker.py": 75.3, + "src/github_usage/gui/wizard/__init__.py": 42.9, + "src/github_usage/gui/wizard/setup_wizard_flow.py": 79.6, + "src/github_usage/gui/wizard/setup_wizard_screen.py": 47.3, + "src/github_usage/gui/workers.py": 0.0, + "src/github_usage/gui_backend.py": 54.2, + "src/github_usage/http_retry.py": 98.6, + "src/github_usage/legacy.py": 93.3, + "src/github_usage/legacy_report.py": 63.9, + "src/github_usage/legacy_report_data.py": 77.7, + "src/github_usage/legacy_report_summary.py": 86.2, + "src/github_usage/legacy_terminal.py": 84.4, + "src/github_usage/redact.py": 97.6, + "src/github_usage/repo_consumers.py": 100.0, + "src/github_usage/report_account.py": 100.0, + "src/github_usage/report_actions.py": 78.3, + "src/github_usage/report_actions_limits.py": 95.6, + "src/github_usage/report_cache.py": 79.3, + "src/github_usage/report_data.py": 90.0, + "src/github_usage/report_forecast.py": 86.4, + "src/github_usage/report_forecast_data.py": 95.0, + "src/github_usage/report_helpers.py": 100.0, + "src/github_usage/report_optional.py": 100.0, + "src/github_usage/report_products.py": 51.3, + "src/github_usage/report_sources.py": 45.5, + "src/github_usage/report_storage.py": 43.0, + "src/github_usage/report_summary.py": 98.4, + "src/github_usage/report_summary_insights.py": 85.8, + "src/github_usage/report_summary_private.py": 95.3, + "src/github_usage/report_workflow_minutes.py": 95.6, + "src/github_usage/schedule_helpers.py": 78.5, + "src/github_usage/setup_ci.py": 15.4, + "src/github_usage/setup_config.py": 82.3, + "src/github_usage/setup_email_config.py": 60.0, + "src/github_usage/setup_launchd.py": 36.2, + "src/github_usage/setup_prompts.py": 38.7, + "src/github_usage/setup_secrets.py": 21.4, + "src/github_usage/setup_wizard.py": 51.4, + "src/github_usage/setup_workflow.py": 81.2, + "src/github_usage/storage.py": 93.3, + "src/github_usage/terminal.py": 100.0, + "src/github_usage/usage_split.py": 92.8, + "src/github_usage/visibility.py": 100.0 +} diff --git a/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md b/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md new file mode 100644 index 0000000..84f33f0 --- /dev/null +++ b/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md @@ -0,0 +1,49 @@ +# Plan: Coverage, Complexity & Module-Size Gates + +- **Date:** 2026-08-07 + +> **Status:** COMPLETE + +- **Branch:** `refactor/coverage-hooks-ci` +- **Goal:** Enforce test coverage, cyclomatic complexity, and module size in hooks and CI at thresholds set just below current measured baselines. + +## Baseline Measurements (2026-08-07) + +- **Coverage (scoped to `src/github_usage`):** overall **76%**. 39 of 89 `src` files below 82%; worst `setup_ci` (15%), `setup_secrets` (21%), `setup_launchd` (36%). (Earlier "86%" figure included `tests/` and `__init__` in the measured set; scoped measurement is the correct product-code number.) +- **Complexity:** 807 blocks; worst cyclomatic complexity = **28** (`setup_wizard._manage_profiles`, D-rank). 55 blocks at C-rank (11+) or worse, including three D-rank. No block exceeds 28. +- **Module size:** warn at 575, block at 650 (current largest 562). Changed from 600/700 to tighten the guard closer to the current ceiling. +- **Coverage tolerance:** `COVERAGE_TOLERANCE`=1.0pp absorbs flaky (non-deterministic) test runs (e.g. `schedules_view.py` oscillates 91↔92 missing lines across identical runs). Only drops beyond the tolerance are treated as regressions. Verified stable across repeated runs while a 23pt real drop still fails. + +## Tasks + +- [x] Add `coverage` and `radon` to the `dev` optional-dependencies in `pyproject.toml`. +- [x] Regenerate `uv.lock` (`uv lock`; `uv lock --check` passes). +- [x] Create `scripts/coverage-check`: scoped to `src/github_usage`; overall must stay ≥ `COVERAGE_TOTAL_MIN` (75). +- [x] Create `coverage-baselines.json` + `scripts/coverage-baselines` generator: each `src` file locked to its recorded percent ("don't get worse"); existing files below 82% are grandfathered at their current value; **new** `src` files must meet `COVERAGE_FILE_MIN` (82). +- [x] Create `scripts/check-complexity`: fails if worst block CC > `MAX_CC` (28, the current worst); module size warns at `WARN_LINES`=575 and blocks at `BLOCK_LINES`=650. +- [x] Wire both into `scripts/check` (after backup policy, before "check passed"). +- [x] Add `coverage-check` and `check-complexity` as pre-push local hooks in `.pre-commit-config.yaml` (mirrors `run-scripts-check`). +- [x] Add a `coverage` job to `.github/workflows/ci.yml` running `scripts/coverage-check`. +- [x] Confirm `scripts/check` passes end-to-end (overall 76% ≥ 75%; no per-file regression; worst CC 28 ≤ 28; max file 562 ≤ 700). +- [x] Validate regression lock: raising a baseline above actual coverage fails the gate; a new 0% file fails the gate. +- [x] Validate `.pre-commit-config.yaml` (`pre-commit validate-config` → rc 0). +- [x] Record in `CHANGELOG.md` `[Unreleased] > Added`. + +## Notes / Deviations + +- **Coverage measured scope:** coverage is measured with `--source=src/github_usage` so only product code is gated (test files excluded). Overall scoped coverage is **76%**, not the earlier 86% (which double-counted `tests/`). +- **Grandfathering without a hardcoded list:** instead of maintaining a separate "exempt files" list, every `src` file's current percentage is the floor (`coverage-baselines.json`). This automatically grandfathers the 39 files below 82% while still blocking any of them from *dropping*, and forces any brand-new file to hit 82%. +- **Lower threshold, same strictness:** `COVERAGE_TOTAL_MIN=75` is just below the 76% overall; the stricter 82% applies to new files. Complexity ceiling `MAX_CC=28` is the current worst, so it blocks only *new* blocks worse than today (no need to exempt the 55 existing C+ blocks). +- `radon --max-cc` was evaluated but rejected: it exits non-zero whenever any C+ block exists regardless of the numeric threshold. The script instead computes the true max from JSON and compares to `MAX_CC`. +- Earlier analysis incorrectly reported "zero C-or-worse blocks" due to grepping a truncated sorted tail; corrected with full-output measurement. + +## Verification + +``` +scripts/check # full harness, green +scripts/coverage-check # overall 76% >= 75%; no per-file regression; new files >= 82% +scripts/coverage-baselines # regenerate baseline file +scripts/check-complexity # worst CC 28 <= 28; warn >=600, block >700 +uv lock --check # consistent +pre-commit validate-config +``` diff --git a/pyproject.toml b/pyproject.toml index 5222154..b5c566e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,10 @@ dev = [ "bandit>=1.7.10", "basedpyright>=1.30", "black>=24.0", + "coverage>=7.0", "pip-audit>=2.7", "pre-commit>=3.7", + "radon>=6.0", "ruff>=0.6", ] # Export-format optional dependencies. PEP 621 does not support nested extras, diff --git a/scripts/check b/scripts/check index 090d0c3..2600f8f 100755 --- a/scripts/check +++ b/scripts/check @@ -32,4 +32,10 @@ scripts/typecheck echo "==> Backup policy" scripts/check-backups +echo "==> Coverage" +scripts/coverage-check + +echo "==> Complexity and module size" +scripts/check-complexity + echo "check passed" diff --git a/scripts/check-complexity b/scripts/check-complexity new file mode 100755 index 0000000..5f84198 --- /dev/null +++ b/scripts/check-complexity @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +# Complexity & module-size policy (thresholds set at current baselines): +# - Cyclomatic complexity: the worst existing block is 28 +# (setup_wizard._manage_profiles). Any block exceeding MAX_CC fails. +# (No grandfather list needed: 28 is already the current max, so this +# blocks any new block worse than what exists today.) +# - Module size: WARN at WARN_LINES (default 575) but only BLOCK at +# BLOCK_LINES (default 650). Current largest module is 562 lines. +# Run with: scripts/check-complexity + +MAX_CC="${MAX_CC:-28}" +WARN_LINES="${WARN_LINES:-575}" +BLOCK_LINES="${BLOCK_LINES:-650}" + +echo "==> Cyclomatic complexity (worst block must not exceed ${MAX_CC})" +cc_json="$(PYTHONPATH=src scripts/python -m radon cc src/github_usage --json 2>/dev/null)" +actual="$(printf '%s' "$cc_json" | scripts/python -c " +import json,sys +d=json.load(sys.stdin) +mx=max((b.get('complexity',0) for bs in d.values() for b in bs), default=0) +print(mx) +")" +if [ "$actual" -gt "$MAX_CC" ]; then + echo "ERROR: worst cyclomatic complexity is ${actual}, exceeds ${MAX_CC}." >&2 + echo " Current C-rank-or-worse blocks:" >&2 + PYTHONPATH=src scripts/python -m radon cc src/github_usage -s 2>/dev/null \ + | grep -E "\b[C-F] \(" >&2 + exit 1 +fi +echo " OK (worst block = ${actual} <= ${MAX_CC})" + +echo "==> Module size (warn >= ${WARN_LINES}, block > ${BLOCK_LINES})" +blocked=0 +warned=0 +while IFS= read -r line; do + file="${line%%:*}" + n="${line##*:}" + if [ "$n" -gt "$BLOCK_LINES" ]; then + echo " BLOCK $file: ${n} lines (> ${BLOCK_LINES})" >&2 + blocked=1 + elif [ "$n" -ge "$WARN_LINES" ]; then + echo " WARN $file: ${n} lines (>= ${WARN_LINES})" >&2 + warned=1 + fi +done < <(wc -l src/github_usage/*.py | sed '$d' | awk '{print $2":"$1}') +if [ "$blocked" -ne 0 ]; then + echo "ERROR: source modules exceed ${BLOCK_LINES} lines" >&2 + exit 1 +fi +if [ "$warned" -ne 0 ]; then + echo " (warnings above; not blocking)" +fi +echo " OK (no module over ${BLOCK_LINES} lines)" diff --git a/scripts/coverage-baselines b/scripts/coverage-baselines new file mode 100755 index 0000000..4e278e5 --- /dev/null +++ b/scripts/coverage-baselines @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$ROOT_DIR" + +# Regenerate coverage-baselines.json from a fresh coverage run. +# Run this only when you intentionally accept current per-file coverage +# as the new floor (e.g. after deliberately lowering a file's coverage). +SRC="src/github_usage" +OUT="coverage-baselines.json" + +PYTHONPATH=src scripts/python -m coverage run --source="$SRC" -m unittest discover -s tests >/dev/null +PYTHONPATH=src scripts/python -m coverage json -o /tmp/coverage.baseline.json >/dev/null 2>&1 + +PYTHONPATH=src scripts/python -c " +import json +cov=json.load(open('/tmp/coverage.baseline.json'))['files'] +base={f: round(d['summary']['percent_covered'],1) for f,d in cov.items()} +json.dump(base, open('$OUT','w'), indent=2, sort_keys=True) +print('wrote $OUT with', len(base), 'src files') +" + +# Surface files below the default new-file target so maintainers know the gap. +PYTHONPATH=src scripts/python -c " +import json +base=json.load(open('$OUT')) +below=[(f,v) for f,v in base.items() if v<82] +below.sort(key=lambda x:x[1]) +print('files below 82% (grandfathered at current value):', len(below)) +" diff --git a/scripts/coverage-check b/scripts/coverage-check new file mode 100755 index 0000000..a82426b --- /dev/null +++ b/scripts/coverage-check @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +# Coverage policy (set just below current baselines): +# - Overall src coverage must stay at/above COVERAGE_TOTAL_MIN (default 75; +# current overall ~76%). +# - Each src file is locked at its recorded baseline in coverage-baselines.json +# ("don't get worse"): a file may not drop below its baseline. +# - New src files (not in the baseline file) must meet COVERAGE_FILE_MIN +# (default 82). Existing files below that are grandfathered at their baseline. +# Baselines are regenerated with: scripts/coverage-baselines +SRC="src/github_usage" +TOTAL_MIN="${COVERAGE_TOTAL_MIN:-75}" +FILE_MIN="${COVERAGE_FILE_MIN:-82}" +# Allow a small tolerance (percentage points) before a per-file drop is +# treated as a regression. Guards against non-deterministic (flaky) tests +# that occasionally leave one extra line uncovered without a real change. +BASELINE_TOLERANCE="${COVERAGE_TOLERANCE:-1.0}" +BASELINES="coverage-baselines.json" + +echo "==> Running tests under coverage (src only)" +PYTHONPATH=src scripts/python -m coverage run --source="$SRC" -m unittest discover -s tests + +echo "==> Coverage report" +PYTHONPATH=src scripts/python -m coverage report + +# Overall gate. +overall=$(PYTHONPATH=src scripts/python -m coverage report --format=total | tail -1) +if ! PYTHONPATH=src scripts/python -c "import sys; sys.exit(0 if float('$overall') >= $TOTAL_MIN else 1)"; then + echo "ERROR: overall src coverage ${overall}% is below minimum ${TOTAL_MIN}%." >&2 + exit 1 +fi +echo " OK overall ${overall}% >= ${TOTAL_MIN}%" + +# Per-file grandfather + regression lock. +if [ ! -f "$BASELINES" ]; then + echo "ERROR: $BASELINES missing; run scripts/coverage-baselines" >&2 + exit 1 +fi +PYTHONPATH=src scripts/python -m coverage json -o /tmp/coverage.current.json >/dev/null 2>&1 +fails=0 +while IFS= read -r line; do + f="${line%%:*}" + pct="${line##*:}" + base=$(PYTHONPATH=src scripts/python -c " +import json,sys +b=json.load(open('$BASELINES')) +print(b.get('$f', '')) +") + if [ -z "$base" ]; then + # New file: must meet the target minimum. + if ! PYTHONPATH=src scripts/python -c "import sys; sys.exit(0 if float('$pct') >= $FILE_MIN else 1)"; then + echo " NEW FILE $f: ${pct}% < ${FILE_MIN}% (target for new files)" >&2 + fails=1 + fi + else + # Existing file: must not drop below its recorded baseline (minus tolerance). + floor=$(PYTHONPATH=src scripts/python -c "print(round(float('$base') - float('$BASELINE_TOLERANCE'), 1))") + if ! PYTHONPATH=src scripts/python -c "import sys; sys.exit(0 if float('$pct') >= float('$floor') else 1)"; then + echo " REGRESSION $f: ${pct}% < baseline ${base}% (floor ${floor}%)" >&2 + fails=1 + fi + fi +done < <(PYTHONPATH=src scripts/python -c " +import json +d=json.load(open('/tmp/coverage.current.json'))['files'] +for f,info in d.items(): + print(f'{f}:{round(info[\"summary\"][\"percent_covered\"],1)}') +") + +if [ "$fails" -ne 0 ]; then + echo "ERROR: coverage regressions detected (see above)." >&2 + exit 1 +fi +echo " OK (no per-file regressions; new files >= ${FILE_MIN}%)" diff --git a/uv.lock b/uv.lock index 58f3180..768aae1 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, ] +[[package]] +name = "basedpyright" +version = "1.39.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/4b/c1f4e211e50389304d6af32b9280e026a7133e3ad59bbdf8f7a3250f8bee/basedpyright-1.39.9.tar.gz", hash = "sha256:32cbea5fc8273e89df3db20daea56cb7286e419ccdfdc479c64759d2dc071901", size = 24412216, upload-time = "2026-06-27T02:19:49.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/d4/e1fa108710d0498a18c77b1e13897f31eab47c69aa8cfe2d2a4df746541e/basedpyright-1.39.9-py3-none-any.whl", hash = "sha256:6b0837b9eba972c71895167ab9b127e6afdbc17abc92312e3f8d15ca82a5611c", size = 13374276, upload-time = "2026-06-27T02:19:54.431Z" }, +] + [[package]] name = "black" version = "26.5.1" @@ -209,6 +221,120 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" }, + { url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" }, + { url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" }, + { url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, + { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, + { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +] + [[package]] name = "cyclonedx-python-lib" version = "11.11.0" @@ -332,9 +458,12 @@ source = { editable = "." } [package.optional-dependencies] dev = [ { name = "bandit" }, + { name = "basedpyright" }, { name = "black" }, + { name = "coverage" }, { name = "pip-audit" }, { name = "pre-commit" }, + { name = "radon" }, { name = "ruff" }, ] export-pdf = [ @@ -348,6 +477,7 @@ gui = [ ] test-pdf = [ { name = "fpdf2" }, + { name = "pypdf" }, ] test-xlsx = [ { name = "openpyxl" }, @@ -356,13 +486,17 @@ test-xlsx = [ [package.metadata] requires-dist = [ { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.10" }, + { name = "basedpyright", marker = "extra == 'dev'", specifier = ">=1.30" }, { name = "black", marker = "extra == 'dev'", specifier = ">=24.0" }, + { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.0" }, { name = "fpdf2", marker = "extra == 'export-pdf'", specifier = ">=2.7" }, { name = "fpdf2", marker = "extra == 'test-pdf'", specifier = ">=2.7" }, { name = "openpyxl", marker = "extra == 'export-xlsx'", specifier = ">=3.1" }, { name = "openpyxl", marker = "extra == 'test-xlsx'", specifier = ">=3.1" }, { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7" }, + { name = "pypdf", marker = "extra == 'test-pdf'", specifier = ">=3.0" }, + { name = "radon", marker = "extra == 'dev'", specifier = ">=6.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "textual", marker = "extra == 'gui'", specifier = ">=0.70.0" }, ] @@ -410,6 +544,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] +[[package]] +name = "mando" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868, upload-time = "2022-02-24T08:12:27.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149, upload-time = "2022-02-24T08:12:25.24Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -529,6 +675,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "nodejs-wheel-binaries" +version = "24.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/22/2a5beb4e21417c73233d9f65cf6f3e96e891b80d2f550a8f630ebc6b88c6/nodejs_wheel_binaries-24.16.0.tar.gz", hash = "sha256:c973cb69dc5fd16e6f6dc6e579e2c3d5534e2a1f57619dddf5ba070efa7dde37", size = 8056, upload-time = "2026-05-30T16:52:09.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d1/68b43b53cd0fa83ae6fd406705023ca988d9e0ca41c724d82e66fbeb2ef6/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9f8f677dcf30e37ac244f07869726abe043f01eb0f45722b1df31cc2af7093c", size = 55666374, upload-time = "2026-05-30T16:51:39.588Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b2/40a989159599080da485de966c4c2d207e852ac7aa7864702626d96c8bf5/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3d0370fe7120ce9697a4f60d40480d2bd8808d9f30131458d5afc0040d4e5a51", size = 55838487, upload-time = "2026-05-30T16:51:43.383Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a7/cd42174fb5ff6faff7fa8d326a18914d8f232098ab5de055b57c16fa13ca/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85dc92bbb79c851569c5925dcc2a4c915a034efab375f99e4e7e6bbe9cca8342", size = 60179540, upload-time = "2026-05-30T16:51:47.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/c8a1f9ae140aa28df8744d984d01d4b3af7cdd6555af12127f40ceb45a7d/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f3036292811514ba847b3708492644764f88a833ac425c5f55007014308ddfd", size = 60716262, upload-time = "2026-05-30T16:51:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/7c35b3737f59e36d0249c265397b7bff570519b95301d6e16ea361e904ad/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:db8a8a76ebd2b28ecbfc9ad464baa3707241b9e050a30e2efdf6f60c0f886502", size = 62230592, upload-time = "2026-05-30T16:51:55Z" }, + { url = "https://files.pythonhosted.org/packages/04/96/d931255cf9d11a84d6b54d882dba7434646467d568ccf070ea3418638df3/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1a3d8f7b4491cbbd023ba3fc4e901fcca2d9fb80d57f24ba3890de8b1dbac03", size = 62841759, upload-time = "2026-05-30T16:51:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7b/8b7a3f41bc255411be30b6d7d288aab8ffd9ea2055db8555ced3548007b9/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:bb136be9944f0662dcf1120f45193a6b75b13fac378971a95cc42c9f879a81aa", size = 42027734, upload-time = "2026-05-30T16:52:03.348Z" }, + { url = "https://files.pythonhosted.org/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36", size = 39714528, upload-time = "2026-05-30T16:52:06.421Z" }, +] + [[package]] name = "openpyxl" version = "3.1.5" @@ -763,6 +925,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pypdf" +version = "6.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" }, +] + [[package]] name = "python-discovery" version = "1.4.3" @@ -865,6 +1036,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "radon" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "mando" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992, upload-time = "2023-03-26T06:24:38.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784, upload-time = "2023-03-26T06:24:33.949Z" }, +] + [[package]] name = "requests" version = "2.34.2" @@ -918,6 +1102,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sortedcontainers" version = "2.4.0" From d002252864f3e14715abedc01810805bdced51ae Mon Sep 17 00:00:00 2001 From: kgrizz-git <216068303+kgrizz-git@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:31:56 -0400 Subject: [PATCH 2/4] fix(ci): seed coverage baseline from CI, widen tolerance for cross-env delta The macOS-generated coverage-baselines.json diverged from CI/Linux (e.g. setup_launchd.py 36.2% local vs 29.5% in CI), causing the per-file regression gate to fail in CI. - Reseed coverage-baselines.json from the CI coverage report (CI is the authoritative gate). - Raise COVERAGE_TOLERANCE default to 7.0pp to absorb the known macOS<->Linux delta for environment-volatile modules. - Document the CI-seeding approach in the archived plan. --- coverage-baselines.json | 124 +++++++++--------- .../2026-08-07-coverage-complexity-gates.md | 3 +- scripts/coverage-check | 14 +- 3 files changed, 72 insertions(+), 69 deletions(-) diff --git a/coverage-baselines.json b/coverage-baselines.json index a3807b9..01f2f85 100644 --- a/coverage-baselines.json +++ b/coverage-baselines.json @@ -2,90 +2,90 @@ "src/github_usage/__init__.py": 100.0, "src/github_usage/__main__.py": 0.0, "src/github_usage/_email_report_common.py": 100.0, - "src/github_usage/_email_report_html_tables.py": 98.4, - "src/github_usage/api.py": 71.8, + "src/github_usage/_email_report_html_tables.py": 98.0, + "src/github_usage/api.py": 72.0, "src/github_usage/auth.py": 93.0, "src/github_usage/billing.py": 95.0, - "src/github_usage/cli.py": 79.6, - "src/github_usage/cli_email_report.py": 86.1, - "src/github_usage/cli_gui.py": 87.3, + "src/github_usage/cli.py": 80.0, + "src/github_usage/cli_email_report.py": 86.0, + "src/github_usage/cli_gui.py": 87.0, "src/github_usage/cli_parsers.py": 100.0, - "src/github_usage/cli_runs.py": 91.7, - "src/github_usage/cli_runs_diff.py": 77.9, + "src/github_usage/cli_runs.py": 92.0, + "src/github_usage/cli_runs_diff.py": 78.0, "src/github_usage/email_report.py": 100.0, - "src/github_usage/email_report_html.py": 88.1, + "src/github_usage/email_report_html.py": 88.0, "src/github_usage/email_report_send.py": 100.0, - "src/github_usage/email_report_text.py": 93.6, - "src/github_usage/export_csv.py": 93.8, - "src/github_usage/export_json.py": 94.1, - "src/github_usage/export_pdf.py": 94.6, - "src/github_usage/export_report.py": 83.1, + "src/github_usage/email_report_text.py": 94.0, + "src/github_usage/export_csv.py": 94.0, + "src/github_usage/export_json.py": 94.0, + "src/github_usage/export_pdf.py": 95.0, + "src/github_usage/export_report.py": 83.0, "src/github_usage/export_text.py": 100.0, - "src/github_usage/export_visibility.py": 81.8, - "src/github_usage/export_xlsx.py": 97.2, + "src/github_usage/export_visibility.py": 82.0, + "src/github_usage/export_xlsx.py": 97.0, "src/github_usage/forecast.py": 100.0, "src/github_usage/gui/__init__.py": 100.0, - "src/github_usage/gui/app.py": 84.9, - "src/github_usage/gui/async_ops.py": 39.5, + "src/github_usage/gui/app.py": 85.0, + "src/github_usage/gui/async_ops.py": 39.0, "src/github_usage/gui/errors.py": 80.0, "src/github_usage/gui/layout.py": 100.0, - "src/github_usage/gui/log_utils.py": 71.4, - "src/github_usage/gui/main_window.py": 69.2, - "src/github_usage/gui/modals.py": 45.5, - "src/github_usage/gui/prefs.py": 95.3, - "src/github_usage/gui/scroll_actions.py": 82.4, - "src/github_usage/gui/state.py": 67.1, + "src/github_usage/gui/log_utils.py": 71.0, + "src/github_usage/gui/main_window.py": 69.0, + "src/github_usage/gui/modals.py": 45.0, + "src/github_usage/gui/prefs.py": 95.0, + "src/github_usage/gui/scroll_actions.py": 82.0, + "src/github_usage/gui/state.py": 67.0, "src/github_usage/gui/views/__init__.py": 100.0, - "src/github_usage/gui/views/email_report_view.py": 51.1, - "src/github_usage/gui/views/report_view.py": 39.9, - "src/github_usage/gui/views/runs_view.py": 56.1, - "src/github_usage/gui/views/schedules_view.py": 62.9, - "src/github_usage/gui/views/setup_profiles_panel.py": 66.9, + "src/github_usage/gui/views/email_report_view.py": 51.0, + "src/github_usage/gui/views/report_view.py": 40.0, + "src/github_usage/gui/views/runs_view.py": 56.0, + "src/github_usage/gui/views/schedules_view.py": 62.0, + "src/github_usage/gui/views/setup_profiles_panel.py": 67.0, "src/github_usage/gui/views/setup_secrets_panel.py": 95.0, - "src/github_usage/gui/views/setup_verify_panel.py": 63.6, - "src/github_usage/gui/views/setup_view.py": 61.7, + "src/github_usage/gui/views/setup_verify_panel.py": 64.0, + "src/github_usage/gui/views/setup_view.py": 62.0, "src/github_usage/gui/widgets/__init__.py": 100.0, - "src/github_usage/gui/widgets/schedule_picker.py": 75.3, - "src/github_usage/gui/wizard/__init__.py": 42.9, - "src/github_usage/gui/wizard/setup_wizard_flow.py": 79.6, - "src/github_usage/gui/wizard/setup_wizard_screen.py": 47.3, + "src/github_usage/gui/widgets/schedule_picker.py": 75.0, + "src/github_usage/gui/wizard/__init__.py": 43.0, + "src/github_usage/gui/wizard/setup_wizard_flow.py": 81.0, + "src/github_usage/gui/wizard/setup_wizard_screen.py": 47.0, "src/github_usage/gui/workers.py": 0.0, - "src/github_usage/gui_backend.py": 54.2, - "src/github_usage/http_retry.py": 98.6, - "src/github_usage/legacy.py": 93.3, - "src/github_usage/legacy_report.py": 63.9, - "src/github_usage/legacy_report_data.py": 77.7, - "src/github_usage/legacy_report_summary.py": 86.2, - "src/github_usage/legacy_terminal.py": 84.4, - "src/github_usage/redact.py": 97.6, + "src/github_usage/gui_backend.py": 54.0, + "src/github_usage/http_retry.py": 99.0, + "src/github_usage/legacy.py": 93.0, + "src/github_usage/legacy_report.py": 64.0, + "src/github_usage/legacy_report_data.py": 78.0, + "src/github_usage/legacy_report_summary.py": 86.0, + "src/github_usage/legacy_terminal.py": 84.0, + "src/github_usage/redact.py": 98.0, "src/github_usage/repo_consumers.py": 100.0, "src/github_usage/report_account.py": 100.0, - "src/github_usage/report_actions.py": 78.3, - "src/github_usage/report_actions_limits.py": 95.6, - "src/github_usage/report_cache.py": 79.3, + "src/github_usage/report_actions.py": 78.0, + "src/github_usage/report_actions_limits.py": 96.0, + "src/github_usage/report_cache.py": 79.0, "src/github_usage/report_data.py": 90.0, - "src/github_usage/report_forecast.py": 86.4, + "src/github_usage/report_forecast.py": 86.0, "src/github_usage/report_forecast_data.py": 95.0, "src/github_usage/report_helpers.py": 100.0, "src/github_usage/report_optional.py": 100.0, - "src/github_usage/report_products.py": 51.3, - "src/github_usage/report_sources.py": 45.5, + "src/github_usage/report_products.py": 51.0, + "src/github_usage/report_sources.py": 45.0, "src/github_usage/report_storage.py": 43.0, - "src/github_usage/report_summary.py": 98.4, - "src/github_usage/report_summary_insights.py": 85.8, - "src/github_usage/report_summary_private.py": 95.3, - "src/github_usage/report_workflow_minutes.py": 95.6, - "src/github_usage/schedule_helpers.py": 78.5, - "src/github_usage/setup_ci.py": 15.4, - "src/github_usage/setup_config.py": 82.3, + "src/github_usage/report_summary.py": 98.0, + "src/github_usage/report_summary_insights.py": 86.0, + "src/github_usage/report_summary_private.py": 95.0, + "src/github_usage/report_workflow_minutes.py": 96.0, + "src/github_usage/schedule_helpers.py": 78.0, + "src/github_usage/setup_ci.py": 15.0, + "src/github_usage/setup_config.py": 83.0, "src/github_usage/setup_email_config.py": 60.0, - "src/github_usage/setup_launchd.py": 36.2, - "src/github_usage/setup_prompts.py": 38.7, - "src/github_usage/setup_secrets.py": 21.4, - "src/github_usage/setup_wizard.py": 51.4, - "src/github_usage/setup_workflow.py": 81.2, - "src/github_usage/storage.py": 93.3, + "src/github_usage/setup_launchd.py": 30.0, + "src/github_usage/setup_prompts.py": 39.0, + "src/github_usage/setup_secrets.py": 21.0, + "src/github_usage/setup_wizard.py": 51.0, + "src/github_usage/setup_workflow.py": 81.0, + "src/github_usage/storage.py": 93.0, "src/github_usage/terminal.py": 100.0, - "src/github_usage/usage_split.py": 92.8, + "src/github_usage/usage_split.py": 93.0, "src/github_usage/visibility.py": 100.0 } diff --git a/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md b/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md index 84f33f0..c31ea9d 100644 --- a/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md +++ b/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md @@ -36,6 +36,7 @@ - **Lower threshold, same strictness:** `COVERAGE_TOTAL_MIN=75` is just below the 76% overall; the stricter 82% applies to new files. Complexity ceiling `MAX_CC=28` is the current worst, so it blocks only *new* blocks worse than today (no need to exempt the 55 existing C+ blocks). - `radon --max-cc` was evaluated but rejected: it exits non-zero whenever any C+ block exists regardless of the numeric threshold. The script instead computes the true max from JSON and compares to `MAX_CC`. - Earlier analysis incorrectly reported "zero C-or-worse blocks" due to grepping a truncated sorted tail; corrected with full-output measurement. +- **CI baseline seeding (post-review fix):** the originally committed `coverage-baselines.json` was generated on macOS and diverged from CI/Linux (e.g. `setup_launchd.py` measured 36.2% locally vs 29.5% in CI). CI runs failed the per-file regression gate. Fixed by reseeding the baseline from the CI coverage report and raising `COVERAGE_TOLERANCE` to 7.0pp to absorb the known macOS↔Linux delta for environment-volatile modules. CI is the authoritative gate; `scripts/coverage-baselines` regenerates from the *local* environment and should only be used to refresh after an intentional local change, not to "fix" CI drift. ## Verification @@ -43,7 +44,7 @@ scripts/check # full harness, green scripts/coverage-check # overall 76% >= 75%; no per-file regression; new files >= 82% scripts/coverage-baselines # regenerate baseline file -scripts/check-complexity # worst CC 28 <= 28; warn >=600, block >700 +scripts/check-complexity # worst CC 28 <= 28; warn >=575, block >650 uv lock --check # consistent pre-commit validate-config ``` diff --git a/scripts/coverage-check b/scripts/coverage-check index a82426b..aad8cae 100755 --- a/scripts/coverage-check +++ b/scripts/coverage-check @@ -8,17 +8,19 @@ cd "$ROOT_DIR" # - Overall src coverage must stay at/above COVERAGE_TOTAL_MIN (default 75; # current overall ~76%). # - Each src file is locked at its recorded baseline in coverage-baselines.json -# ("don't get worse"): a file may not drop below its baseline. +# ("don't get worse"): a file may not drop below its baseline (minus tolerance). # - New src files (not in the baseline file) must meet COVERAGE_FILE_MIN # (default 82). Existing files below that are grandfathered at their baseline. -# Baselines are regenerated with: scripts/coverage-baselines +# The committed baseline is seeded from CI (Linux) coverage; see the plan. SRC="src/github_usage" TOTAL_MIN="${COVERAGE_TOTAL_MIN:-75}" FILE_MIN="${COVERAGE_FILE_MIN:-82}" -# Allow a small tolerance (percentage points) before a per-file drop is -# treated as a regression. Guards against non-deterministic (flaky) tests -# that occasionally leave one extra line uncovered without a real change. -BASELINE_TOLERANCE="${COVERAGE_TOLERANCE:-1.0}" +# Tolerance (percentage points) before a per-file drop is a regression. +# Covers two things: (1) non-deterministic (flaky) tests that occasionally +# leave one extra line uncovered, and (2) the known macOS-vs-Linux coverage +# delta for environment-volatile modules (notably setup_launchd.py, ~6pt). +# The committed baseline is CI/Linux-seeded, so CI is the authoritative gate. +BASELINE_TOLERANCE="${COVERAGE_TOLERANCE:-7.0}" BASELINES="coverage-baselines.json" echo "==> Running tests under coverage (src only)" From 58e686345826698f8993b945306e8e1fe1104547 Mon Sep 17 00:00:00 2001 From: kgrizz-git <216068303+kgrizz-git@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:29:10 -0400 Subject: [PATCH 3/4] simplify(coverage): drop per-file baseline lock, keep overall 75% gate The per-file regression lock (coverage-baselines.json) was environment-sensitive: macOS-generated baselines diverged from CI/Linux (e.g. setup_launchd.py 36.2% local vs 29.5% CI), failing CI. Replace it with a single portable gate: - Overall src coverage must stay >= 75% (COVERAGE_TOTAL_MIN). - New src files (not in committed tree) must meet 82% (COVERAGE_FILE_MIN), detected via git ls-tree HEAD. - Remove coverage-baselines.json and scripts/coverage-baselines. - Update CHANGELOG and archived plan. --- CHANGELOG.md | 2 +- .../2026-08-07-coverage-complexity-gates.md | 14 ++---- scripts/coverage-check | 47 +++++-------------- 3 files changed, 18 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db6cc2c..87302b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ This project follows the structure from Keep a Changelog and intends to use Sema ### Added -- **Coverage, complexity, and module-size gates:** `scripts/coverage-check` runs the unittest suite under `coverage` (scoped to `src/github_usage`). Overall src coverage must stay ≥ `COVERAGE_TOTAL_MIN` (default 75; current ~76%). Each file is locked to its recorded baseline in `coverage-baselines.json` ("don't get worse" — a file may not drop below its baseline; existing files below the 82% target are grandfathered), while any **new** `src` file must meet `COVERAGE_FILE_MIN` (default 82%). A `COVERAGE_TOLERANCE` (default 1.0 percentage point) absorbs non-deterministic (flaky) test runs so only real drops fail. `scripts/coverage-baselines` regenerates the baseline file. `scripts/check-complexity` fails if the worst cyclomatic complexity exceeds `MAX_CC` (default 28; current worst) and warns at `WARN_LINES`=575 / blocks at `BLOCK_LINES`=650 for module size (current largest 562). Both scripts are wired into `scripts/check`, added as pre-push hooks in `.pre-commit-config.yaml`, and coverage runs as its own `coverage` job in `ci.yml`. `coverage` and `radon` added to the `dev` extra; `uv.lock` regenerated. +- **Coverage, complexity, and module-size gates:** `scripts/coverage-check` runs the unittest suite under `coverage` (scoped to `src/github_usage`). Overall src coverage must stay ≥ `COVERAGE_TOTAL_MIN` (default 75; current ~76%). Any **new** `src` file (not in the committed tree) must meet `COVERAGE_FILE_MIN` (default 82%) so coverage cannot erode one file at a time; existing files are protected by the overall gate only. `scripts/check-complexity` fails if the worst cyclomatic complexity exceeds `MAX_CC` (default 28; current worst) and warns at `WARN_LINES`=575 / blocks at `BLOCK_LINES`=650 for module size (current largest 562). Both scripts are wired into `scripts/check`, added as pre-push hooks in `.pre-commit-config.yaml`, and coverage runs as its own `coverage` job in `ci.yml`. `coverage` and `radon` added to the `dev` extra; `uv.lock` regenerated. - **Private-repo top consumers** ([plan](docs/superpowers/plans/archived/2026-07-31-private-top-consumers.md)): local full report (terminal + TUI), email text/HTML, and JSON exports gain private-only top lists for Actions minutes and billed storage alongside existing overall rankings; overall top-by-storage (billed avg MB) and artifact-scan private lists where they add signal. Shared `repo_consumers.py` helper with `private_list_is_redundant` guards; report cache version bumped to 3. HTML table helpers extracted to `_email_report_html_tables.py`. - **Minutes-by-workflow for top private consumer** ([plan](docs/superpowers/plans/archived/2026-07-31-private-top-consumers.md)): estimated per-workflow Actions minutes from completed run wall-clock times for the top private-repo minutes consumer. New `report_workflow_minutes.py` with shared `runs_cache`; terminal and email sections include an approximation caveat (not billable — will not match billed repo totals). Quota estimates bump by +10 when the workflow path is enabled. - **Private-usage emphasis for Actions free-tier reporting** ([plan](docs/superpowers/plans/archived/2026-07-30-private-usage-emphasis.md)): Limits Summary, utilization bars, and forecasts measure **private-repo** Actions minutes/storage against the free tier; public standard-runner usage is shown separately as free. Artifact storage section frames the 500 MB private allowance as GB-hrs accrual, splits artifacts vs release assets, and surfaces expiry/retention. Larger-runner SKUs are flagged `*`. Legacy reports include a Sources footer. Email `repo_consumers` gains `by_visibility`; report cache version bumped to 2 (stale v1 snapshots rejected). CSV/XLSX/PDF/JSON exports and TUI summary rows carry the same private-vs-public framing, storage analysis, and Sources section. diff --git a/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md b/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md index c31ea9d..747d71e 100644 --- a/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md +++ b/docs/superpowers/plans/archived/2026-08-07-coverage-complexity-gates.md @@ -12,38 +12,34 @@ - **Coverage (scoped to `src/github_usage`):** overall **76%**. 39 of 89 `src` files below 82%; worst `setup_ci` (15%), `setup_secrets` (21%), `setup_launchd` (36%). (Earlier "86%" figure included `tests/` and `__init__` in the measured set; scoped measurement is the correct product-code number.) - **Complexity:** 807 blocks; worst cyclomatic complexity = **28** (`setup_wizard._manage_profiles`, D-rank). 55 blocks at C-rank (11+) or worse, including three D-rank. No block exceeds 28. - **Module size:** warn at 575, block at 650 (current largest 562). Changed from 600/700 to tighten the guard closer to the current ceiling. -- **Coverage tolerance:** `COVERAGE_TOLERANCE`=1.0pp absorbs flaky (non-deterministic) test runs (e.g. `schedules_view.py` oscillates 91↔92 missing lines across identical runs). Only drops beyond the tolerance are treated as regressions. Verified stable across repeated runs while a 23pt real drop still fails. ## Tasks - [x] Add `coverage` and `radon` to the `dev` optional-dependencies in `pyproject.toml`. - [x] Regenerate `uv.lock` (`uv lock`; `uv lock --check` passes). -- [x] Create `scripts/coverage-check`: scoped to `src/github_usage`; overall must stay ≥ `COVERAGE_TOTAL_MIN` (75). -- [x] Create `coverage-baselines.json` + `scripts/coverage-baselines` generator: each `src` file locked to its recorded percent ("don't get worse"); existing files below 82% are grandfathered at their current value; **new** `src` files must meet `COVERAGE_FILE_MIN` (82). +- [x] Create `scripts/coverage-check`: scoped to `src/github_usage`; overall must stay ≥ `COVERAGE_TOTAL_MIN` (75); **new** `src` files (not in the committed tree) must meet `COVERAGE_FILE_MIN` (82). - [x] Create `scripts/check-complexity`: fails if worst block CC > `MAX_CC` (28, the current worst); module size warns at `WARN_LINES`=575 and blocks at `BLOCK_LINES`=650. - [x] Wire both into `scripts/check` (after backup policy, before "check passed"). - [x] Add `coverage-check` and `check-complexity` as pre-push local hooks in `.pre-commit-config.yaml` (mirrors `run-scripts-check`). - [x] Add a `coverage` job to `.github/workflows/ci.yml` running `scripts/coverage-check`. -- [x] Confirm `scripts/check` passes end-to-end (overall 76% ≥ 75%; no per-file regression; worst CC 28 ≤ 28; max file 562 ≤ 700). -- [x] Validate regression lock: raising a baseline above actual coverage fails the gate; a new 0% file fails the gate. +- [x] Confirm `scripts/check` passes end-to-end (overall 76% ≥ 75%; worst CC 28 ≤ 28; max file 562 ≤ 700). +- [x] Validate new-file gate: a new 0% src file fails the gate. - [x] Validate `.pre-commit-config.yaml` (`pre-commit validate-config` → rc 0). - [x] Record in `CHANGELOG.md` `[Unreleased] > Added`. ## Notes / Deviations - **Coverage measured scope:** coverage is measured with `--source=src/github_usage` so only product code is gated (test files excluded). Overall scoped coverage is **76%**, not the earlier 86% (which double-counted `tests/`). -- **Grandfathering without a hardcoded list:** instead of maintaining a separate "exempt files" list, every `src` file's current percentage is the floor (`coverage-baselines.json`). This automatically grandfathers the 39 files below 82% while still blocking any of them from *dropping*, and forces any brand-new file to hit 82%. +- **Per-file baseline lock dropped:** an earlier design locked every file to a baseline in `coverage-baselines.json` ("don't get worse"). That baseline was environment-sensitive — macOS-generated numbers diverged from CI/Linux (e.g. `setup_launchd.py` 36.2% local vs 29.5% CI), causing CI failures. Simplified to a single portable **overall ≥ 75%** gate plus an **82% gate for new files only** (detected via `git ls-tree HEAD`). No baseline file to drift; fully portable across OSes. - **Lower threshold, same strictness:** `COVERAGE_TOTAL_MIN=75` is just below the 76% overall; the stricter 82% applies to new files. Complexity ceiling `MAX_CC=28` is the current worst, so it blocks only *new* blocks worse than today (no need to exempt the 55 existing C+ blocks). - `radon --max-cc` was evaluated but rejected: it exits non-zero whenever any C+ block exists regardless of the numeric threshold. The script instead computes the true max from JSON and compares to `MAX_CC`. - Earlier analysis incorrectly reported "zero C-or-worse blocks" due to grepping a truncated sorted tail; corrected with full-output measurement. -- **CI baseline seeding (post-review fix):** the originally committed `coverage-baselines.json` was generated on macOS and diverged from CI/Linux (e.g. `setup_launchd.py` measured 36.2% locally vs 29.5% in CI). CI runs failed the per-file regression gate. Fixed by reseeding the baseline from the CI coverage report and raising `COVERAGE_TOLERANCE` to 7.0pp to absorb the known macOS↔Linux delta for environment-volatile modules. CI is the authoritative gate; `scripts/coverage-baselines` regenerates from the *local* environment and should only be used to refresh after an intentional local change, not to "fix" CI drift. ## Verification ``` scripts/check # full harness, green -scripts/coverage-check # overall 76% >= 75%; no per-file regression; new files >= 82% -scripts/coverage-baselines # regenerate baseline file +scripts/coverage-check # overall 76% >= 75%; new files >= 82% scripts/check-complexity # worst CC 28 <= 28; warn >=575, block >650 uv lock --check # consistent pre-commit validate-config diff --git a/scripts/coverage-check b/scripts/coverage-check index aad8cae..2e110c1 100755 --- a/scripts/coverage-check +++ b/scripts/coverage-check @@ -4,24 +4,15 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" -# Coverage policy (set just below current baselines): +# Coverage policy: # - Overall src coverage must stay at/above COVERAGE_TOTAL_MIN (default 75; -# current overall ~76%). -# - Each src file is locked at its recorded baseline in coverage-baselines.json -# ("don't get worse"): a file may not drop below its baseline (minus tolerance). -# - New src files (not in the baseline file) must meet COVERAGE_FILE_MIN -# (default 82). Existing files below that are grandfathered at their baseline. -# The committed baseline is seeded from CI (Linux) coverage; see the plan. +# current overall ~76%). This is the single portable gate. +# - Any NEW src file (not present in the previous commit's tree) must meet +# COVERAGE_FILE_MIN (default 82). Existing files are not individually +# gated; the overall number protects them. SRC="src/github_usage" TOTAL_MIN="${COVERAGE_TOTAL_MIN:-75}" FILE_MIN="${COVERAGE_FILE_MIN:-82}" -# Tolerance (percentage points) before a per-file drop is a regression. -# Covers two things: (1) non-deterministic (flaky) tests that occasionally -# leave one extra line uncovered, and (2) the known macOS-vs-Linux coverage -# delta for environment-volatile modules (notably setup_launchd.py, ~6pt). -# The committed baseline is CI/Linux-seeded, so CI is the authoritative gate. -BASELINE_TOLERANCE="${COVERAGE_TOLERANCE:-7.0}" -BASELINES="coverage-baselines.json" echo "==> Running tests under coverage (src only)" PYTHONPATH=src scripts/python -m coverage run --source="$SRC" -m unittest discover -s tests @@ -37,34 +28,20 @@ if ! PYTHONPATH=src scripts/python -c "import sys; sys.exit(0 if float('$overall fi echo " OK overall ${overall}% >= ${TOTAL_MIN}%" -# Per-file grandfather + regression lock. -if [ ! -f "$BASELINES" ]; then - echo "ERROR: $BASELINES missing; run scripts/coverage-baselines" >&2 - exit 1 -fi +# New-file gate: compare the current src tree against the committed tree to +# find files that did not exist before. A brand-new source file must meet the +# target minimum so coverage does not erode one file at a time. PYTHONPATH=src scripts/python -m coverage json -o /tmp/coverage.current.json >/dev/null 2>&1 +base_tree=$(git ls-tree -r --name-only HEAD -- "$SRC" 2>/dev/null || true) fails=0 while IFS= read -r line; do f="${line%%:*}" pct="${line##*:}" - base=$(PYTHONPATH=src scripts/python -c " -import json,sys -b=json.load(open('$BASELINES')) -print(b.get('$f', '')) -") - if [ -z "$base" ]; then - # New file: must meet the target minimum. + if ! printf '%s\n' "$base_tree" | grep -qx "$f"; then if ! PYTHONPATH=src scripts/python -c "import sys; sys.exit(0 if float('$pct') >= $FILE_MIN else 1)"; then echo " NEW FILE $f: ${pct}% < ${FILE_MIN}% (target for new files)" >&2 fails=1 fi - else - # Existing file: must not drop below its recorded baseline (minus tolerance). - floor=$(PYTHONPATH=src scripts/python -c "print(round(float('$base') - float('$BASELINE_TOLERANCE'), 1))") - if ! PYTHONPATH=src scripts/python -c "import sys; sys.exit(0 if float('$pct') >= float('$floor') else 1)"; then - echo " REGRESSION $f: ${pct}% < baseline ${base}% (floor ${floor}%)" >&2 - fails=1 - fi fi done < <(PYTHONPATH=src scripts/python -c " import json @@ -74,7 +51,7 @@ for f,info in d.items(): ") if [ "$fails" -ne 0 ]; then - echo "ERROR: coverage regressions detected (see above)." >&2 + echo "ERROR: new-file coverage below ${FILE_MIN}% (see above)." >&2 exit 1 fi -echo " OK (no per-file regressions; new files >= ${FILE_MIN}%)" +echo " OK (new src files >= ${FILE_MIN}%)" From 8d87e21cb81f0983ffee406aef59fcb94d14ff20 Mon Sep 17 00:00:00 2001 From: kgrizz-git <216068303+kgrizz-git@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:29:42 -0400 Subject: [PATCH 4/4] chore: remove obsolete coverage-baselines files Delete coverage-baselines.json and scripts/coverage-baselines, superseded by the simplified overall-coverage gate. --- coverage-baselines.json | 91 -------------------------------------- scripts/coverage-baselines | 31 ------------- 2 files changed, 122 deletions(-) delete mode 100644 coverage-baselines.json delete mode 100755 scripts/coverage-baselines diff --git a/coverage-baselines.json b/coverage-baselines.json deleted file mode 100644 index 01f2f85..0000000 --- a/coverage-baselines.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "src/github_usage/__init__.py": 100.0, - "src/github_usage/__main__.py": 0.0, - "src/github_usage/_email_report_common.py": 100.0, - "src/github_usage/_email_report_html_tables.py": 98.0, - "src/github_usage/api.py": 72.0, - "src/github_usage/auth.py": 93.0, - "src/github_usage/billing.py": 95.0, - "src/github_usage/cli.py": 80.0, - "src/github_usage/cli_email_report.py": 86.0, - "src/github_usage/cli_gui.py": 87.0, - "src/github_usage/cli_parsers.py": 100.0, - "src/github_usage/cli_runs.py": 92.0, - "src/github_usage/cli_runs_diff.py": 78.0, - "src/github_usage/email_report.py": 100.0, - "src/github_usage/email_report_html.py": 88.0, - "src/github_usage/email_report_send.py": 100.0, - "src/github_usage/email_report_text.py": 94.0, - "src/github_usage/export_csv.py": 94.0, - "src/github_usage/export_json.py": 94.0, - "src/github_usage/export_pdf.py": 95.0, - "src/github_usage/export_report.py": 83.0, - "src/github_usage/export_text.py": 100.0, - "src/github_usage/export_visibility.py": 82.0, - "src/github_usage/export_xlsx.py": 97.0, - "src/github_usage/forecast.py": 100.0, - "src/github_usage/gui/__init__.py": 100.0, - "src/github_usage/gui/app.py": 85.0, - "src/github_usage/gui/async_ops.py": 39.0, - "src/github_usage/gui/errors.py": 80.0, - "src/github_usage/gui/layout.py": 100.0, - "src/github_usage/gui/log_utils.py": 71.0, - "src/github_usage/gui/main_window.py": 69.0, - "src/github_usage/gui/modals.py": 45.0, - "src/github_usage/gui/prefs.py": 95.0, - "src/github_usage/gui/scroll_actions.py": 82.0, - "src/github_usage/gui/state.py": 67.0, - "src/github_usage/gui/views/__init__.py": 100.0, - "src/github_usage/gui/views/email_report_view.py": 51.0, - "src/github_usage/gui/views/report_view.py": 40.0, - "src/github_usage/gui/views/runs_view.py": 56.0, - "src/github_usage/gui/views/schedules_view.py": 62.0, - "src/github_usage/gui/views/setup_profiles_panel.py": 67.0, - "src/github_usage/gui/views/setup_secrets_panel.py": 95.0, - "src/github_usage/gui/views/setup_verify_panel.py": 64.0, - "src/github_usage/gui/views/setup_view.py": 62.0, - "src/github_usage/gui/widgets/__init__.py": 100.0, - "src/github_usage/gui/widgets/schedule_picker.py": 75.0, - "src/github_usage/gui/wizard/__init__.py": 43.0, - "src/github_usage/gui/wizard/setup_wizard_flow.py": 81.0, - "src/github_usage/gui/wizard/setup_wizard_screen.py": 47.0, - "src/github_usage/gui/workers.py": 0.0, - "src/github_usage/gui_backend.py": 54.0, - "src/github_usage/http_retry.py": 99.0, - "src/github_usage/legacy.py": 93.0, - "src/github_usage/legacy_report.py": 64.0, - "src/github_usage/legacy_report_data.py": 78.0, - "src/github_usage/legacy_report_summary.py": 86.0, - "src/github_usage/legacy_terminal.py": 84.0, - "src/github_usage/redact.py": 98.0, - "src/github_usage/repo_consumers.py": 100.0, - "src/github_usage/report_account.py": 100.0, - "src/github_usage/report_actions.py": 78.0, - "src/github_usage/report_actions_limits.py": 96.0, - "src/github_usage/report_cache.py": 79.0, - "src/github_usage/report_data.py": 90.0, - "src/github_usage/report_forecast.py": 86.0, - "src/github_usage/report_forecast_data.py": 95.0, - "src/github_usage/report_helpers.py": 100.0, - "src/github_usage/report_optional.py": 100.0, - "src/github_usage/report_products.py": 51.0, - "src/github_usage/report_sources.py": 45.0, - "src/github_usage/report_storage.py": 43.0, - "src/github_usage/report_summary.py": 98.0, - "src/github_usage/report_summary_insights.py": 86.0, - "src/github_usage/report_summary_private.py": 95.0, - "src/github_usage/report_workflow_minutes.py": 96.0, - "src/github_usage/schedule_helpers.py": 78.0, - "src/github_usage/setup_ci.py": 15.0, - "src/github_usage/setup_config.py": 83.0, - "src/github_usage/setup_email_config.py": 60.0, - "src/github_usage/setup_launchd.py": 30.0, - "src/github_usage/setup_prompts.py": 39.0, - "src/github_usage/setup_secrets.py": 21.0, - "src/github_usage/setup_wizard.py": 51.0, - "src/github_usage/setup_workflow.py": 81.0, - "src/github_usage/storage.py": 93.0, - "src/github_usage/terminal.py": 100.0, - "src/github_usage/usage_split.py": 93.0, - "src/github_usage/visibility.py": 100.0 -} diff --git a/scripts/coverage-baselines b/scripts/coverage-baselines deleted file mode 100755 index 4e278e5..0000000 --- a/scripts/coverage-baselines +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -cd "$ROOT_DIR" - -# Regenerate coverage-baselines.json from a fresh coverage run. -# Run this only when you intentionally accept current per-file coverage -# as the new floor (e.g. after deliberately lowering a file's coverage). -SRC="src/github_usage" -OUT="coverage-baselines.json" - -PYTHONPATH=src scripts/python -m coverage run --source="$SRC" -m unittest discover -s tests >/dev/null -PYTHONPATH=src scripts/python -m coverage json -o /tmp/coverage.baseline.json >/dev/null 2>&1 - -PYTHONPATH=src scripts/python -c " -import json -cov=json.load(open('/tmp/coverage.baseline.json'))['files'] -base={f: round(d['summary']['percent_covered'],1) for f,d in cov.items()} -json.dump(base, open('$OUT','w'), indent=2, sort_keys=True) -print('wrote $OUT with', len(base), 'src files') -" - -# Surface files below the default new-file target so maintainers know the gap. -PYTHONPATH=src scripts/python -c " -import json -base=json.load(open('$OUT')) -below=[(f,v) for f,v in base.items() if v<82] -below.sort(key=lambda x:x[1]) -print('files below 82% (grandfathered at current value):', len(below)) -"