diff --git a/src/agent_comms/commander/rules/tests-and-commands.md b/src/agent_comms/commander/rules/tests-and-commands.md index 5b5f4876a7..863dbff8ee 100644 --- a/src/agent_comms/commander/rules/tests-and-commands.md +++ b/src/agent_comms/commander/rules/tests-and-commands.md @@ -14,4 +14,4 @@ type: feedback - An order check counts over the whole source tree (`grep -r ... src`), never one file. - Run verb gate loops under `bash -c`: zsh passes a quoted "order idle" as one argument. - A wait or kill by process pattern brackets its first letter (`grep -q "[v]erify"`) or waits on a file or PID, or it matches itself. -- Lint, every commit: run `ruff check` on the .py files in your diff (`git diff --name-only origin/main -- '*.py'`) and fix only the findings on lines you added or changed. Never run `ruff format` on a whole file, never rename a class or function to quiet a lint rule, and never touch lines the order didn't name. Main's existing lint debt is counted by the ratchet; sweeping it is an off-order edit. Your reply includes `git diff --stat origin/main...HEAD`. +- Lint warns, never blocks: the debt ratchet reports ruff and Black growth as warnings on the file and gates only restated facts. Read the warnings on lines you added or changed; fix one when it names a real defect, such as an undefined name or dead code. Never run `ruff format` on a whole file, never rename a class or function to quiet a lint rule, and never touch lines the order didn't name. Your reply includes `git diff --stat origin/main...HEAD`. diff --git a/tests/guards/test_debt_ratchet.py b/tests/guards/test_debt_ratchet.py index 0621bad747..72bd3c7605 100644 --- a/tests/guards/test_debt_ratchet.py +++ b/tests/guards/test_debt_ratchet.py @@ -694,8 +694,6 @@ async def second(value): (AnyName, "from typing import Any\ndef read(value: Any) -> None: ...\n"), (AnyName, "from typing import Any\nvalue = Any\n"), (UnannotatedParameter, "def read(value): ...\n"), - (Unformatted, "value=1\n"), - (LintFindings, "import os\n"), (TypeErrors, 'value: int = "text"\n'), ], ) @@ -712,6 +710,21 @@ def test_per_file_increase_cannot_be_offset_or_moved( ) +@pytest.mark.parametrize( + ("measure", "source"), [(Unformatted, "value=1\n"), (LintFindings, "import os\n")] +) +def test_advisory_growth_is_measured_and_warned_never_failed( + repo: Repository, measure: type[Measure], source: str +) -> None: + base = repo.commit({"new.py": ""}) + head = repo.commit({"new.py": source}) + status, report = repo.compare(base, head, measure) + key = measure.key(f"{repo.root}/new.py") + assert status == 0 + assert report["delta"][key] > 0 + assert measure.grew(key, 1).startswith(f"::warning file={repo.root}/new.py::") + + def test_a_renamed_file_compares_with_its_old_path(repo: Repository) -> None: kept = "".join(f"def keep{i}(value: int) -> int:\n return value\n" for i in range(8)) source = kept + "def read(value): ...\n" diff --git a/tools/debt_ratchet.py b/tools/debt_ratchet.py index 4237a9809e..e8aea938a9 100644 --- a/tools/debt_ratchet.py +++ b/tools/debt_ratchet.py @@ -154,6 +154,11 @@ def owns(cls, key: str) -> bool: def blocks(cls, delta: dict[str, int]) -> bool: return any(value > 0 for key, value in delta.items() if cls.owns(key)) + @classmethod + def grew(cls, key: str, grown: int) -> str: + """The line reporting one key's growth.""" + return f"grew {key} +{grown}" + @classmethod def run_by(cls) -> type[Measure]: """The measure a comparison runs to take this one's measurements.""" @@ -688,7 +693,21 @@ def combine(cls, results: list, repo: Path, base: str, head: str, changed: set[s return before, after -class Unformatted(CheckedRevision): +class Advisory(Measure): + """Growth that restates no fact, such as a lint convention or layout: the ratchet warns + on the file and never fails on it. Only measures of restated facts block a merge.""" + + @classmethod + def blocks(cls, delta: dict[str, int]) -> bool: + return False + + @classmethod + def grew(cls, key: str, grown: int) -> str: + path = key.partition(":")[2] + return f"::warning file={path}::{cls.__name__} +{grown} (warns, never blocks)" + + +class Unformatted(Advisory, CheckedRevision): """1 when Black would rewrite the file: a formatted file stays formatted.""" finding = re.compile(r"^would reformat (.+)$", re.MULTILINE) @@ -732,7 +751,7 @@ def findings(cls, repo: Path, ref: str, head: str, root: str, selected: list[str return side, Counter(found) -class LintFindings(CheckedRevision): +class LintFindings(Advisory, CheckedRevision): """Ruff diagnostics under the selected rules.""" finding = re.compile(r"^(.+?\.py):\d+:\d+: ", re.MULTILINE) @@ -2630,9 +2649,10 @@ def main() -> int: print(json.dumps(FieldCodec.project(result, "report"), indent=2), flush=True) # The failing step's last lines become check annotations (.github/run-step.sh merges # stderr), so what grew goes to stderr last; stdout stays the one JSON report. - for name, grown in sorted(result.delta.items()): - if grown > 0: - print(f"grew {name} +{grown}", file=sys.stderr) + for measure in Measure.members_with(Measure): + for key, grown in sorted(result.delta.items()): + if grown > 0 and measure.owns(key): + print(measure.grew(key, grown), file=sys.stderr) for name, sites in sorted(result.found.items()): print(f"found {name} at {' '.join(f'{site.path}:{site.line}' for site in sites)}", file=sys.stderr)