Skip to content

feat(collector): add per-installer badge files (installer-mix v2)#49

Merged
cmeans-claude-dev[bot] merged 8 commits into
mainfrom
feat/installer-mix
Apr 29, 2026
Merged

feat(collector): add per-installer badge files (installer-mix v2)#49
cmeans-claude-dev[bot] merged 8 commits into
mainfrom
feat/installer-mix

Conversation

@cmeans-claude-dev

@cmeans-claude-dev cmeans-claude-dev Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

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-family aggregate). v1 hero kept side-by-side, additive throughout.

What's in the diff

  • run_pypinfo returns dict[str, int] instead of int (allowlist filter and CI filter unchanged; six-keyed dict, zero-filled, ordered via new _INSTALLER_NAMES tuple).
  • _collect_one writes 8 badge files per successful package (1 v1 hero + 7 v2). Driven by a new module-level _INSTALLER_BADGE_SPECS tuple so the (filename, label, counts_key) relationship is visible at a glance.
  • PackageOutcome.counts: dict[str, int] | None carries the per-installer breakdown.
  • _health.json per-package successful entries gain a counts field (additive; top-level count preserved verbatim for backwards compat).
  • README.md dogfood badge row expanded with the six individual installer badges; new ## Use this service for your own package section between ## Install and ## Status documents the URL pattern for third-party packages (8-row table + URL-encoding gotcha + copy-pasteable markdown example).
  • CHANGELOG.md [Unreleased] gets one ### Added bullet covering all of the above.

Spec / plan

In-tree on this branch:

Decisions 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.json filename, 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 equal sum(per-installer).
  • _health.json top-level fields unchanged. Per-package count field unchanged. New counts map 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.toml and triggers PyPI publish. Minor bump justified because run_pypinfo's return type changes; the function isn't _underscore-prefixed and tests / future internal callers depend on its shape.

Test plan

  • CI passes: lint, typecheck, test (3.11/3.12/3.13), deploy-smoke, all green
  • Coverage gate (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 additive counts map)
  • On the next v* tag (the v0.2.0 release), publish.yml extracts the ## [Unreleased] ### Added block via the awk extractor — locally validated against this CHANGELOG entry
  • After deploying the new collector to CT 112, smoke-check that all 8 badge files appear under https://pypi-badges.intfar.com/<package>/ for each configured package; spot-check one via curl | jq to confirm shields.io endpoint shape

🤖 Generated with Claude Code

cmeans-claude-dev Bot and others added 8 commits April 28, 2026 20:05
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>
@github-actions github-actions Bot added the Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA label Apr 29, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot added Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 29, 2026

@cmeans cmeans left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cmeans cmeans added the QA Active QA is actively reviewing; Dev should not push changes label Apr 29, 2026
@github-actions github-actions Bot removed the Ready for QA Dev work complete — QA can begin review label Apr 29, 2026

@cmeans cmeans left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_ALLOWLIST skips, ci == "True" skips, missing installer_name raises).
  • _INSTALLER_NAMES ordered 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 of per_installer["pip"] + ["pipenv"] + ["pipx"]); run_pypinfo's zero-fill guarantees the three keys always exist, so no KeyError risk.
  • _INSTALLER_BADGE_SPECS tuple 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 CollectorError or OSError inside _collect_one returns a failure outcome and skips badge writes for that package only.
  • Failure-path _health.json entry shape unchanged ({error, window_days}, no counts key).
  • badge.py and __main__.py unchanged — 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 on count == X now assert on sum(result.values()) == X, plus test_collect_writes_health_file_with_per_package_counts_and_timestamps which got an expanded equality on the additive counts map).

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)".

@cmeans

cmeans commented Apr 29, 2026

Copy link
Copy Markdown
Owner

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:

  • Local pytest: 79/79 pass at 100% coverage on src/, fail_under = 100 gate honored
  • Local ruff check + format check: clean
  • Local mypy: clean
  • Spec ↔ collector.py ↔ tests ↔ README cross-walk: all match (8 badge files written, 6-keyed zero-filled run_pypinfo dict, pip-family aggregate = pip+pipenv+pipx, _health.json additive, dogfood row shows 6 individual installers per spec §README addendum)
  • Backwards-compat regression locked by test_collect_v1_hero_count_unchanged_against_pre_v2_fixture and test_health_file_top_level_count_preserved

CI green at 48d1904 (lint, typecheck, test 3.11/3.12/3.13, deploy-smoke).

Checkbox 2 left unchecked because the finding is about its parenthetical text, not its substantive claim — once Dev edits, check on round 2.

@cmeans cmeans added QA Failed QA found issues — needs dev attention and removed QA Active QA is actively reviewing; Dev should not push changes labels Apr 29, 2026
@cmeans-claude-dev

Copy link
Copy Markdown
Contributor Author

QA round 1 finding addressed.

Change: PR body test-plan parenthetical updated from (6 new tests + 1 pre-existing test updated for the additive health-entry shape) to (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).

Audit: counted against the diff exactly as QA did:

  • 8 new test functions (across Tasks 1, 2, 3, 4): test_run_pypinfo_returns_per_installer_dict, test_run_pypinfo_zeroes_installers_with_no_rows, test_package_outcome_carries_per_installer_counts, 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, test_health_file_includes_per_installer_counts_map, test_health_file_top_level_count_preserved.
  • 8 pre-existing tests adapted: 7 test_run_pypinfo_* tests rewritten from count == X to sum(result.values()) == X (the int-to-dict ripple from Task 1), plus test_collect_writes_health_file_with_per_package_counts_and_timestamps updated for the additive counts map (Task 4).

No code changes — just the PR body text. CI re-run isn't needed since the diff at 48d1904 is unchanged.

@cmeans cmeans removed the QA Failed QA found issues — needs dev attention label Apr 29, 2026
@cmeans-claude-dev cmeans-claude-dev Bot added the Ready for QA Dev work complete — QA can begin review label Apr 29, 2026
@cmeans cmeans added the QA Active QA is actively reviewing; Dev should not push changes label Apr 29, 2026
@github-actions github-actions Bot removed the Ready for QA Dev work complete — QA can begin review label Apr 29, 2026

@cmeans cmeans left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cmeans

cmeans commented Apr 29, 2026

Copy link
Copy Markdown
Owner

QA round 2 → Ready for QA Signoff.

Round 1's parenthetical observation is fully addressed at the PR-body level. Head SHA unchanged (48d1904), so the substantive verification carries forward; re-ran 79/79 pytest + ruff/format/mypy clean in this session per the standing pre-signoff verification rule. Spec ↔ implementation alignment, backwards-compat regression coverage, and CI green status all confirmed unchanged from round 1.

Awaiting maintainer QA Approved.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge QA Approved Manual QA testing completed and passed and removed QA Active QA is actively reviewing; Dev should not push changes Ready for QA Signoff QA passed — ready for maintainer final review and merge labels Apr 29, 2026
@cmeans-claude-dev cmeans-claude-dev Bot merged commit 2898ee9 into main Apr 29, 2026
52 checks passed
@cmeans-claude-dev cmeans-claude-dev Bot deleted the feat/installer-mix branch April 29, 2026 14:49
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 29, 2026
…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>
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 29, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants