From 1aff5ae854924fe1d94c2a4eb24d8813e139c163 Mon Sep 17 00:00:00 2001 From: Zain Dana Harper <17142659+HarperZ9@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:55:59 -0700 Subject: [PATCH] Correct a false claim in .gitattributes, and control two uncontrolled gates The .gitattributes comment said a gate compares the committed bytes to a fresh render, and that normalizing line endings per platform would make that comparison depend on who checked the repository out. Neither is true. Both sides of the comparison are read with Path.read_text, which applies universal-newline translation, so the comparison passes on a CRLF checkout with or without the pin. The pin is still worth keeping, for a narrower reason. The receipt records each drawing's byte count and SHA-256 read from the checked-out file, and those two numbers do depend on the checkout. The comment now says that instead. The test file already proved the outcome-box gate can fail, by pointing ART at a throwaway spec whose note is far too wide. The wrapper gate and the tagline gate had nothing of the kind: twelve green lines said they ran. The three geometry checks now take their spec list as an argument instead of reading the loaded set directly, and art.the_gate_can_fail feeds them one spec built to break all three at once: a note too long for the wrapper, a tagline past the rule, and an outcome band with one over-wide label and one over-long note. It asserts the exact count of complaints, so a check that half-works is caught too. Verified against a deliberately weakened copy of the module, where the control names the budgets that stopped catching anything. GATES in the test names its gates rather than counting them, so the new one is added there. Co-Authored-By: Claude Opus 5 --- .gitattributes | 6 +++--- tests/test_repo_art.py | 1 + tools/check_repo_art.py | 41 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.gitattributes b/.gitattributes index 18454b2..91648a9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ -# The artwork under docs/art/ is generated, and a gate compares the committed -# bytes to a fresh render. Normalizing line endings per platform would make that -# comparison depend on who checked the repository out. +# The art under docs/art/ is generated. render_repo_art.py writes LF on every +# platform, so pin the checkout to LF as well and each file's byte count and +# SHA-256 stay the same whoever cloned the repository. docs/art/*.svg text eol=lf docs/art/*.json text eol=lf diff --git a/tests/test_repo_art.py b/tests/test_repo_art.py index 2b1e63f..d8e0360 100644 --- a/tests/test_repo_art.py +++ b/tests/test_repo_art.py @@ -25,6 +25,7 @@ "art.every_illustration_is_shown", "art.tagline_stays_inside_its_rule", "art.outcome_fits_its_box", + "art.the_gate_can_fail", ) DRAWINGS = ( diff --git a/tools/check_repo_art.py b/tools/check_repo_art.py index 3d70deb..1cd4559 100644 --- a/tools/check_repo_art.py +++ b/tools/check_repo_art.py @@ -130,8 +130,12 @@ def check_note_survives_the_wrapper(_unused: list[Path]) -> list[str]: """Card notes wrap to three lines and the wrapper drops the rest, so an edited sentence can lose its ending in the drawing while reading fine in the spec.""" + return _notes_the_wrapper_cuts(_loaded()) + + +def _notes_the_wrapper_cuts(specs: list[dict]) -> list[str]: bad = [] - for spec in _loaded(): + for spec in specs: for flow in spec.get("flows", []): for stage in flow["stages"]: drawn = " ".join(FLOW._wrap(stage["note"])) @@ -167,8 +171,12 @@ def check_every_illustration_is_shown(_unused: list[Path]) -> list[str]: def check_tagline_stays_inside_its_rule(_unused: list[Path]) -> list[str]: """The tagline is one unwrapped line under a rule that ends at x=700. Past that it runs on toward the aperture and nothing about the render fails.""" + return _taglines_that_overrun(_loaded()) + + +def _taglines_that_overrun(specs: list[dict]) -> list[str]: bad = [] - for spec in _loaded(): + for spec in specs: tagline = spec["header"]["tagline"] if len(tagline) > TAGLINE_BUDGET: bad.append(f"{len(tagline)} characters runs past the rule: {tagline!r}") @@ -185,8 +193,12 @@ def _outcome_budgets(count: int) -> tuple[int, int]: def check_outcome_fits_its_box(_unused: list[Path]) -> list[str]: """An outcome box is one unwrapped label over one unwrapped note, and neither is clipped, so an over-long note runs into the next box.""" + return _outcomes_that_overflow(_loaded()) + + +def _outcomes_that_overflow(specs: list[dict]) -> list[str]: bad = [] - for spec in _loaded(): + for spec in specs: for flow in spec.get("flows", []): label_budget, note_budget = _outcome_budgets(len(flow["outcomes"])) for item in flow["outcomes"]: @@ -198,6 +210,28 @@ def check_outcome_fits_its_box(_unused: list[Path]) -> list[str]: return bad +# A spec built to break all three geometry budgets at once. Every other check +# here reports clean, which says it ran and not that it works. +CONTROL = [{ + "header": {"tagline": "x" * (TAGLINE_BUDGET + 1)}, + "flows": [{ + "stages": [{"title": "CARD", "note": "word " * 60}], + "outcomes": [{"label": "OK", "note": "x" * 200}, + {"label": "y" * 200, "note": "short"}], + }], +}] + + +def check_the_gate_can_fail(_unused: list[Path]) -> list[str]: + """Feed the three geometry checks input they have to reject.""" + return [f"the gate missed {what}" for caught, what in ( + (len(_notes_the_wrapper_cuts(CONTROL)) == 1, "a truncated note"), + (len(_taglines_that_overrun(CONTROL)) == 1, "a tagline past its rule"), + (len(_outcomes_that_overflow(CONTROL)) == 2, + "an over-wide label and an over-long note"), + ) if not caught] + + CHECKS = [ ("spec.present", check_spec_present), ("art.matches_spec", check_artwork_matches_spec), @@ -211,6 +245,7 @@ def check_outcome_fits_its_box(_unused: list[Path]) -> list[str]: ("art.every_illustration_is_shown", check_every_illustration_is_shown), ("art.tagline_stays_inside_its_rule", check_tagline_stays_inside_its_rule), ("art.outcome_fits_its_box", check_outcome_fits_its_box), + ("art.the_gate_can_fail", check_the_gate_can_fail), ]