ci: move type-check slack into the budget json and trim lint timeout

Make slack declarative: the budget is now {"slack": N, "files": {path: count}}
so the tolerance is tuned in JSON without editing the script, mirroring how
ruff-strict-budget.json carries its slack. --update preserves the existing
slack. Also drop the lint job timeout from 15m to 10m; the mypy and
basedpyright passes add ~2m, leaving the job around 4-5m, so 10m is a
comfortable margin.
This commit is contained in:
mateo-berri 2026-06-14 16:11:29 +00:00
parent 8adb982f36
commit eaa8546e71
No known key found for this signature in database
4 changed files with 2474 additions and 2461 deletions

View file

@ -14,7 +14,7 @@ permissions:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 10
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,13 @@
"""Per-file count gate for mypy and basedpyright.
Each tool's output is reduced to a count of errors per file and checked against
a committed budget, the max errors allowed per file. Counts ignore line and
column numbers, so they survive code moving within a file; a file fails only
when it gains more errors than its ceiling. Run with --update to re-capture the
budget from the current tree (ratchet). The tool output is read from stdin, so
the caller decides how to invoke mypy or basedpyright.
a committed budget of the form {"slack": N, "files": {path: count}}. Counts
ignore line and column numbers, so they survive code moving within a file; a
file fails only when it gains more than `slack` errors over its recorded count.
slack lives in the JSON so it can be tuned without touching this script. Run
with --update to re-capture the counts from the current tree (ratchet),
preserving the existing slack. Tool output is read from stdin, so the caller
decides how to invoke mypy or basedpyright.
"""
import argparse
@ -24,10 +26,9 @@ PATTERNS: Mapping[str, re.Pattern[str]] = {
"basedpyright": re.compile(r"^\s*(?P<file>.+?):\d+:\d+ - error:"),
}
# Headroom per file on top of its recorded count, so an inference ripple in an
# unrelated file (basedpyright especially) does not fail the build over a couple
# of errors. Small on purpose: a file still fails once it drifts past this.
PER_FILE_SLACK = 5
# Seed slack written into a freshly created budget. Existing budgets keep
# whatever slack is already declared in their JSON.
DEFAULT_SLACK = 5
class Breach(NamedTuple):
@ -73,15 +74,21 @@ def budget_path(tool: str) -> Path:
def cmd_update(tool: str, counts: Mapping[str, int]) -> None:
path = budget_path(tool)
path.write_text(json.dumps(dict(sorted(counts.items())), indent=2) + "\n")
slack = (
json.loads(path.read_text()).get("slack", DEFAULT_SLACK)
if path.exists()
else DEFAULT_SLACK
)
data = {"slack": slack, "files": dict(sorted(counts.items()))}
path.write_text(json.dumps(data, indent=2) + "\n")
print(
f"Re-captured {tool} per-file budget: {len(counts)} files, {sum(counts.values())} errors"
f"Re-captured {tool} per-file budget: {len(counts)} files, {sum(counts.values())} errors (slack {slack})"
)
def cmd_check(tool: str, counts: Mapping[str, int]) -> None:
budget = json.loads(budget_path(tool).read_text())
breaches = evaluate(counts, budget, PER_FILE_SLACK)
breaches = evaluate(counts, budget["files"], budget["slack"])
if not breaches:
print(
f"OK: every file is within its {tool} ceiling ({sum(counts.values())} errors total)"