Skip to content

v4.50 — Community Badges Backed By Real Data

A community can now carry up to four marks, and every one of them is a true statement about that community — computed from real data, labelled with the exact threshold it states.

Summary

Before this milestone, CommunityBadges.vue and its composable useCommunityBadges had zero production call sites — the component existed only in Storybook. That was the smaller problem. The component's props and the CommunityBadgesType GraphQL schema disagreed on 12 of 13 names, and four of the ten badges Community#badges could return were hardcoded false in the server, permanently. Wiring the component up as-is would have shipped confident, specific claims about communities — "1,000 members," "top contributor," "perfect month" — that the underlying data could never support.

Josh chose Option A: wire it up properly, rather than deleting the component (Orion's recommendation) or leaving it dormant. Five phases shipped over two calendar days, each as its own PR directly against master. Phase 1 (Desi) made the hard calls: it locked a final inventory of four kept badges and six dropped ones, and along the way reversed two of the kickoff ROADMAP's own recommendations after checking whether the data behind them was real. communities.is_verified looked like a real column — it is one — but grepping every writer of it found exactly one, a dev-only seeding rake task; in production it is false for every community, forever. That is a hardcoded false wearing a database column, the same defect class this milestone exists to remove, so verified was dropped. topActive was dropped too, for a structurally similar reason: fixing its most visible flaw (a proxy that conflates any user-row write with community activity) would still have left a second hardcoded placeholder, response_score = 50, inside the same input.

Phase 2 made the four survivors — club1k, wins500, streak100, featured — real, each behind a named constant, and deleted the six dropped fields from both the GraphQL type and the resolver in the same commit as all four codegen artifacts the schema change touches (one more than CLAUDE.md's documented three — the fourth was caught by its own CI guard, not assumed). Phase 3 rewrote the component's props to match the schema field-for-field and moved it out of the communities/ (discovery) directory into community/ (detail-page) — the neighborhood it actually belongs in. Phase 4 gave the composable its first call site ever, rendering the row on the community detail page, and correctly handled a subtlety Phase 3's own merged code first surfaced: communityBadges is a nullable field, and a null response (the caller can't see the community) is not the same state as a successful response where all four marks are false — the former renders an error pill, the latter renders nothing at all. Phase 5 documented the shipped system, including the earlySupporter/is_verified finding as a worked example for the next contributor deciding whether a new mark is ready to ship.

Goal

Make every community badge the app displays a true statement about that community — by deciding which of the ten badges describe something the product can actually compute, implementing those, deleting the ones that don't, and only then rendering them on a real surface with copy that matches the number underneath.

Scope — What Shipped

  • docs/ui-specs/v4.50-community-badges.md (Phase 1) — the binding design contract: four badges kept, six dropped, each survivor's exact server-side threshold written as a sentence and stated verbatim by its own label.
  • Community#badges (Phase 2, rails_api/app/models/community.rb) — now returns exactly club1k, wins500, streak100, featured, each computed from real data behind a named constant (CLUB_MEMBER_THRESHOLD, WINS_THRESHOLD, STREAK_DAYS/STREAK_HOLDERS_THRESHOLD). No false literal remains.
  • Types::CommunityBadgesType (Phase 2) — six fields removed (verified, topActive, earlySupporter, topContributor, allyMagnet, perfectMonth); a breaking schema change made while the query had zero consumers.
  • ionic_frontend/src/components/community/CommunityBadges.vue (Phase 3) — moved from components/communities/, props rewritten field-for-field against the real schema, loading/errored states added, all eight Storybook stories rewritten with play() interaction coverage.
  • useCommunityBadges on views/Community.vue (Phase 4) — the composable's first production call site, gated v-if="!isTeamRoom". A nullable payload is treated as an error state, distinct from a genuinely empty (all-false) community. CommunityHero.vue's old Featured pill, which used to vanish on a community's busiest posting days, is gone — featured now lives only on the highlights row.
  • Docs (Phase 5) — new docs/features/community-highlights.md, guide_site/communities.md copy replacing the retired in-app tooltip, corrected stale claims in docs/features/communities.md and the GraphQL architecture docs, and two GraphQL field-description wording fixes.

Phases

PhaseNameStatusPlansHighlights
1Badge contract — decide which badges are realShipped1Locked 4 kept / 6 dropped; reversed two starting recommendations (verified, topActive) after checking real write paths rather than trusting real-looking columns.
2Backend — make every kept badge computable and honestShipped1Every kept badge computed, no false literal; six fields deleted with all four codegen artifacts (one undocumented) committed in the same commit.
3Frontend contract reconciliationShipped1Props rewritten field-for-field; component moved to its correct directory; loading/errored states added ahead of Phase 4's need.
4Wire it to a surface (W2-04 closed by deletion)Shipped1First production call site for useCommunityBadges; correctly distinguishes a nullable "can't see this community" response from a genuinely empty one. One capture criterion carried forward (OBJ-3262).
5DocumentationShipped2Badge reference doc, guide-site copy, corrected stale claims, GraphQL wording fixes — split into OBJ-3263 (Codi) and OBJ-3264 (Dori).

Key Decisions

  • A badge with no honest definition is deleted, not defaulted to false — the governing rule this milestone establishes, stated in docs/features/community-highlights.md. Ten badges were declared at the schema layer; four survived contact with the data.
  • A real column is not a real signalcommunities.is_verified exists, is queryable, and had a GraphQL field, but has no production write path outside a dev seeding task. The check for the next badge: grep every writer of the field before trusting the reader.
  • streak100 reads users.longest_streak, never current_streakcurrent_streak has no decay job, so a member who stopped showing up months ago would still count; longest_streak is monotonic, so the mark can under-count while data is stale but can never over-claim.
  • The W2-04 tooltip is retired by deletion, not restored — v4.47's locked concept-affordance grammar tiers a non-interactive pill as not warranting a tooltip; once each label states its own threshold ("500 goals done"), the explanation is the label. The fuller explanation moved to guide_site/communities.md instead of an in-app bubble.
  • "The query failed" and "the query succeeded and told you nothing" are different statescommunityBadges is nullable, and a null payload (caller cannot see the community) renders an error pill, not silence. Only a successful response with all four marks false renders nothing.

Requirements Coverage

6 / 6 milestone success criteria satisfied, 48 / 49 phase-level acceptance criteria satisfied (see .planning/milestones/v4.50-community-badges-real-data-MILESTONE-AUDIT.md).

CategoryCountStatus
CONTRACT-* (Phase 1)9All satisfied
BACKEND-* (Phase 2)10All satisfied
PROPS-* (Phase 3)11All satisfied
WIRE-* (Phase 4)98 satisfied, 1 deferred (OBJ-3262, live 375px capture)
DOCS-* (Phase 5)10All satisfied

Outcomes

Every badge the app can display is now a true statement, computed from real data: 1,000+ members, 500+ completed goals, 10+ members who have each reached a 100-day streak, and an admin-set Staff pick. A component that had never once been called in production now renders on the community detail page for the first time, with correct handling for loading, error, and empty states — and the six badges that had no honest definition are gone from the public API surface entirely, not lingering as a permanent false.

Tech Debt

  • (Phase 4) The live 375px light+dark capture of the populated highlights row on a real staging community never shipped — a placeholder route in the evidence directive degraded silently instead of failing the gate. Tracked as OBJ-3262. Non-blocking: CommunityBadges.vue has zero line changes in the Phase 4 diff, so the component's own pixels were already Storybook-verified in Phase 3.
  • (Phase 5) require_extra_approval_for_unattributed_changes blocked PR #2843 for ~11 hours, diagnosed three times across two issues as a missing-reviewer-identity problem before the ruleset was read directly. Tracked as OBJ-3319 for reuse on future agent-authored PRs carrying claude/multica-agent co-author trailers.
  • de16bd112 — Phase 1: Badge contract (PR #2772)
  • 113fbaed0 — Phase 2: Backend (PR #2815)
  • 603b2d048 — Phase 3: Frontend contract reconciliation (PR #2832)
  • 7a774832b — Phase 4: Wire it to a surface (PR #2839)
  • a7119c838 — Phase 5: GraphQL description wording (OBJ-3263, PR #2844)
  • fac5bf300 — Phase 5: Badge reference docs (OBJ-3264, PR #2843)
  • 119ccd2c4 — Milestone ROADMAP tick (PR #2798)

Last updated: 2026-09-03

Loading…