Skip to content

v4.47 — Concept Legibility

Every H-severity concept the app names in its UI — rarity tiers, pace, streak freezes, the six overlapping social nouns — can now be understood without leaving the screen, and guide_site/ has stopped being a page nobody links to.

Summary

Before this milestone, the app named dozens of domain-specific concepts with no explanation anywhere near them: a "Rare+" tile with no definition, a bare "Ahead / Behind / On Track" pace pill with no stated baseline, six social nouns (Ally, Partner, Supporter, Team, Community, Sub-community) that collide in everyday English and were never disambiguated in-app. The obvious fix — add a tooltip — wasn't available: UiTooltip.vue, the app's one interactive explanation primitive, never opened on a tap (a 400ms press-and-hold timer that a normal ~100ms tap never crosses) and couldn't wrap a sentence. And the guide site already had the words this milestone needed — guide_site/achievements-xp-and-ranks.md:88 defined "Rare+" in exactly the phrasing the app needed — but HELP_URL had exactly one call site in the whole frontend, a single root-level "Help" link in the side menu. This was a plumbing problem wearing a writing problem's clothes.

Six phases, three days, all direct to master. Phase 1 audited the app and found 41 surfaces needing attention (11 high-severity, 19 medium, 11 low), split into a progress/gamification wave and a social-vocabulary wave. Phases 2 and 3 repaired the two primitives everything downstream needed: a rewritten UiTooltip scored against WCAG's actual dismissible/hoverable/persistent obligations rather than the touch bug alone, plus a shared ConceptExplainerSheet extracted from three one-off explainer screens that had grown independently. Phases 4 and 5 consumed the audit's triage verbatim — no re-litigating severity or remedy tier — and shipped 25 of the 30 high/medium findings, deferring the rest with cited evidence rather than silently dropping them. Phase 6 added the one artifact the guide site never had: a glossary, opening with the exact collisions this milestone existed to resolve.

One finding outgrew its own milestone: two rank ladders (a streak ladder and an XP ladder) shared nine of ten rung names, so a user could simultaneously hold two different-sounding ranks that were both "correct." This milestone shipped the inline qualification the ROADMAP scoped it for and explicitly ruled a rename out of scope — then Josh made the product call to rename the XP ladder anyway, shipped separately as OBJ-3024 one day later. The collision named here is now fully retired, not just labeled around.

Goal

Every concept the app names in its UI can be understood without leaving the screen — either the explanation is on the surface, or a working affordance leads to it.

Scope — What Shipped

  • Frontend primitive: UiTooltip.vue rewritten against WCAG SC 1.4.13 (dismissible, hoverable, persistent) — tap-to-toggle instead of press-and-hold, wraps multi-sentence content inside a 375px viewport, dismisses on Escape and outside-tap, keeps aria-describedby stable rather than visibility-bound.
  • Frontend primitive: helpUrl(topic) + useHelpLink() (ionic_frontend/src/constants/urls.ts) — a topic → guide-page-slug map, unit-tested against guide_site/ on disk so a page rename that doesn't update the map fails a test rather than shipping a dead link.
  • Frontend primitive: ConceptExplainerSheet.vue — the shared shape extracted from XpRanksInfoModal.vue, StreakDetailsModal.vue, and PointsExplainerSheet.vue; PointsExplainerSheet migrated onto it as proof, the other two left byte-unchanged.
  • Removed: InfoCard.vue (zero call sites, dead), MomentumBar.vue (superseded by DashboardHero/MomentumStat, retargeted mid-Phase-2 after discovering it was already off the live dashboard), the v-tooltip directive (registered nowhere, silently dropping ten badge explanations — now declared unsupported with a CI guard, .github/workflows/v-tooltip-guard.yml, against reintroduction).
  • New surface copy/affordances: rarity tiers, pace baseline, streak-freeze mechanics, activity-heatmap scale, Stim XP's dropped qualifier, the two-rank-ladder qualification (wave 1 — 18 rows); the six-way social-noun collision via WhosWhoExplainer.vue, community/team vocabulary renames, helpUrl() deep links from real surfaces (wave 2 — 7 rows, 6 more deferred T0 with cited evidence).
  • New docs: guide_site/glossary.md — the guide's 20th page, opening with "Words that collide" (streak rank vs. XP rank, rest day vs. streak freeze, the six social nouns) before its term list.
  • Doc corrections: docs/features/achievements.md's rarity table (wrong on 13 of 21 rows against RARITY_MAP, now source-cited); streak-freeze refill cadence (was "weekly," is streak / 7); a goal-status list missing Paused/Completed; guide_site/goals.md's pace baseline (was implying it compares to a target date the code never reads).
  • Filed, not built here: OBJ-3224 (streak freezes accrue with no in-app way to spend one), OBJ-3062 (reduced-motion gap on three bottom sheets), OBJ-3063 (CommunityBadges.vue — zero call sites, prop/schema mismatches; now routed into the newly-kicked-off v4.50), OBJ-3064 (feed pagination) — all backlog or routed onward, none blocking this close.

Phases

PhaseNameStatusPlansHighlights
1Concept ambiguity audit — the inventoryShipped1Triaged 41 surfaces (target ≥25) into wave 1/wave 2 with severity, guide-coverage flag, and remedy tier. Found the two-rank-ladder collision, the unregistered v-tooltip directive, and Stim XP's dropped qualifier — none on the kickoff floor.
2Fix the explanation primitivesShipped1Rewrote UiTooltip against WCAG SC 1.4.13; added helpUrl(); deleted InfoCard.vue; declared v-tooltip unsupported with a CI guard. Retargeted from a dead MomentumBar.vue onto the live dashboard mid-phase.
3Shared explainer sheetShipped1Extracted ConceptExplainerSheet.vue from three one-off explainers; proved it on exactly one migration, left the other two untouched.
4Remediation wave 1 — progress & gamification vocabularyShipped4 (PRs)All 18 wave-1 H/M rows shipped: rarity, pace, streak freezes, heatmap scale, Stim XP, the rank-ladder qualification. Found and fixed two factual errors in pre-existing copy along the way.
5Remediation wave 2 — social vocabulary and guide deep linksShipped4 (PRs)7 of 13 wave-2 rows shipped, 6 deferred T0 with cited evidence. Built WhosWhoExplainer.vue — one contrasting sheet for six colliding nouns, not six inline definitions. Zero-line diff on AlliesPage.vue, confirming nothing here contradicts OBJ-3009.
6Glossary and docs closeShipped2 (PRs)Added guide_site/glossary.md; closed nine recorded doc-drift items; found and corrected docs/features/achievements.md's rarity table (13/21 rows wrong).

Key Decisions

  • Repair the primitives before writing any remediation copy — "add a tooltip" wasn't a valid fix while the tooltip didn't open on a tap. Phases 2 and 3 built the substrate every later phase consumed with zero re-litigation, the same sequencing this repo's other primitive-first milestones (e.g. v4.45's reconnect substrate) have used.
  • The audit's triage is consumed verbatim, not re-argued — Phases 4 and 5 shipped exactly the severity/tier calls Phase 1 made. This kept two independently-dispatched remediation waves consistent with each other and with the audit's own T0 (no action) tier, which exists specifically to prevent over-explaining.
  • Six colliding social nouns get one contrasting sheet, not six inline definitions (WhosWhoExplainer.vue) — defining each term separately at six surfaces would be exactly the nagging UI the T0 tier exists to prevent, and would still fail to convey that the terms are distinguished from each other, which was the actual user problem.
  • Renaming the rank ladder was ruled out of this milestone's scope, then done anyway as a separate, deliberate product call (OBJ-3024) — a milestone can correctly decide a fix is bigger than its own scope without that meaning the fix doesn't happen; it means it gets its own decision-maker and its own PR.
  • v-tooltip was declared not a supported API rather than repaired as a directive — every capability the affordance grammar needs (a focusable-trigger requirement, aria-describedby ownership, a dismissal affordance) is a component capability, not a directive's. Its ten dead usages were removed in Phase 2; Phase 5 found the component they lived on had zero production call sites anyway.
  • New copy is written only where the guide has none — reuse or link first. Of the 30 H/M rows shipped, most pointed at guide copy that already existed; new writing was reserved for the eleven genuine gaps the audit found.

Requirements Coverage

6 / 6 milestone success criteria satisfied, 30 / 30 phase-level acceptance criteria satisfied (see .planning/milestones/v4.47-concept-legibility-MILESTONE-AUDIT.md).

Success criterionStatus
Any H-severity concept label is explainable without leaving the screen
UiTooltip works on touch, keyboard, and screen reader — proven by interaction tests
helpUrl() deep-links exist and resolve to real guide pages
A glossary page exists and is reachable
No regression on the three pre-existing explainer screens
Nothing contradicts or duplicates OBJ-3009

Outcomes

A user hitting a domain-jargon label the app never explained — "Rare+", a pace pill, a streak freeze, the difference between an Ally and a Partner — now finds the explanation on the surface, one tap into a sheet, or one tap into a real guide page, instead of finding nothing. guide_site/ stopped being an island: helpUrl() has real consumers beyond the side-menu root link, at community, team, and analytics surfaces. The app's one interactive explanation primitive works on a tap, a keyboard, and a screen reader, proven by interaction tests rather than by inspection. A glossary exists for the terms that collide with each other, not just the terms nobody explained. And a collision this milestone deliberately left half-fixed — the two rank ladders — got fully resolved one day later by a product decision this audit surfaced the need for.

Tech Debt

  • (Phase 6) The glossary page is reachable only via the guide's own sidebar, not the top-level VitePress nav — one click away, not zero.
  • (Phase 6) One doc-drift item (D11, the "sub-community" → "Room" rename) is closed in fact but missing from the Phase 1 audit doc's own disposition table — a bookkeeping gap, not a content gap.
  • (Phase 5) Three wave-2 rows deferred T0: two were scoped to a component (CommunityBadges.vue) with zero production call sites, one was superseded by OBJ-3009.
  • (Milestone-wide) Verification of the 25 shipped rows rests on reviewer/CI evidence and a spot-checked sample of light/dark captures, not an individual human UAT pass on every row — consistent with how this repo's other recent milestones close.
  • Roadmap: v4.47-concept-legibility-ROADMAP.md
  • Milestone Audit: v4.47-concept-legibility-MILESTONE-AUDIT.md
  • Source issue: OBJ-3013 — "Audit app for ambiguous concepts/surfaces missing descriptions/tooltips," filed by Maggie as Josh's follow-up to OBJ-3009
  • Coordination anchor: OBJ-3014 (parent milestone issue)
  • Follow-ups filed: OBJ-3224 (unspendable streak freezes), OBJ-3062 (reduced-motion gap), OBJ-3063 (CommunityBadges.vue, routed into v4.50), OBJ-3064 (feed pagination), OBJ-3024 (rank-ladder rename — already shipped)
  • 6cb43be89 — Phase 1: Concept ambiguity audit (PR #2700)
  • 540cb8ee7 — Phase 2: Fix the explanation primitives (PR #2701)
  • d292c136d — Phase 3: Shared explainer sheet (PR #2704)
  • f3835e0de, 53b50fb29, b7b46778, a2da9fcb7 — Phase 4: Remediation wave 1 (PRs #2705, #2706, #2707, #2711)
  • a28c00f3d, b567c34ec, 319c6b1bd, 8705d69c9 — Phase 5: Remediation wave 2 (PRs #2720, #2721, #2722, #2771)
  • bc70e53c3, a23bf0f88 — Phase 6: Glossary and docs close (PRs #2793, #2820)
  • 1ec49f3d9 — OBJ-3024: rank-ladder rename (PR #2710, not a phase of this milestone but retiring a finding it surfaced)

Last updated: 2026-09-02

Loading…