From 5265cfa7caaae67dcfca538582fa380dca8f5f47 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 24 Oct 2026 23:17:32 -0400 Subject: [PATCH 1/3] DonePhaseBroken counts each broken site by its phase and site Main's DonePhaseBroken spelled each site "Phase::site" through Measure.key and split it back with partition to word the growth line. #3151 deletes Measure.key, so the merge was textually clean and the ratchet would raise AttributeError. PhaseSiteCount carries the phase class and the site as fields, and the count words its own growth line; Measure.grew asks it. The ratchet's Repository fixture now carries the origin remote every real checkout has, and the DonePhaseBroken test's own copy is deleted. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013ejYrMLnLWah8wCDDcb57V --- tests/guards/test_debt_ratchet.py | 2 +- tools/debt_ratchet.py | 34 ++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/guards/test_debt_ratchet.py b/tests/guards/test_debt_ratchet.py index 46bf12b472..54891a576f 100644 --- a/tests/guards/test_debt_ratchet.py +++ b/tests/guards/test_debt_ratchet.py @@ -135,6 +135,7 @@ def __init__(self, path: Path, root: str): self.path = path self.root = root self.git("init", "-q") + self.git("remote", "add", "origin", str(SOURCE)) def git(self, *args: str) -> str: return subprocess.check_output( @@ -339,7 +340,6 @@ def test_a_done_phase_fails_the_change_that_breaks_it(tmp_path: Path) -> None: objects = subprocess.check_output(["git", "-C", str(SOURCE), "rev-parse", "--path-format=absolute", "--git-common-dir"], text=True).strip() (tmp_path / ".git/objects/info/alternates").write_text(f"{objects}/objects\n") - repo.git("remote", "add", "origin", str(SOURCE)) owned = "class Owned:\n pass\n" base = repo.commit({"agent_comms/owned.py": owned}) head = repo.commit({"agent_comms/owned.py": owned + "\n\ndef owns(value):\n" diff --git a/tools/debt_ratchet.py b/tools/debt_ratchet.py index 9f9c0c92a7..146c7b5cc6 100644 --- a/tools/debt_ratchet.py +++ b/tools/debt_ratchet.py @@ -162,8 +162,8 @@ def blocks(cls, delta: Counts) -> bool: @classmethod def grew(cls, key: MeasureCount, grown: int) -> str: - """The line reporting one count's growth.""" - return f"grew {key.text} +{grown}" + """The line reporting one count's growth; the count words it.""" + return key.grew(grown) @classmethod def run_by(cls) -> type[Measure]: @@ -225,6 +225,10 @@ def warning(self, text: str) -> str: """A check annotation for this count; a total annotates no file.""" return f"::warning::{text}" + def grew(self, grown: int) -> str: + """The line saying this count grew.""" + return f"grew {self.text} +{grown}" + @dataclass(frozen=True) class FileCount(MeasureCount): @@ -258,6 +262,21 @@ def text(self) -> str: return f"{super().text}::{self.name}" +@dataclass(frozen=True) +class PhaseSiteCount(MeasureCount): + """One site where a done plan phase's evidence fails.""" + + phase: type[PlanPhase] + site: str + + @property + def text(self) -> str: + return f"{super().text}:{self.phase.__name__}::{self.site}" + + def grew(self, grown: int) -> str: + return f"done plan phase {self.phase.__name__} no longer holds: {self.site}" + + # One revision's counts, each under the record of what it counts. type Counts = dict[MeasureCount, int] @@ -2576,25 +2595,20 @@ def parts(cls, repo: Path, base: str, head: str, changed: set[str], @classmethod def combine(cls, results: list, repo: Path, base: str, head: str, changed: set[str], - root: str) -> tuple[dict[str, int], dict[str, int]]: + root: str) -> tuple[Counts, Counts]: """A failing pull request pays for reading its sites; a passing one never reads them.""" if not results: return {}, {} before, after = results reading = PlanReading((), MainTree(Repo.of(repo), revision(repo, head))) - broken = { - cls.key(f"{phase.__name__}::{site}"): 1 + broken: Counts = { + PhaseSiteCount(cls, phase, site): 1 for phase in PlanPhase.members_with(PlanPhase) if phase.__name__ in after - before for site in phase.evidence.sites(reading) or (reading.describe(phase.evidence),) } return dict.fromkeys(broken, 0), broken - @classmethod - def grew(cls, key: str, grown: int) -> str: - phase, _, site = key.partition(":")[2].partition("::") - return f"done plan phase {phase} no longer holds: {site}" - @classmethod def failing(cls, repo: Path, commit: str) -> frozenset[str]: """The name of each phase not done at a commit.""" From 98e91cce9e38b093361de08ee4fe2cad669cd881 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 24 Oct 2026 23:21:54 -0400 Subject: [PATCH 2/3] DonePhaseBroken finds its source root with inspect.getfile, which raises on a missing file An empty __file__ fallback resolved to the working directory, so the measure would silently run over nothing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013ejYrMLnLWah8wCDDcb57V --- tools/debt_ratchet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/debt_ratchet.py b/tools/debt_ratchet.py index f88e487616..e8365da57f 100644 --- a/tools/debt_ratchet.py +++ b/tools/debt_ratchet.py @@ -2590,7 +2590,7 @@ class DonePhaseBroken(Measure): @classmethod def parts(cls, repo: Path, base: str, head: str, changed: set[str], root: str) -> tuple[Callable[[], object], ...]: - declared = Path(sys.modules[PlanPhase.__module__].__file__ or "").resolve() + declared = Path(inspect.getfile(PlanPhase)).resolve() if root != f"{declared.relative_to(REPOSITORY).parts[0]}/": return () return tuple(partial(cls.failing, repo, revision(repo, ref)) for ref in (base, head)) From e83c1ad40d4194bb151d89e95bdae3dab1eaed7d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 24 Oct 2026 23:26:03 -0400 Subject: [PATCH 3/3] StoreLockCalls test reads its counts through Measure.at #3194 merged a minute after #3151 deleted Measure.key, so main's ratchet suite raised AttributeError in both roots. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013ejYrMLnLWah8wCDDcb57V --- tests/guards/test_debt_ratchet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/guards/test_debt_ratchet.py b/tests/guards/test_debt_ratchet.py index d511346023..9c725172c9 100644 --- a/tests/guards/test_debt_ratchet.py +++ b/tests/guards/test_debt_ratchet.py @@ -148,8 +148,8 @@ def test_a_new_store_lock_call_fails_on_its_own_file_with_its_line(repo): " with store.locked():\n pass\n"}) status, report = repo.compare(base, head, StoreLockCalls) assert status == 1 - grown = StoreLockCalls.key(f"{repo.root}/other.py") - assert report["delta"] == {StoreLockCalls.key(f"{repo.root}/turns.py"): -1, grown: 1} + grown = StoreLockCalls.at(f"{repo.root}/other.py").text + assert report["delta"] == {StoreLockCalls.at(f"{repo.root}/turns.py").text: -1, grown: 1} assert report["found"] == {grown: [{"path": f"{repo.root}/other.py", "line": line} for line in (2, 4)]}