From 69a6fccddfad9ef443d39bd8570bcb04b68e9083 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 21:06:25 -0700 Subject: [PATCH 1/4] Report a retired badge written inline, not only one defined readme_shield_findings scanned reference definitions alone for a retired badge service, so a repo writing it as an inline image rendered the dead badge and reported nothing. The comment above the loop claimed it was scanned "wherever it sits", and the selftest case was named "reported wherever it sits" while testing the reference form only, so the claim and its evidence agreed with each other and not with the code. shield_endpoints already resolves both forms, and its own docstring records this exact lesson for every other shield. This is the one site that had not taken it: the link-naming loops read definitions because their rule is about definitions, and the two shield-presence sites already call shield_endpoints. A definition is still reported where nothing renders it, since a retired service left in the reference block is removed with the badge rather than after it, and a definition rendered by an image is one finding rather than two. Measured against all 22 cataloged READMEs: shield findings stay at 7 and no verdict moves, so no false positive. All three repos carrying the badge use the reference form, so this closes a blind spot rather than catching a live miss. The new cases were A/B tested against the pre-fix function: the inline case goes 0 -> 1 and the fenced sample stays 0. Raised by Copilot as a suppressed finding on promotion pull request #635. Co-Authored-By: Claude Opus 5 (1M context) --- spec/audit.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/audit.py b/spec/audit.py index 614ed8c3..1c2aa4b9 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -790,10 +790,17 @@ def readme_shield_findings(text, model, entry): defs = {m.group(1): m.group(2) for m in _LINK_DEF.finditer(unfenced_text(text))} # A retired badge service is scanned across the whole document rather than per section, since a dead badge is wrong wherever it sits. # It renders broken rather than absent, which a visitor reads as a failing build rather than as a stale badge. + # Both forms are read, since reading definitions alone made an inline badge invisible rather than wrong, which is the reading shield_endpoints already takes for every other shield. + # A definition is reported even where nothing renders it, because a retired service left in the reference block is removed with the badge rather than after it. + rendered = shield_endpoints(unfenced_text(text), defs) for dep in model.get("deprecatedShields", []): + defined = set() for ref, url in sorted(defs.items()): if dep["match"] in url: + defined.add(url) findings.append(("LETTER", f"readme: `[{ref}]` renders {dep['label']}, which is retired - {dep['reason']} (spec/readme-structure.md)")) + for url in sorted({u for u in rendered if dep["match"] in u} - defined): + findings.append(("LETTER", f"readme: an inline image renders {dep['label']}, which is retired - {dep['reason']} (spec/readme-structure.md)")) targets = {(p.get("target") if isinstance(p, dict) else p) for p in entry.get("publish", [])} secrets = set(entry.get("requiredSecrets", [])) want = [] @@ -1736,6 +1743,11 @@ def _selftest(): ("a fenced badge sample does not satisfy a required shield", conformant.replace("[![GitHub Release][c]][x]\\\n", "```md\n[![GitHub Release][c]][x]\n```\n"), {}, 1), ("a fenced license shield does not trip the exclusive rule", conformant.replace("## Overview", "```md\n![License][license-shield]\n```\n\n## Overview"), {}, 0), ("a retired badge service is reported wherever it sits", conformant.replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1), + # The case above named every placement and read only the reference block, so an inline badge was invisible rather than wrong. + # That is the reading shield_endpoints already takes for every other shield, and this one had not taken it. + ("a retired badge written inline is reported", conformant.replace("[![Last Commit][b]][x]", "[![Last Commit][b]][x]\\\n![Last Build](https://byob.yarr.is/o/r/lastbuild)"), {}, 1), + ("a retired badge defined and rendered is one finding, not two", conformant.replace("[![Last Commit][b]][x]", "[![Last Commit][b]][x]\\\n![Last Build][last-build-shield]").replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1), + ("a retired badge shown as a fenced sample is markup", conformant.replace("## Overview", "```md\n![Last Build](https://byob.yarr.is/o/r/lastbuild)\n```\n\n## Overview"), {}, 0), ("the pre-release shield is told from the release shield by its query", conformant.replace("?include_prereleases&label=GitHub%20Pre-Release", "?label=Another%20Release"), {}, 1), # The license shield is an ordinary member of the base class, addressed to a different section. ("the license shield in the closing License section", conformant, {}, 0), From a571d792151bceeffd7ea295459e930af3cf64bc Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 21:11:53 -0700 Subject: [PATCH 2/4] Say what a retired definition does, and unfence the document once Two findings from round 1, both accepted. The finding text claimed every matching definition "renders" the retired badge, while the comment directly above it said a definition is reported even where nothing renders it. The two contradicted each other, and the message was the wrong one: telling a maintainer a badge renders sends them looking for it on a page that does not show it. The wording now follows which of the three shapes it is, and the selftest asserts the wording rather than only the count, since a count cannot tell one wording from another. unfenced_text was computed twice back to back. Once now, which is the cheaper read and also the safer one: the definitions and the rendered images can no longer be taken from two different views of the document. Measured against all 22 cataloged READMEs: shield findings stay at 7 and every live retired-badge finding keeps the word "renders", because all three repos carrying the badge do render it. The new wording fires only on the unrendered case, A/B tested against the pre-fix function, which says "renders" there and is wrong to. Co-Authored-By: Claude Opus 5 (1M context) --- spec/audit.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index 1c2aa4b9..314c695c 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -787,18 +787,22 @@ def readme_shield_findings(text, model, entry): repo can be measured against. """ findings = [] - defs = {m.group(1): m.group(2) for m in _LINK_DEF.finditer(unfenced_text(text))} + # One unfenced view serves both passes, so the definitions and the rendered images cannot be read from different documents. + unfenced = unfenced_text(text) + defs = {m.group(1): m.group(2) for m in _LINK_DEF.finditer(unfenced)} # A retired badge service is scanned across the whole document rather than per section, since a dead badge is wrong wherever it sits. # It renders broken rather than absent, which a visitor reads as a failing build rather than as a stale badge. # Both forms are read, since reading definitions alone made an inline badge invisible rather than wrong, which is the reading shield_endpoints already takes for every other shield. # A definition is reported even where nothing renders it, because a retired service left in the reference block is removed with the badge rather than after it. - rendered = shield_endpoints(unfenced_text(text), defs) + # Which of the three it is decides the wording, since a definition nothing renders is not rendering anything and saying so sends the reader looking for a badge that is not on the page. + rendered = shield_endpoints(unfenced, defs) for dep in model.get("deprecatedShields", []): defined = set() for ref, url in sorted(defs.items()): if dep["match"] in url: defined.add(url) - findings.append(("LETTER", f"readme: `[{ref}]` renders {dep['label']}, which is retired - {dep['reason']} (spec/readme-structure.md)")) + verb = f"renders {dep['label']}" if url in rendered else f"defines {dep['label']} and nothing renders it" + findings.append(("LETTER", f"readme: `[{ref}]` {verb}, which is retired - {dep['reason']} (spec/readme-structure.md)")) for url in sorted({u for u in rendered if dep["match"] in u} - defined): findings.append(("LETTER", f"readme: an inline image renders {dep['label']}, which is retired - {dep['reason']} (spec/readme-structure.md)")) targets = {(p.get("target") if isinstance(p, dict) else p) for p in entry.get("publish", [])} @@ -1748,6 +1752,10 @@ def _selftest(): ("a retired badge written inline is reported", conformant.replace("[![Last Commit][b]][x]", "[![Last Commit][b]][x]\\\n![Last Build](https://byob.yarr.is/o/r/lastbuild)"), {}, 1), ("a retired badge defined and rendered is one finding, not two", conformant.replace("[![Last Commit][b]][x]", "[![Last Commit][b]][x]\\\n![Last Build][last-build-shield]").replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1), ("a retired badge shown as a fenced sample is markup", conformant.replace("## Overview", "```md\n![Last Build](https://byob.yarr.is/o/r/lastbuild)\n```\n\n## Overview"), {}, 0), + # The wording follows which of the three shapes it is, since a definition nothing renders is not rendering anything. + # Saying it renders sends the reader looking for a badge that is not on the page. + ("an unrendered definition says so rather than claiming a render", conformant.replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1, "nothing renders it"), + ("a rendered definition says renders", conformant.replace("[![Last Commit][b]][x]", "[![Last Commit][b]][x]\\\n![Last Build][last-build-shield]").replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1, "renders the byob"), ("the pre-release shield is told from the release shield by its query", conformant.replace("?include_prereleases&label=GitHub%20Pre-Release", "?label=Another%20Release"), {}, 1), # The license shield is an ordinary member of the base class, addressed to a different section. ("the license shield in the closing License section", conformant, {}, 0), @@ -1759,12 +1767,18 @@ def _selftest(): ("inline shields count as present", inline_all.replace("PLACEHOLDER-a", "github/actions/workflow/status/o/r").replace("PLACEHOLDER-b", "github/last-commit/o/r").replace("PLACEHOLDER-c", "github/v/release/o/r").replace("PLACEHOLDER-d", "github/v/release/o/r?include_prereleases").replace("PLACEHOLDER-license-shield", "github/license/o/r"), {}, 0), ("an inline shield in the wrong section is still exclusive", conformant.replace("## Overview", "![License](https://img.shields.io/github/license/o/r)\n\n## Overview"), {}, 1), ] - for label, text, ent, wantn in shield_cases: + # A case may carry a fifth element, a substring the finding text must contain. + # A count alone cannot tell one wording from another, and the wording is the whole subject of some of these cases. + for case in shield_cases: + label, text, ent, wantn = case[:4] + want_text = case[4] if len(case) > 4 else None got = readme_shield_findings(text, rm, ent) - if len(got) != wantn: + good = len(got) == wantn and (want_text is None or any(want_text in t for _, t in got)) + if not good: ok = False - print(f" {'ok ' if len(got) == wantn else 'FAIL'} want={wantn} got={len(got)} readme shields: {label}") - if len(got) != wantn: + shown = f"want={wantn}" if want_text is None else f"want={wantn}+'{want_text}'" + print(f" {'ok ' if good else 'FAIL'} {shown} got={len(got)} readme shields: {label}") + if not good: for _, t in got: print(f" {t}") From 0d811201167aa5bf137ac61b86bfbf52af26a416 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 21:16:47 -0700 Subject: [PATCH 3/4] Attribute a retired render by reference name, never by URL Round 2 found the fourth shape, and it was a mis-attribution rather than a wording gap: the same endpoint rendered inline leaves a reference definition unused, so testing whether the URL appears anywhere in the rendered set credited that render to a reference nothing uses. The message then told a maintainer that `[ref]` renders the badge when deleting `[ref]` would leave the badge on the page. Attribution is now by reference name, from the image-reference uses, and the URL decides only between the two unused cases: rendered elsewhere, or rendered nowhere at all. Measured against all 22 cataloged READMEs: shield findings stay at 7 and no verdict or wording moves, since no repo carries the shape. A/B on a fixture that does: the previous code says "renders" and is wrong to, where this says "defines it and it is rendered elsewhere". Co-Authored-By: Claude Opus 5 (1M context) --- spec/audit.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index 314c695c..b287c38a 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -794,14 +794,21 @@ def readme_shield_findings(text, model, entry): # It renders broken rather than absent, which a visitor reads as a failing build rather than as a stale badge. # Both forms are read, since reading definitions alone made an inline badge invisible rather than wrong, which is the reading shield_endpoints already takes for every other shield. # A definition is reported even where nothing renders it, because a retired service left in the reference block is removed with the badge rather than after it. - # Which of the three it is decides the wording, since a definition nothing renders is not rendering anything and saying so sends the reader looking for a badge that is not on the page. + # Which of the four it is decides the wording, since a definition nothing renders is not rendering anything and saying so sends the reader looking for a badge that is not on the page. + # Attribution is by reference name and never by URL: the same endpoint rendered inline leaves this definition unused, so reading the URL alone would credit a render to a reference nothing uses. rendered = shield_endpoints(unfenced, defs) + used_refs = {m.group(1) for m in _MD_IMAGE_REF.finditer(unfenced)} for dep in model.get("deprecatedShields", []): defined = set() for ref, url in sorted(defs.items()): if dep["match"] in url: defined.add(url) - verb = f"renders {dep['label']}" if url in rendered else f"defines {dep['label']} and nothing renders it" + if ref in used_refs: + verb = f"renders {dep['label']}" + elif url in rendered: + verb = f"defines {dep['label']} and it is rendered elsewhere" + else: + verb = f"defines {dep['label']} and nothing renders it" findings.append(("LETTER", f"readme: `[{ref}]` {verb}, which is retired - {dep['reason']} (spec/readme-structure.md)")) for url in sorted({u for u in rendered if dep["match"] in u} - defined): findings.append(("LETTER", f"readme: an inline image renders {dep['label']}, which is retired - {dep['reason']} (spec/readme-structure.md)")) @@ -1756,6 +1763,8 @@ def _selftest(): # Saying it renders sends the reader looking for a badge that is not on the page. ("an unrendered definition says so rather than claiming a render", conformant.replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1, "nothing renders it"), ("a rendered definition says renders", conformant.replace("[![Last Commit][b]][x]", "[![Last Commit][b]][x]\\\n![Last Build][last-build-shield]").replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1, "renders the byob"), + # The same endpoint rendered inline leaves this definition unused, so attributing by URL would credit the render to a reference nothing uses. + ("an unused definition beside an inline render is not credited with it", conformant.replace("[![Last Commit][b]][x]", "[![Last Commit][b]][x]\\\n![Last Build](https://byob.yarr.is/o/r/lastbuild)").replace("[license-shield]: https://img.shields.io/github/license/o/r\n", "[license-shield]: https://img.shields.io/github/license/o/r\n[last-build-shield]: https://byob.yarr.is/o/r/lastbuild\n"), {}, 1, "rendered elsewhere"), ("the pre-release shield is told from the release shield by its query", conformant.replace("?include_prereleases&label=GitHub%20Pre-Release", "?label=Another%20Release"), {}, 1), # The license shield is an ordinary member of the base class, addressed to a different section. ("the license shield in the closing License section", conformant, {}, 0), From 5877b9ea284d4d886dd750db91291b421b136c1d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 21:21:00 -0700 Subject: [PATCH 4/4] Hold the rendered endpoints as a set, and name both by namespace Round 3 clarity finding, accepted. shield_endpoints returns a list and every use here is a membership test or a set difference, so it is converted once. The names now say which namespace each set holds, rendered_urls and defined_urls against used_refs, since this block reasons about reference names and URLs a line apart and the bug the previous round fixed was exactly a URL standing in for a reference name. No behavior change: the selftest passes unchanged and the corpus reports the same 7 shield findings with no set moving. Co-Authored-By: Claude Opus 5 (1M context) --- spec/audit.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index b287c38a..de4faec5 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -796,21 +796,22 @@ def readme_shield_findings(text, model, entry): # A definition is reported even where nothing renders it, because a retired service left in the reference block is removed with the badge rather than after it. # Which of the four it is decides the wording, since a definition nothing renders is not rendering anything and saying so sends the reader looking for a badge that is not on the page. # Attribution is by reference name and never by URL: the same endpoint rendered inline leaves this definition unused, so reading the URL alone would credit a render to a reference nothing uses. - rendered = shield_endpoints(unfenced, defs) + # A set rather than the list shield_endpoints returns, since every use here is membership or a difference, and the names say which of the two namespaces each holds. + rendered_urls = set(shield_endpoints(unfenced, defs)) used_refs = {m.group(1) for m in _MD_IMAGE_REF.finditer(unfenced)} for dep in model.get("deprecatedShields", []): - defined = set() + defined_urls = set() for ref, url in sorted(defs.items()): if dep["match"] in url: - defined.add(url) + defined_urls.add(url) if ref in used_refs: verb = f"renders {dep['label']}" - elif url in rendered: + elif url in rendered_urls: verb = f"defines {dep['label']} and it is rendered elsewhere" else: verb = f"defines {dep['label']} and nothing renders it" findings.append(("LETTER", f"readme: `[{ref}]` {verb}, which is retired - {dep['reason']} (spec/readme-structure.md)")) - for url in sorted({u for u in rendered if dep["match"] in u} - defined): + for url in sorted({u for u in rendered_urls if dep["match"] in u} - defined_urls): findings.append(("LETTER", f"readme: an inline image renders {dep['label']}, which is retired - {dep['reason']} (spec/readme-structure.md)")) targets = {(p.get("target") if isinstance(p, dict) else p) for p in entry.get("publish", [])} secrets = set(entry.get("requiredSecrets", []))