Skip to content

v1.12 — Architecture Health

We paid down accumulated drift across three surfaces — backend coupling, domain documentation, and a god component — so the next milestone ships against a healthier substrate.

Summary

Codebases age. Features stack on features. Code that was once clean becomes tangled. v1.12 took three independent debt surfaces that had matured in the same window and bundled them into one focused sustainability milestone: loose coupling in the backend (CheckInHabit no longer touches Gamification directly), accurate architecture documentation (DDD docs updated for six weeks of bounded-context evolution), and testable frontend components (Goal.vue decomposed from 1,796 lines into focused, mountable pieces).

This is infrastructure work, not feature work. Users see no new screens, no copy change, no behavior delta. Streaks still freeze, XP still awards, milestones still complete identically. But the substrate is now cleaner: goal-tracking and gamification are decoupled via domain events, the architecture docs tell the truth again, and the Goal page can now be unit-tested in isolation instead of requiring end-to-end fixtures.

The outcome is that v1.13 and beyond ship faster. Clearer docs reduce miscoding risk. Decoupled systems mean gamification can evolve without touching goal-tracking. Smaller components mean Coach, retention, and UI experiments can iterate without touching thousand-line files.

Goal

Pay down accumulated architecture drift across three independent surfaces — backend coupling, DDD documentation, and the Goal.vue god view — so that v1.13+ ships against a healthier substrate. Specifically: decouple CheckInHabit from GamificationService via the established DomainEvents pattern; refresh the four DDD bounded-context docs to match six milestones of evolution (GDPR, gamification expansion, onboarding) and resolve the model-as-publisher question on goal.rb:216; decompose ionic_frontend/src/views/Goal.vue (1,796 lines, 2× the next-largest file) into a layout shell + focused components, modeled on the proven RoadmapBuilder.vue extraction. No user-facing behavior change. No new visual surface. No paywall.

Scope — What Shipped

  • CheckInHabit decoupled to domain events — when a user completes a habit, GoalTracking::CheckInHabit now publishes goal_tracking.habit_checked_in via the DomainEvents facade; Gamification::Subscribers registers a handler that awards XP and partner bonus identically to the pre-refactor direct calls. Loose coupling, explicit side effects.
  • DDD documentation refreshed00-overview.md, 01-bounded-contexts.md, 03-context-map.md, 04-domain-events.md updated to reflect v1.6–v1.11 evolution: GDPR cross-cutting concern added, gamification additions catalogued (repair_streak, award_partner_bonus, grant_starter_freeze, new jobs/subscribers), onboarding interactions classified, all four gamification.streak_* events documented with publisher + payload contract.
  • Model-as-publisher decision documentedgoal.rb:216 model-as-publisher question resolved and recorded in 03-context-map.md as a bounded exception (models publish domain events; interactions consume and orchestrate side effects); decision signed off by Maggie + Josh.
  • Goal.vue decomposed into focused components — four new components extracted: GoalTimelineSurface.vue (event feed + encouragement reactions), GoalHabitSection.vue (check-in grid + streak display), GoalMilestoneList.vue (milestone counter + toggle tree), GoalRoadmapPanel.vue (roadmap builder wrapper). Original 1,796-line God component reduced to 874-line shell (−51%, −922 lines).
  • Dead CSS purged — ~320 lines of unused scoped styles removed from Goal.vue during Phase 58; only live classes retained.
  • Shared types extractedtypes.ts created under components/goal/ to eliminate duplicate type declarations across Goal and extracted components; single source of truth imported by Goal.vue, GoalMilestoneList.vue, and GoalRoadmapPanel.vue.

Phases

PhaseNameStatusPlansHighlights
53CheckInHabit decouples to domain eventShipped2Event publishing wired, Gamification subscriber registered, loose coupling achieved
54DDD documentation refresh + goal.rb:216 decisionShipped3All four DDD docs updated, GDPR + gamification additions, model-as-publisher decision documented
55Goal.vue extracts GoalTimelineSurfaceShipped1Event feed + reactions surface isolated, named to avoid collision with routing timeline
56Goal.vue extracts GoalHabitSectionShipped1Check-in grid + streak display isolated, 1,796 → 1,775 line reduction
57Goal.vue extracts GoalMilestoneList + counter syncShipped2Milestone list + optimistic counter updates isolated, cross-component delta pattern established
58Goal.vue shell thin-out + GoalRoadmapPanel extraction + dead CSS purgeShipped2Final extraction, dead CSS removed (~320 lines), shared types extracted, 1,430 → 874 lines (−51% total)

Key Decisions

  • Domain events as the integration pattern — CheckInHabit publishes; Gamification subscribes. No direct coupling. This pattern is locked as the canonical way for interactions to signal side effects that other bounded contexts need to hear about.
  • Model-as-publisher as bounded exceptiongoal.rb publishes gamification.streak_advanced at the model layer (not interaction layer like Phase 53 CheckInHabit). This is documented as an exception to the general rule, with clear rationale: the event is so foundational to goal semantics that it belongs in the aggregate. Future model-as-publisher cases will follow the same exception process (Maggie + Josh sign-off) and be documented.
  • Component extraction over API refactoring — Goal.vue was already functionally modular (timeline composable, habit grid, milestone list, roadmap builder); the issue was code organization, not architecture. Rather than restructure via composables or state management, extraction focused on the render boundary — one component per logical surface — so each can be unit-tested in isolation.
  • Placement under components/goal/ — All extracted components live together under a feature domain folder, not scattered in top-level components/. This keeps Goal-related surfaces discoverable and decouples them from unrelated component directory sprawl.
  • Type extraction for shared surfaces — Rather than duplicate Milestone, MilestoneEvent types across Goal.vue, GoalMilestoneList.vue, and GoalRoadmapPanel.vue, extracted to types.ts under components/goal/ as single source of truth.

Requirements Coverage

18 / 18 requirements satisfied.

CategoryCountStatus
DECOUPLE3✅ All satisfied (Phase 53)
DDD5✅ All satisfied (Phase 54)
GOAL-EXTRACT5✅ All satisfied (Phases 55–58; original GOAL-EXTRACT-03 GoalHero retired as already-satisfied)
FIDELITY2✅ All satisfied (Phases 53–58)
DOCS3✅ All satisfied (Phase 54)

Full requirements list: v1.12-REQUIREMENTS.md on GitHub.

Outcomes

The codebase is now cleaner for the teams building on it:

  • Backend: Goal-tracking and gamification are decoupled. Future XP-rule changes, badge additions, or streak-mechanics experiments won't require touching check_in_habit.rb — subscribers handle the wiring.
  • Documentation: Architecture docs are current. New team members reading DDD docs will see the actual state of the system, not a stale snapshot. The model-as-publisher decision is recorded, ending ambiguity about when models publish events.
  • Frontend: Goal page is unit-testable. The largest view in the app (1,796 lines) is now 874 lines with extracted components that can mount in Vitest in isolation. This unblocks new feature work (Coach experiments, retention mechanics) that previously had to lean entirely on Cypress E2E.
  • Health: v1.13+ starts with a lighter substrate. The same three debt surfaces won't accumulate as quickly because (a) coupling is explicit and loose, (b) docs are the source of truth, and (c) components are small enough to refactor without epic effort.

Tech Debt

  • (Phase 57–58 carry-forward) milestone-completed-delta optimistic-counter pattern implemented for flag-off path (GoalMilestoneList) but not flag-on path (GoalRoadmapPanel). When the goal-roadmap-v1-9 flag is ON and a roadmap step toggles, counter lags during the mutation round-trip. Not a regression (Phase 57 scope was flag-off only), but unifies both paths in v1.13.
  • (Phase 58 housekeeping) Five v1.13 extraction candidates named: GoalAccountabilityPartner.vue (~42 lines), GoalPrivacyConfirmDialog.vue (~76 lines), useGoalDerivedFields composable (~38 lines), GoalHeaderRow.vue (~30 lines), inline description-card surface (~50 lines). These are too small or too coupled to extract cleanly in v1.12; v1.13 backlog if appetite exists.
  • (Phase 58 minor) userId ghost variable passed to roadmap mutation but not declared in variables; Apollo silently discards it. v1.13 cleanup candidate.

Key phase-closure and feature commits:

  • c7bd0b06 — feat(v1.12/53): CheckInHabit decouples to domain event, Gamification subscriber registered (Phase 53)
  • f29a9a21 — docs(v1.12/54): DDD refresh (bounded-contexts, context-map, domain-events, overview) + goal.rb:216 decision (Phase 54)
  • 337c3941 — feat(v1.12/55): Goal.vue extracts GoalTimelineSurface component (Phase 55)
  • ece4ea56 — feat(v1.12/56): Goal.vue extracts GoalHabitSection component (Phase 56)
  • 0c77fbd3 — feat(v1.12/57): Goal.vue extracts GoalMilestoneList + milestone-completed-delta pattern (Phase 57)
  • c7c102b7 — feat(v1.12/58): Goal.vue shell thin-out, GoalRoadmapPanel extraction, dead CSS purge, types.ts shared (Phase 58)
  • 82720806 — docs(v1.12): rollout narrative + operational playbook (Phase 58, Dori)

Version: v3.9.45
Shipped: 2026-05-03
Last updated: 2026-05-22

Loading…