From 34b84a27bde8ee7beb4595430d3b46f26183d081 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Wed, 12 Aug 2026 15:23:41 -0700 Subject: [PATCH] Carry which kind of nothing, not a sentence about it dormant_rules computed one of three distinct diagnoses and then rendered it away into prose, so the only way to ask which one it was was to match a substring. Four tests did exactly that. That fails open. Reword a diagnosis for clarity, keep the matched substring by luck, and the test still passes while checking something weaker than it was written to check -- the failure mode is invisible because a passing test looks the same either way. _Dormant now carries `kind` (a Literal, so mypy rejects a typo) and `detail` (the shadower's issue, when there is one). main() renders the sentence from _DORMANT_WHY. The computation no longer knows any wording, so rephrasing a diagnosis is a change to output alone. No behavior change. The two _run_main tests still assert on the rendered EXPLAINED NOTHING lines and still pass, which is what pins the output format across this refactor. Both mutation proofs still fail as they should: swapping the reverted/excluded kinds fails 4 tests, removing the internal _sorted_rules call fails 1. Gates unmoved at 108/0, 90/0 and 1/0. Co-Authored-By: Claude Opus 5 --- tests/v2/test_differential.py | 21 +++++++------ tools/differential/compare.py | 57 +++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/tests/v2/test_differential.py b/tests/v2/test_differential.py index 0daeacb..b576539 100644 --- a/tests/v2/test_differential.py +++ b/tests/v2/test_differential.py @@ -961,8 +961,8 @@ def test_dormant_rules_reports_a_rule_whose_behavior_vanished() -> None: report = compare.dormant_rules( rules, {"fix(a)"}, [("John Smith", {"given"})]) assert report.awake == () - assert [i for i, _ in report.undeclared] == ["fix(b)"] - assert "reverted" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(b)"] + assert report.undeclared[0].kind == "reverted" def test_dormant_rules_names_the_rule_that_shadows_one() -> None: @@ -976,8 +976,9 @@ def test_dormant_rules_names_the_rule_that_shadows_one() -> None: "fields": ["given"]}] report = compare.dormant_rules( rules, {"fix(broad)"}, [("John Smith", {"given"})]) - assert [i for i, _ in report.undeclared] == ["fix(narrow)"] - assert "shadowed by 'fix(broad)'" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(narrow)"] + assert report.undeclared[0].kind == "shadowed" + assert report.undeclared[0].detail == "fix(broad)" def test_dormant_rules_distinguishes_an_excluded_shape() -> None: @@ -988,8 +989,8 @@ def test_dormant_rules_distinguishes_an_excluded_shape() -> None: never = [{"why": "protected", "name_regex": "Smith"}] report = compare.dormant_rules( rules, set(), [("John Smith", {"given"})], never) - assert [i for i, _ in report.undeclared] == ["fix(a)"] - assert "[[never]]" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(a)"] + assert report.undeclared[0].kind == "excluded" def test_dormant_rules_is_silent_about_a_declared_rule() -> None: @@ -1022,8 +1023,8 @@ def test_dormant_rules_names_the_shadower_that_does_the_shadowing() -> None: rules, {"fix(a)", "fix(z)"}, [("Alpha One", {"given"}), ("Zeta One", {"given"}), ("Zeta Two", {"given"}), ("Zeta Three", {"given"})]) - assert [i for i, _ in report.undeclared] == ["fix(idle)"] - assert "shadowed by 'fix(z)'" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["fix(idle)"] + assert report.undeclared[0].detail == "fix(z)" def test_dormant_rules_sorts_before_diagnosing() -> None: @@ -1041,8 +1042,8 @@ def test_dormant_rules_sorts_before_diagnosing() -> None: "fields": ["given"]}] report = compare.dormant_rules( rules, {"specific"}, [("John Smith", {"given"})]) - assert [i for i, _ in report.undeclared] == ["broad"] - assert "shadowed by 'specific'" in report.undeclared[0][1] + assert [d.issue for d in report.undeclared] == ["broad"] + assert report.undeclared[0].detail == "specific" def test_validate_rules_rejects_two_rules_sharing_an_issue() -> None: diff --git a/tools/differential/compare.py b/tools/differential/compare.py index fe3c043..afda334 100644 --- a/tools/differential/compare.py +++ b/tools/differential/compare.py @@ -19,7 +19,7 @@ import tomllib from collections import Counter from pathlib import Path -from typing import NamedTuple +from typing import Literal, NamedTuple HERE = Path(__file__).resolve().parent FIELDS = ("title", "first", "middle", "last", "suffix", "nickname", @@ -639,11 +639,37 @@ def classify(name: str, diff_fields: set[str], return None +#: How each `_Dormant.kind` reads in the report. Here rather than in +#: dormant_rules so the computation never has to know the wording, and +#: rephrasing one is a change to output alone. +_DORMANT_WHY = { + "reverted": ("matched no diffing name -- the behavior it describes " + "may have been reverted"), + "shadowed": "shadowed by {by}", + "excluded": ("every diffing name it matches is refused by a " + "[[never]] exclusion"), +} + + +class _Dormant(NamedTuple): + """One rule that explained nothing, and why. + + `kind` rather than a sentence because the three have three different + fixes, and a caller that wants to tell them apart should not have to + parse prose to do it. The wording lives in main(), which is the only + place that renders it -- so rephrasing a diagnosis stays a change to + output alone, and the tests that pin the DISTINCTION keep working. + """ + issue: str + kind: Literal["reverted", "shadowed", "excluded"] + #: the issue that claimed it, when `kind` is "shadowed"; else "" + detail: str + + class _Dormancy(NamedTuple): """What a run found out about rules that explained nothing.""" - #: (issue, diagnosis) for every rule that explained nothing and does - #: not declare `dormant` - undeclared: tuple[tuple[str, str], ...] + #: every rule that explained nothing and does not declare `dormant` + undeclared: tuple[_Dormant, ...] #: issues declaring `dormant` that explained at least one diff awake: tuple[str, ...] @@ -690,18 +716,16 @@ def dormant_rules(rules: list[dict[str, object]], explained: set[str], continue matched = [(n, d) for n, d in diffing if _entry_matches(rule, n, d)] + winners = Counter( + c for c in (classify(n, d, ordered, exclusions) + for n, d in matched) if c is not None) if not matched: - why = ("matched no diffing name -- the behavior it describes " - "may have been reverted") + undeclared.append(_Dormant(issue, "reverted", "")) + elif winners: + undeclared.append( + _Dormant(issue, "shadowed", winners.most_common(1)[0][0])) else: - winners = Counter( - c for c in (classify(n, d, ordered, exclusions) - for n, d in matched) if c is not None) - why = (f"shadowed by {winners.most_common(1)[0][0]!r}" - if winners else - "every diffing name it matches is refused by a " - "[[never]] exclusion") - undeclared.append((issue, why)) + undeclared.append(_Dormant(issue, "excluded", "")) return _Dormancy(tuple(undeclared), tuple(awake)) @@ -833,8 +857,9 @@ def main() -> int: print(f" {n!r}") print() dormancy = dormant_rules(rules, set(by_issue), diffing, exclusions) - for issue, why in dormancy.undeclared: - print(f"EXPLAINED NOTHING {issue!r}\n {why}") + for dormant in dormancy.undeclared: + print(f"EXPLAINED NOTHING {dormant.issue!r}\n " + f"{_DORMANT_WHY[dormant.kind].format(by=repr(dormant.detail))}") for issue in dormancy.awake: print(f"NO LONGER DORMANT {issue!r}\n it explained a diff in " f"this run, so its `dormant` reason is now false -- remove "