feat(collector): add per-installer badge files (installer-mix v2)#49
Conversation
Captures the brainstorm output for the v2 installer-mix feature: per-package emission of seven new badge JSON files (six individual installers in the v1 allowlist + a pip-family aggregate), v1 hero kept side-by-side, no new config fields, README dogfood row expanded to show all six individual installers, new "Use this service for your own package" section documenting the shields.io endpoint URL pattern for third-party packages. Decisions locked during brainstorm: Form A (4 badges per package displayed), absolute counts (not percentages), bucketing B3 (seven files: six individual + pip-family aggregate), CI filter C1 (non-CI only — same philosophy as v1), no per-installer brand colors, no per-package configurability in v2, v0.2.0 minor bump on the next release after this lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decomposes the spec into 7 bite-sized tasks (TDD per task — failing test, run, implement, run, commit). Tasks 1-4 cover the collector behavior (run_pypinfo signature change, PackageOutcome.counts field, 8-file write loop with pip-family aggregate, _health.json additive expansion). Task 5 covers README updates (dogfood + endpoint instructions). Task 6 covers the CHANGELOG entry. Task 7 covers push + PR open. Each task lists exact files, line numbers, code blocks, commands, and expected output — no placeholders. Self-review passes against the spec: every spec requirement maps to a task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preserves the per-installer breakdown that the BigQuery query already produces (pivot by `ci AND installer` since v0.1.0). v1 summed it into a single int; v2 needs the breakdown for per-installer badges. Six-keyed dict, ordered via new _INSTALLER_NAMES tuple, zero-filled for installers with no rows in the window. No behavior change to the v1 hero badge — _collect_one sums the dict's values to recover the same int before passing to badge.build_payload. Existing tests that asserted on the int return adapt to assert on sum(result.values()); two new tests cover the dict shape directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds optional `counts: dict[str, int] | None = None` field. Populated on success path (next task), None on failure path (unchanged failure constructor). The pre-existing top-level `count` (the v1 hero sum) stays verbatim — this is purely additive so anything reading PackageOutcome today continues to work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per package per window, _collect_one now writes 8 badge JSON files: the
existing v1 hero (downloads-<N>d-non-ci.json, unchanged) plus seven new:
installer-{pip,pipenv,pipx,uv,poetry,pdm,pip-family}-<N>d-non-ci.json.
Counts sourced from the per-installer dict run_pypinfo now returns;
pip-family aggregate (pip + pipenv + pipx) computed in collect.
Module-level _INSTALLER_BADGE_SPECS tuple drives the per-installer
writes, keeping the relationship between filename, label, and dict key
visible at a glance. v1 hero stays as an explicit write so its
slightly-different label format isn't buried in the spec list.
PackageOutcome.counts is now populated on success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-package successful entry in _health.json gains a `counts` field
with the six allowlisted installers + pip-family aggregate. Top-level
`count` field preserved verbatim — anything reading _health.json today
(monitoring, the live deploy, future scripts) keeps working unchanged.
Failure-path entries unchanged: { error, window_days }, no counts key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dogfood badge row for pypi-winnow-downloads itself now shows all six individual installer counts alongside the existing v1 hero — demonstrating the per-installer breakdown the v2 collector emits. New "Use this service for your own package" section between Install and Status documents the URL pattern for all eight badge files (v1 hero + six individual + pip-family aggregate), with copy-pasteable markdown for embedding any of them in a third-party README. Includes the URL-encoding gotcha (%2F / %3A) that shields.io's endpoint argument requires. README is pyproject.toml's long_description, so the new section also lands on the PyPI project page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single ### Added bullet covering: seven new per-installer badge files (six individual + pip-family aggregate), v1 hero unchanged, _health.json per-package counts map (additive), README dogfood + endpoint instructions, zero-knob configuration. References the spec doc for the design rationale and the backwards-compat guarantees. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
cmeans
left a comment
There was a problem hiding this comment.
QA Round 1 — Findings
Reviewed at head 48d1904. CI all green. Local re-run: 79/79 pytest, 100% coverage on src/, ruff/format/mypy clean.
Spec ↔ implementation match
The spec (docs/superpowers/specs/2026-04-28-installer-mix-badge-design.md) calls for: run_pypinfo returns dict[str, int], six-keyed zero-filled allowlist, _collect_one writes 8 badge files (1 v1 hero + 6 individual + 1 pip-family aggregate), _health.json per-package gains additive counts map, README dogfood row expanded with 6 individual installers (pip-family deliberately omitted from dogfood per spec §README addendum), new "Use this service for your own package" section between Install and Status. All matched in the implementation; spec ↔ collector.py ↔ tests ↔ README align cleanly.
Backwards compatibility (the load-bearing claim of this PR)
| Surface | Status |
|---|---|
downloads-<N>d-non-ci.json filename / label / value |
unchanged — hero_total = sum(per_installer.values()) is mathematically equivalent to the v1 single-pass sum over the same allowlist+CI-filtered rows |
_health.json top-level fields (started, finished, packages) |
unchanged |
_health.json per-package count |
unchanged (still the v1 hero total) |
_health.json per-package error, window_days |
unchanged |
_health.json per-package counts |
new, additive |
PackageOutcome.counts field |
inserted between count and error with default None — all existing call sites use keyword args, so positional drift doesn't bite |
test_collect_v1_hero_count_unchanged_against_pre_v2_fixture and test_health_file_top_level_count_preserved lock both ends of the contract in CI.
Code review notes (no findings, just things checked)
- Allowlist + CI filter unchanged from v1 (
installer not in _INSTALLER_ALLOWLISTskips,ci == "True"skips, missinginstaller_nameraises). _INSTALLER_NAMESordered tuple drives_INSTALLER_ALLOWLIST = frozenset(_INSTALLER_NAMES); single source of truth, no risk of the two falling out of sync.- pip-family aggregate computed in
_collect_one(sum ofper_installer["pip"] + ["pipenv"] + ["pipx"]);run_pypinfo's zero-fill guarantees the three keys always exist, so noKeyErrorrisk. _INSTALLER_BADGE_SPECStuple makes the (filename, label, dict-key) relationship visible at a glance — clean enough that adding a future installer is an obvious edit.- Per-package isolation preserved: any
CollectorErrororOSErrorinside_collect_onereturns a failure outcome and skips badge writes for that package only. - Failure-path
_health.jsonentry shape unchanged ({error, window_days}, nocountskey). badge.pyand__main__.pyunchanged — no surprise escape hatch into adjacent modules.- shields.io endpoint URL encoding in the new README section (
%2F/%3A) matches the existing v1 hero badge's encoding.
Findings
1. PR-body test-plan parenthetical undercounts the test work (observation).
Test-plan checkbox 2 reads: "locally 79 tests pass at 100% coverage (6 new tests + 1 pre-existing test updated for the additive health-entry shape)". The count is correct, but the breakdown is stale.
Counted against the diff:
- 8 new test functions:
test_run_pypinfo_returns_per_installer_dict,test_run_pypinfo_zeroes_installers_with_no_rows,test_package_outcome_carries_per_installer_counts,test_health_file_includes_per_installer_counts_map,test_health_file_top_level_count_preserved,test_collect_writes_eight_files_per_successful_package,test_collect_pip_family_aggregate_equals_pip_plus_pipenv_plus_pipx,test_collect_v1_hero_count_unchanged_against_pre_v2_fixture. - 8 pre-existing tests updated for the new return shape (the 7
test_run_pypinfo_*tests that previously asserted oncount == Xnow assert onsum(result.values()) == X, plustest_collect_writes_health_file_with_per_package_counts_and_timestampswhich got an expanded equality on the additivecountsmap).
The discrepancy doesn't affect outcome (coverage gate is 100%, all 79 pass) — it's a reading-accuracy nit. Spec's "Tests" section lists six named scenarios, two of which got split into two tests during implementation (e.g., returns_per_installer_dict and zeroes_installers_with_no_rows); the spec didn't make a "6 total" claim, so it's the PR body parenthetical that's stale, not the spec.
Suggested edit: update the parenthetical to "(8 new tests + ~8 pre-existing tests updated to assert on the new dict return shape)".
|
QA round 1 → QA Failed (one observation). Verdict basis: standing rule — every observation blocks signoff. The finding is low-priority (PR-body parenthetical inaccuracy, no behavioral impact); Dev can edit the test-plan parenthetical and re-apply Ready for QA. What I ran:
CI green at Checkbox 2 left unchecked because the finding is about its parenthetical text, not its substantive claim — once Dev edits, check on round 2. |
|
QA round 1 finding addressed. Change: PR body test-plan parenthetical updated from Audit: counted against the diff exactly as QA did:
No code changes — just the PR body text. CI re-run isn't needed since the diff at |
cmeans
left a comment
There was a problem hiding this comment.
QA Round 2 — PASS
Head unchanged at 48d1904 — Dev's round-2 update was a PR-body edit only (test-plan parenthetical now correctly reads "8 new tests + 8 pre-existing tests updated to assert on the new dict return shape, including 1 health-file test updated for the additive counts map"). Round-1 finding fully addressed.
Re-verified in this session at the same SHA: 79/79 pytest, ruff check + format clean, mypy clean. All round-1 substantive verifications (spec ↔ collector.py ↔ tests ↔ README cross-walk, backwards-compat regression locked by test_collect_v1_hero_count_unchanged_against_pre_v2_fixture and test_health_file_top_level_count_preserved) carry forward unchanged.
CI green at 48d1904 (lint, typecheck, test 3.11/3.12/3.13, deploy-smoke).
Zero findings. Ready for QA Signoff.
|
QA round 2 → Ready for QA Signoff. Round 1's parenthetical observation is fully addressed at the PR-body level. Head SHA unchanged ( Awaiting maintainer QA Approved. |
…le block each Round 1 QA flagged that the v0.2.0 section had `### Changed` (Alpha→Beta) → `### Added` (PR #49) → `### Changed` (PR #50) — both wrong order (KaC v1.1.0 specifies Added → Changed → Deprecated → Removed → Fixed → Security) and duplicate `### Changed` blocks (standard KaC has one block per category per release). PR #41 specifically enforces this ordering on auto-CHANGELOG runs; hand-authored release commit shouldn't regress it. Reorders the v0.2.0 section so `### Added` comes first, then a single `### Changed` block containing both the README modernization bullet (PR #50) and the Alpha → Beta promotion bullet, in that order. The auto-release-page workflow added in PR #47 will now render the v0.2.0 GitHub Release page with the standard KaC shape. No content change — just structural fix. Same words, regrouped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…51) * release: v0.2.0 — installer-mix v2 + uv-first README modernization Bumps version 0.1.3 → 0.2.0 and promotes the [Unreleased] section to [0.2.0] - 2026-04-29. Two PRs land in this release; full bullets in CHANGELOG.md. Minor bump (rather than patch) is justified because run_pypinfo's return type changed from `int` to `dict[str, int]` in PR #49 — not a public API but a public-ish internal contract that tests and future callers depend on. The feature surface is also new (7 new badge files per package per window), making 0.2.0 the right semantic line. ### Added - feat(collector): add per-installer badge files (installer-mix v2) (#49, 2898ee9). Six individual installer badges + pip-family aggregate per package; v1 hero unchanged. Already verified end-to-end on the live CT 112 deploy. ### Changed - docs: uv-first install everywhere + by-installer dogfood layout + breakdown narrative (#50, 830c051). Hero in top metadata row; by-installer breakdown below blockquote; uv tool install leads in README and deploy/README.md; SECURITY.md mentions both uv and pip. uv.lock refreshed to pick up the version bump (single source of truth in pyproject.toml; uv lock writes 0.2.0 into uv.lock automatically). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: promote Alpha → Beta on v0.2.0 Trigger: four v0.1.x releases shipped, real production deploy running since 2026-04-25 producing daily badges for four packages, 100% test coverage on src/, additive-only schema evolution (v2 added new badge files alongside the unchanged v1 hero rather than mutating anything). Folding into the v0.2.0 release PR so the bump lands with the minor version cut. - pyproject.toml: `Development Status :: 3 - Alpha` → `Development Status :: 4 - Beta`. PyPI's classifier display picks this up on next ship. - README.md ## Status section rewritten: leads with "Beta as of v0.2.0", names the live-since date, mentions the 100% coverage, and adds the explicit guarantee that the v1 hero badge JSON shape and filename are stable through 1.0 (so README readers and downstream maintainers know what's frozen). - CHANGELOG.md gets a new ### Changed bullet under [0.2.0] with the rationale + scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(changelog): fix v0.2.0 KaC ordering — Added before Changed, single block each Round 1 QA flagged that the v0.2.0 section had `### Changed` (Alpha→Beta) → `### Added` (PR #49) → `### Changed` (PR #50) — both wrong order (KaC v1.1.0 specifies Added → Changed → Deprecated → Removed → Fixed → Security) and duplicate `### Changed` blocks (standard KaC has one block per category per release). PR #41 specifically enforces this ordering on auto-CHANGELOG runs; hand-authored release commit shouldn't regress it. Reorders the v0.2.0 section so `### Added` comes first, then a single `### Changed` block containing both the README modernization bullet (PR #50) and the Alpha → Beta promotion bullet, in that order. The auto-release-page workflow added in PR #47 will now render the v0.2.0 GitHub Release page with the standard KaC shape. No content change — just structural fix. Same words, regrouped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
First v2 badge candidate from the project's "Future badge candidates" backlog: per-installer breakdown of non-CI downloads, surfaced as seven additional shields.io endpoint badge JSON files per package per window (six individual installers + a
pip-familyaggregate). v1 hero kept side-by-side, additive throughout.What's in the diff
run_pypinforeturnsdict[str, int]instead ofint(allowlist filter and CI filter unchanged; six-keyed dict, zero-filled, ordered via new_INSTALLER_NAMEStuple)._collect_onewrites 8 badge files per successful package (1 v1 hero + 7 v2). Driven by a new module-level_INSTALLER_BADGE_SPECStuple so the(filename, label, counts_key)relationship is visible at a glance.PackageOutcome.counts: dict[str, int] | Nonecarries the per-installer breakdown._health.jsonper-package successful entries gain acountsfield (additive; top-levelcountpreserved verbatim for backwards compat).README.mddogfood badge row expanded with the six individual installer badges; new## Use this service for your own packagesection between## Installand## Statusdocuments the URL pattern for third-party packages (8-row table + URL-encoding gotcha + copy-pasteable markdown example).CHANGELOG.md[Unreleased]gets one### Addedbullet covering all of the above.Spec / plan
In-tree on this branch:
docs/superpowers/specs/2026-04-28-installer-mix-badge-design.mddocs/superpowers/plans/2026-04-28-installer-mix-badge.mdDecisions made during brainstorming (Form A absolute counts / B3 bucketing / C1 non-CI filter only / no per-installer brand colors / no per-package configurability) all trace to the spec.
Backwards compatibility
downloads-<N>d-non-ci.jsonfilename, schema, and value unchanged for any given pypinfo response. Regression test (test_collect_v1_hero_count_unchanged_against_pre_v2_fixture) locks this in — same fixture, hero count must equalsum(per-installer)._health.jsontop-level fields unchanged. Per-packagecountfield unchanged. Newcountsmap is purely additive.Version bump
This PR ships the feature only. The v0.2.0 release PR (separate, follows the v0.1.3 release flow) bumps
pyproject.tomland triggers PyPI publish. Minor bump justified becauserun_pypinfo's return type changes; the function isn't_underscore-prefixed and tests / future internal callers depend on its shape.Test plan
fail_under = 100) maintained — locally 79 tests pass at 100% coverage (8 new tests + 8 pre-existing tests updated to assert on the new dict return shape, including 1 health-file test updated for the additivecountsmap)v*tag (the v0.2.0 release),publish.ymlextracts the## [Unreleased] ### Addedblock via the awk extractor — locally validated against this CHANGELOG entryhttps://pypi-badges.intfar.com/<package>/for each configured package; spot-check one viacurl | jqto confirm shields.io endpoint shape🤖 Generated with Claude Code