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 exactlyclub1k,wins500,streak100,featured, each computed from real data behind a named constant (CLUB_MEMBER_THRESHOLD,WINS_THRESHOLD,STREAK_DAYS/STREAK_HOLDERS_THRESHOLD). Nofalseliteral 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 fromcomponents/communities/, props rewritten field-for-field against the real schema,loading/erroredstates added, all eight Storybook stories rewritten withplay()interaction coverage.useCommunityBadgesonviews/Community.vue(Phase 4) — the composable's first production call site, gatedv-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 —featurednow lives only on the highlights row.- Docs (Phase 5) — new
docs/features/community-highlights.md,guide_site/communities.mdcopy replacing the retired in-app tooltip, corrected stale claims indocs/features/communities.mdand the GraphQL architecture docs, and two GraphQL field-description wording fixes.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | Badge contract — decide which badges are real | Shipped | 1 | Locked 4 kept / 6 dropped; reversed two starting recommendations (verified, topActive) after checking real write paths rather than trusting real-looking columns. |
| 2 | Backend — make every kept badge computable and honest | Shipped | 1 | Every kept badge computed, no false literal; six fields deleted with all four codegen artifacts (one undocumented) committed in the same commit. |
| 3 | Frontend contract reconciliation | Shipped | 1 | Props rewritten field-for-field; component moved to its correct directory; loading/errored states added ahead of Phase 4's need. |
| 4 | Wire it to a surface (W2-04 closed by deletion) | Shipped | 1 | First 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). |
| 5 | Documentation | Shipped | 2 | Badge 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 indocs/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 signal —
communities.is_verifiedexists, 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. streak100readsusers.longest_streak, nevercurrent_streak—current_streakhas no decay job, so a member who stopped showing up months ago would still count;longest_streakis 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.mdinstead of an in-app bubble. - "The query failed" and "the query succeeded and told you nothing" are different states —
communityBadgesis nullable, and a null payload (caller cannot see the community) renders an error pill, not silence. Only a successful response with all four marksfalserenders 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).
| Category | Count | Status |
|---|---|---|
| CONTRACT-* (Phase 1) | 9 | All satisfied |
| BACKEND-* (Phase 2) | 10 | All satisfied |
| PROPS-* (Phase 3) | 11 | All satisfied |
| WIRE-* (Phase 4) | 9 | 8 satisfied, 1 deferred (OBJ-3262, live 375px capture) |
| DOCS-* (Phase 5) | 10 | All 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.vuehas 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_changesblocked 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 carryingclaude/multica-agentco-author trailers.
Related Artifacts
- Roadmap: v4.50-community-badges-real-data-ROADMAP.md
- Milestone Audit: v4.50-community-badges-real-data-MILESTONE-AUDIT.md
- Source issue: OBJ-3063 — "Community badge system is half-built and would ship wrong data — decide wire-up vs. delete," filed by Orion off Desi's v4.47 Phase 5 Task 1 delivery (OBJ-3019)
- Coordination anchor: OBJ-3134 (parent milestone issue)
- Carried forward, not closed: OBJ-3262 (Phase 4's deferred live capture), OBJ-3319 (merge-gate signature)
Related Commits
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