feat(docs): self-host the repo-derived README badges - #243
Open
cdeust wants to merge 2 commits into
Open
Conversation
Behavior-preserving. A second generator (generate_repo_badges.py, next commit) needs the same shields-style geometry the MCP Toplist refresher already had, so the geometry moves to a sibling module rather than being copied. What stays in the refresher is what makes that badge THAT badge: the bar icon, the banner palette, and the wording of its claim. Path-based sibling import, following the launcher.py/launcher_deps*.py idiom already in scripts/: it resolves identically whether the file runs as a script (script dir already on sys.path) or is loaded through importlib.util.spec_from_file_location by a test, which is how tests_py loads every script gate. XML escaping moves INTO the renderer rather than staying at each call site. The one bug this code has ever shipped was an unescaped '<' from the "Top <0.1%" tier making the badge unparseable XML; centralising the escape means a new generator cannot reintroduce it by forgetting. Proof of preservation: render_badge(964/81919, 2026-07-28) is byte-identical to the committed assets/badge-mcp-toplist.svg, and the existing 51 tests pass unchanged (51 passed in 0.82s). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw
The README's five repo-derived badges were remote images: a third-party request fired on every view, and a claim about this repository that its host could restate at any time with no commit here. Each of those five figures is determined by the working tree, so a committed SVG can always be made true again by regenerating it, with no network at all. scripts/generate_repo_badges.py renders them from the canonical readers already used by the doc-claims gate — not reimplementations of them, since a badge that disagreed with the gate is the exact drift this prevents. It runs as a blocking check on every push and PR: `--check` in the static job for the four file-derived badges, and `--check --test-count` in the test job, where the collected count is known. Converting the badges alone would have silently disabled two gates. check_doc_claims.py matched the version and test figures by regex over the shields.io URLs; against the new README those patterns find nothing and both checks pass quietly. They now read each figure from the committed SVG's <title> via check_badge(), which fails closed on a missing file and on a title it cannot match, and check_no_hotlinked_badges() rejects any reintroduced img.shields.io so the reversion is loud. Two badges are deliberately left live. The CI status badge reflects the last run on main; a committed copy would assert "passing" while main was broken, which is false by construction rather than merely stale, and it is GitHub-hosted so it is not a third-party beacon. The OpenSSF badge is an external body's live assessment that can be downgraded without a commit here, and .bestpractices.json justifies displaying their image. badge_render.render() now parses its own output and raises BadgeMarkupError on anything not well-formed: a provenance comment containing "--" is illegal inside an XML comment and produced five unparseable badges during this work. The guard sits on the finished artifact rather than on any one way to break it. Also syncs the advertised suite size 6348 -> 6373 across 11 sites in 6 files, the count the 51 new tests move it to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw
cdeust
force-pushed
the
docs/self-hosted-badges
branch
from
July 28, 2026 21:47
f22fa65 to
18d4505
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on
docs/mcp-toplist-badge(#242) — review that one first; GitHub retargets this tomainwhen #242 merges.The README's five repo-derived badges were remote images: a third-party request on every view, and a claim about this repository its host could restate with no commit here. Every one of those figures is determined by the working tree, so a committed SVG can always be made true again by regenerating it — with no network at all.
Two commits
93dd60erefactor(scripts): extract the shared badge SVG renderer — behavior-preserving.assets/badge-mcp-toplist.svgis byte-identical before and after; its 51 tests pass unchanged.f22fa65feat(docs): self-host the repo-derived README badges — the change itself.What it does
scripts/generate_repo_badges.pyrenders license / python / tests / references / version from the canonical readerscheck_doc_claims.pyalready uses — not reimplementations, since a badge that disagreed with the gate would be the exact drift this exists to prevent.--checkin the static-claims job (four file-derived badges, no network, no suite) and--check --test-countin the test job, where the collected count is known.check_doc_claims.pymatched the version and test figures by regex over the shields.io URLs. Against the new README those patterns match nothing and both checks would have passed quietly — a gate that reports success while reading nothing. They now read each figure from the committed SVG's<title>viacheck_badge(), which fails closed on a missing file and on an unmatched title;check_no_hotlinked_badges()rejects any reintroducedimg.shields.ioso reverting this is loud.badge_render.render()now parses its own output and raisesBadgeMarkupErroron anything not well-formed. A provenance comment containing--is illegal inside an XML comment and produced five unparseable badges during this work; the guard is on the finished artifact rather than on any one way to break it.Two badges deliberately NOT converted
main. A committed copy would assert "passing" whilemainwas broken — false by construction, not merely stale. Also GitHub-hosted, so not a third-party beacon: the reader already loaded the page from that origin..bestpractices.jsonadditionally justifies displaying their image.The MCP Toplist badge sits between the two and is committed, because it carries an explicit "as of <month>" stamp: a stale copy is still a true statement about a point in time.
Say the word if you want either of the two converted anyway.
Completion Ledger (§13.2)
Path enumeration — every branch and error arm in the diff
canonical_licence()— foundtest_licence_comes_from_the_packaging_metadatacanonical_licence()— both patterns miss →ClaimErrortest_a_missing_licence_is_an_error_not_a_guesscanonical_python_floor()— foundtest_python_floor_comes_from_requires_pythoncanonical_python_floor()— miss →ClaimErrortest_a_missing_python_floor_is_an_error_not_a_guessbuild_badges()—test_countpresent → 5 badgestest_every_badge_states_its_canonical_valuebuild_badges()—test_count is None→ tests badge omitted, not guessedtest_without_a_live_count_no_test_badge_is_builtRepoBadge.render()— provenance/derivation embedded in the filetest_every_badge_carries_its_derivation_in_the_fileRepoBadge.render()—<title>states the claimtest_the_title_states_the_claim_for_screen_readerswrite()— target absent or differs → written, returns Truetest_a_drifted_count_is_rewritten_on_a_real_runwrite()— target identical → no write, returns Falsetest_generation_then_check_is_cleanstale()— file missingtest_a_missing_badge_is_reported_and_nothing_is_writtenstale()— content differstest_a_hand_edited_badge_is_reported_as_stalestale()— current → Nonetest_generation_then_check_is_cleanmain()—--checkwith failures → exit 1test_a_hand_edited_badge_is_reported_as_stalemain()—--checkclean → exit 0test_generation_then_check_is_cleanmain()— unreadable canonical source → exit 2, distinct from drifttest_an_unreadable_canonical_source_exits_two_not_onemain()—--checkmust not repair what it rejectstest_check_does_not_repair_the_file_it_rejectsmain()— write mode, changed/unchanged talliestest_a_drifted_count_is_rewritten_on_a_real_runcheck_badge()— file missing → fails closedtest_a_missing_badge_file_fails_closedcheck_badge()— title unmatched → fails closedtest_a_badge_without_a_version_title_fails_closedcheck_badge()— figure disagreestest_stale_committed_badge_is_reportedcheck_badge()— agrees → no failurecheck_doc_claims.pyrun, cleancheck_no_hotlinked_badges()— hotlink found, with line numbertest_a_reintroduced_shields_hotlink_is_reportedcheck_no_hotlinked_badges()— nonetest_the_readme_hotlinks_no_shields_badgebadge_render.render()— well-formed → returnedtest_every_badge_is_well_formed_xmlbadge_render.render()—--in provenance →BadgeMarkupErrortest_a_double_hyphen_in_provenance_is_refusedbadge_render.render()— markup-bearing text escapedtest_markup_bearing_text_is_escapedtest_committed_badges_agree_with_the_repository,test_every_committed_badge_on_disk_is_well_formedtest_the_readme_shows_the_committed_filestest_the_live_status_badges_stay_live§13.1 checklist
A. Correctness & behavior
6373 passed, 30 subtests passed in 174.77slocally; 108/108 across the three badge test files.Nonetest count, markup-bearing text,--in a comment, drifted figure — each row above.ClaimErrormessages, exit 1 vs exit 2, the specific gate message. No error arm is unexercised.--checknever writes (test_check_does_not_repair_the_file_it_rejects); generation is idempotent (test_generation_then_check_is_clean).write()no-ops when content matches.B. Concurrency — N/A: single-shot synchronous scripts, no shared mutable state, no async, no external waits.
C. Resources & performance
Path.read_text/write_text; no handles held, no temp artifacts. Tests use per-test temp dirs.D. Security
saxutils.escapeand the result is re-parsed.E. Interfaces & compatibility
check_doc_claims.py's CLI is unchanged..github/workflows/ci.yml(both jobs, wired in this PR),CONTRIBUTING.mdandCLAUDE.md(doc-claims workflow, updated),refresh_mcp_toplist_badge.py(sharesbadge_render, output byte-identical — verified in commit 1).F. Observability & operations
run scripts/generate_repo_badges.py); asserted by the check-mode tests. Success printsbadges OK (N checked).test_without_a_live_count_no_test_badge_is_built.G. Tests
--comment →test_a_double_hyphen_in_provenance_is_refused; silently-disabled gate →test_a_missing_badge_file_fails_closed+test_a_badge_without_a_version_title_fails_closed._FakeRepo+ per-test temp dirs, no fixed paths, no sleeps, no ordering dependence. Full suite green at default parallelism.test_the_readme_hotlinks_no_shields_badge,test_the_live_status_badges_stay_live,test_check_does_not_repair_the_file_it_rejects,test_without_a_live_count_no_test_badge_is_built.ruff check+ruff format --checkclean on all touched files.H. Code quality & delivery
RepoBadgeis data + its own rendering, one badge per instance; adding a badge is one list entry, no edit towrite/stale/main(OCP). Layering: the generator depends on the canonical readers, never the reverse. Sizes: generator 261 ln, largest function 24 ln, ≤2 params, ≤2 nesting levels. No magic numbers — the one measured constant (5.79 px/char) is sourced inbadge_render.pywith its measurement date.check_doc_claims.py,refresh_mcp_toplist_badge.py); one language per file.README.md,CONTRIBUTING.md,CLAUDE.md,docs/ASSURANCE-CASE.md,.bestpractices.jsonupdated in the same PR.badge_render.render()returning malformed XML (guard + regression test); the stale 6348 count in 11 sites across 6 files (synced). No unfixed, un-issued seen defect remains.Standing caveat (unchanged from #242)
BADGE_REFRESH_TOKENis not set, so the monthly MCP-toplist refresh PRs will not trigger required checks until a PAT withreposcope is added. Unrelated to this diff; restated so it does not get lost.🤖 Generated with Claude Code
https://claude.ai/code/session_01BJt9KN23rUNQzwoJv4jRWw