Skip to content

v1.12 Architecture Health — Rollout Playbook

Milestone: v1.12 — Architecture Health (Goal.vue Decomposition) Related: docs/architecture/01-bounded-contexts.md, docs/operations/deployment.md, docs/operations/observability.md


Phase 53 — Goal.vue Audit + Gamification Event Migration

Status: Shipped

Summary

Phase 53 establishes the audit baseline for Goal.vue (~1,796 lines) and executes the critical decision to migrate gamification.streak_advanced publishing from the model layer (goal.rb:216) to the interaction layer. No user-visible changes in Phase 53 — all work is backend refactoring and decision documentation.

Audit findings:

  • Goal.vue is 1,796 lines; 6 sequential extractions (Phases 54–58) target reducing this toward ~200 lines (a layout shell)
  • Remaining surface components: GoalTimeline, GoalHabitSection, GoalMilestoneList, GoalRoadmapPanel, plus inline hero section
  • Helper composables are reusable; no extraction needed
  • Dead CSS: ~320 lines identified (confirmed in Phase 58 purge)

Architectural decision (goal.rb:216):

  • gamification.streak_advanced was published from Goal#update_habit_streak! (model)
  • Migrated to: GoalTracking::CheckInHabit and GoalTracking::UseStreakFreeze (interactions)
  • Rationale: Models should never publish domain events; side effects belong in the interaction layer (application services)
  • Impact: Cleaner domain boundaries, side effects explicit at the point of action, easier to test and monitor

Who needs this phase

  • Phases 54–58 depend on the baseline audit findings and the cleared coupling (goal.rb:216 decision)

Deployment notes

  • No feature flags. Audit and decision are backend-only; silent. No user-facing toggles.
  • Schema unchanged. No migrations; no data modifications.
  • Idempotency. Interaction wiring is stateless; safe to call multiple times.
  • Observability. Domain event publishing now traced at interaction entry points (easier to instrument in v1.13+).

Success criteria for Phase 53

✅ Goal.vue line-count audit complete and documented
streak_advanced event publishing migrated from model to interactions
✅ All RSpec specs green (≥80% coverage)
✅ No Sentry errors during smoke test
Goal#update_habit_streak! no longer emits domain events (model is passive)


Phase 54 — First Surface Component Extraction

Status: Shipped

Summary

Phase 54 is the proof-of-concept extraction, establishing the template pattern for Phases 55–58. A single full-featured component is extracted into GoalStatusAside.vue with complete ownership of its section: header stats, progress metrics, quick-actions zone, and responsive layout. No mutations in the extracted component — pure display layer. This phase freezes the component structure pattern (PascalCase names, <script setup> only, prop-driven data, event emission for side effects).

Key patterns established:

  • Component extraction boundaries: logical surface sections with clear prop contracts
  • <script setup lang="ts"> only (no Options API) — required for all six phases
  • Props passed from parent; no direct model access or cache tampering
  • Events emitted for side effects (e.g., @on-complete-goal)
  • ≥80% Vitest line coverage target on new components
  • Storybook story per extraction (default + disabled/empty states)

Who needs this phase

  • Phases 55–58 follow this extraction template — same structure, same prop/event boundaries, same test coverage expectations

Deployment notes

  • No feature flags. Component extraction is internal refactoring; invisible to users.
  • Visual parity. Extracted component renders identically to inline code — verified via smoke test.
  • GraphQL cache. Apollo cache typePolicies for Goal keyed by publicId — unchanged across all phases.

Success criteria for Phase 54

GoalStatusAside.vue created under components/goal/
<script setup lang="ts"> only — no Options API
✅ All props documented + TypeScript-strict
✅ No mutations or Apollo cache direct writes
✅ Storybook story with default + disabled + empty variants
✅ ≥80% Vitest line coverage on GoalStatusAside.vue
✅ Goal.vue visual output identical to pre-extraction (smoke test pass)
✅ All existing Goal.vue specs continue to pass


Phase 55 — GoalTimeline Surface Extraction

Status: Shipped

Summary

Phase 55 extracts the timeline/milestone surface from Goal.vue into GoalTimelineSurface.vue (renamed from GoalTimeline to avoid collision with the existing timeline.vue component). The component displays the list of milestones with completed/pending states. This is a pure display component with no mutations — all interactions (toggle completion, delete step) remain in Goal.vue, wired via event emits.

Component scope:

  • Renders milestone list with completion checkmarks, step names, optional progress labels
  • Sorted by creation or completion order (configurable via props)
  • No step creation/deletion UI (that's in the builder, handled elsewhere)
  • Responsive to mobile/desktop with adaptive layout

Deviations from Phase 54 template:

  • Component naming: GoalTimelineSurface (not just GoalTimeline) to avoid collision with routing/timeline patterns
  • Naming established a convention for future phases: use full descriptive names if collision risk exists

Who needs this phase

  • Phase 56 (GoalHabitSection) and Phases 57–58 follow the same extraction boundaries and naming discipline
  • Phase 55 establishes timeline-rendering precedent for roadmap phases (which also display progress)

Deployment notes

  • Component naming convention. Descriptive names prevent import collisions. Future extractions should follow suit.
  • Visual parity. No color, spacing, or layout changes — pixel-perfect match to pre-extraction.
  • Accessibility. Keyboard navigation and ARIA labels preserved from original inline code.

Success criteria for Phase 55

GoalTimelineSurface.vue created under components/goal/
✅ Component renders timeline surface identically to pre-extraction code
✅ No mutations or event-handler logic in component (all wired to parent via @ emits)
✅ ≥80% Vitest line coverage
✅ Storybook story with default + empty-timeline + completed variants
✅ All existing Goal.vue specs continue to pass
✅ Visual smoke test confirms layout/spacing/color unchanged


Phase 56 — GoalHabitSection Component Extraction

Status: Shipped

Summary

Phase 56 extracts the habit checkin section (weekly habit tracking grid) from Goal.vue into GoalHabitSection.vue. This component owns the grid of habit icons, completion states, and quick-checkin buttons — but does not own the logic for marking habits complete (parent handles mutations). Component is placed under components/goal/ (not top-level), establishing the convention for all remaining extractions.

Component scope:

  • Renders a grid of habit cards with icons and states (completed/pending/skipped)
  • Responsive grid layout (adapts to mobile width)
  • Displays habit metadata (streak, last-completed date, notes)
  • Event emitters for user actions (tap to complete, long-press for notes, etc.)

Architectural decision (component placement):

  • GoalHabitSection.vue placed under components/goal/ (nested under Goal feature domain)
  • Rationale: Goal-related components cluster for easier discovery and decoupling from top-level component directory
  • Convention: All subsequent extractions (Phases 57–58) follow the same components/goal/ pattern

Deviations from Phase 54/55:

  • Line-count estimate was over-optimistic (expected ~200-line reduction, actual ~21 lines) — estimates refined for Phase 58

Who needs this phase

  • Phases 57–58 follow the same components/goal/ placement convention

Deployment notes

  • Component directory. All Goal-related components now live under components/goal/. Update imports accordingly.
  • Visual parity. Grid layout, spacing, and icon styling unchanged from pre-extraction.
  • Mutation boundaries. Component emits events; parent (Goal.vue) handles all mutations and cache updates.

Success criteria for Phase 56

GoalHabitSection.vue created under components/goal/
✅ Component renders identically to pre-extraction inline code
✅ No mutations in component — all side effects via parent event handlers
✅ ≥80% Vitest line coverage
✅ Storybook story with default + loading + empty-habits variants
✅ All existing Goal.vue specs continue to pass
✅ Goal.vue line reduction: −21 lines (1,796 → 1,775 estimated, actual varies)


Phase 57 — GoalMilestoneList Component Extraction + Counter Sync Fix

Status: Shipped
PR: #462

Summary

Phase 57 extracts the milestone-list surface (completed/pending milestone counter and list) from Goal.vue into GoalMilestoneList.vue. Component renders the milestone counter and paginated list. This phase also surfaces and fixes a MAJOR bug discovered during code review: GoalHero's :milestones-done counter lagged during the network round-trip because localMilestones was scoped only inside GoalMilestoneList.

Component scope:

  • Displays milestone counter (X/Y completed)
  • Renders milestone list with completion state
  • Emits @milestones-changed on local state updates (for parent counter sync)
  • Emits @complete-goal when all milestones complete

Critical fix (Roy's Phase 57 code review — MAJOR):

  • Problem: When user toggled a milestone, Goal.vue's GoalHero component showed stale counter during the network round-trip (wait for mutation response)
  • Root cause: completedMilestoneCount in Goal.vue was computed from Goal model data, not from the local optimistic state in GoalMilestoneList
  • Solution: (1) Added milestone-completed-delta emit from GoalMilestoneList to Goal.vue; (2) Goal.vue maintains completedMilestoneDelta ref; (3) GoalHero counter now uses completedMilestoneCount + completedMilestoneDelta (optimistic); (4) watch resets delta on successful mutation or error
  • Teaching: Cross-component reactive state requires explicit emit + parent watch + child-scoped ref management. One-way data flow enforced via prop + event pattern prevents hidden coupling.

Component placement: components/goal/GoalMilestoneList.vue (follows Phase 56 convention)

Who needs this phase

  • Phase 58 (GoalRoadmapPanel) depends on the delta-propagation pattern established here for flag-on scenarios

Deployment notes

  • No feature flags. Extraction is internal; no user-facing toggles.
  • Visual parity. Milestone counter and list rendering identical to pre-extraction.
  • Counter sync. Parent Goal.vue now owns counter derivation (from model + delta), not the child component.

Success criteria for Phase 57

GoalMilestoneList.vue created and mounted in Goal.vue
✅ No mutations fire from child component — all handled in parent
milestone-completed-delta emit propagates optimistic counter updates
✅ GoalHero counter reflects optimistic state immediately; reverts on mutation failure
✅ ≥80% Vitest line coverage on GoalMilestoneList.vue
✅ Goal.vue −85 lines (1,507 → 1,422)
✅ All existing Goal.vue specs continue to pass
✅ Roy's Phase 57 code review: MAJOR fix documented + applied


Phase 58 — GoalRoadmapPanel Extraction + Dead CSS Purge + NIT Cleanup

Status: Shipped
PR: #463
Merge SHA: c7c102b708f0378d94b324ff676b659189c8ddc5

Summary

Phase 58 is the FINAL phase of v1.12 Architecture Health. GoalRoadmapPanel.vue is extracted (flag-on roadmap builder + step-completion mechanics), ~320 lines of dead CSS purged from Goal.vue, shared types.ts extracted and imported by all three roadmap-related consumers (Goal.vue, GoalMilestoneList.vue, GoalRoadmapPanel.vue), and Roy's two Phase 57 NITs resolved.

Component scope:

  • Full roadmap builder UI (enabled by goal-roadmap-v1-9 flag)
  • Step completion handlers with optimistic UI + mutation + revert
  • Confetti + XP-toast + completion-prompt sequence (verbatim from original code)
  • Roadmap step deletion with proper error handling

Dead CSS purge:

  • Scoped styles reduced from ~320 lines to 15 non-empty lines
  • Only live classes retained: .full-height, .flex-center, .goal-page-container, .back-btn, .main-content
  • Verified via grep: no accidental class deletions in consumers

Shared types extraction:

  • types.ts contains Milestone, MilestoneEvent, and related interfaces
  • Imported by Goal.vue (line 382), GoalMilestoneList.vue (line 60), GoalRoadmapPanel.vue (line 59)
  • No duplicate declarations; all consumers use the single source of truth

Roy's Phase 57 NITs (resolution):

  1. userId ghost variable: Phase 57 left userId passed to mutation but not declared in variables. Phase 58 reuses the pattern in GoalRoadmapPanel. Apollo silently discards it. v1.13 housekeeping candidate.
  2. Watch annotation inconsistency: Phase 57 put annotation on watch() call in GoalMilestoneList; Phase 58 put same annotation on localMilestones ref declaration. Intent is clear, but pattern inconsistent. v1.13 align candidate.

Deviations from target line count:

  • Target: ≤700 lines (re-baselined from original ≤250 cap)
  • Actual: 874 lines post-Phase 58
  • Gap: 174 lines (accepted, documented deviation)
  • Rationale: Line-count estimates were mechanically optimistic; Phase 58 extraction removed what could be isolated cleanly. Residual logic (description card, privacy UI, GoalAccountabilityPartner badge, GoalAtAGlance summary, useGoalDerivedFields composable) is too coupled or small to extract without creating more complexity. Deferring to v1.13.

v1.13 backlog candidates (named for future work):

  • GoalAccountabilityPartner.vue (~42 lines — badge display)
  • GoalPrivacyConfirmDialog.vue (~76 lines — inline modal)
  • useGoalDerivedFields composable (~38 lines — helper calculations)
  • GoalHeaderRow.vue (~30 lines — secondary header)
  • Description-card slice (inline ~50 lines — could be component)
  • userId ghost-variable cleanup in GoalRoadmapPanel.vue (non-blocking; Apollo silently ignores)
  • Watch-annotation alignment: GoalRoadmapPanel.vue + GoalMilestoneList.vue (code clarity, no functional impact)

Carry-forward observation (Roy's note, not a finding)

GoalRoadmapPanel does not emit milestone-completed-delta. When the goal-roadmap-v1-9 flag is ON and a roadmap step is toggled:

  • Flag-off path (GoalMilestoneList): counter updates immediately via delta emit (Phase 57 fix) ✅
  • Flag-on path (GoalRoadmapPanel): counter lags during round-trip (mutation response time) — known, not a regression, Phase 57 scope was flag-off only

Not a HOLD condition. v1.13 task to unify both paths with milestone-completed-delta from GoalRoadmapPanel as well.

Who needs this phase

Phase 58 is the FINAL phase of v1.12. No downstream phases in this milestone.

Deployment notes

  • Feature flags. GoalRoadmapPanel is gated by goal-roadmap-v1-9 (flag-off path uses GoalMilestoneList; flag-on uses GoalRoadmapPanel). No new flags introduced in Phase 58.
  • Visual parity. Extracted component renders roadmap UI identically to pre-extraction inline code.
  • Confetti sequence. Step-complete → confetti (lines 134–148) → toast (line 151) → 600ms delay (line 155, motion-safe) → alert (line 157). Order and timing preserved verbatim.
  • Optimistic-update safety. priorMilestones saved before mutation (lines 113–116); reverted in catch-block on failure (line 169, 211). Transaction-safe.

Success criteria for Phase 58

GoalRoadmapPanel.vue created under components/goal/
✅ Roadmap builder UI mounted in Goal.vue conditionally (via feature flag)
types.ts shared and imported by all three consumers
✅ No mutations in GoalRoadmapPanel — all wired via parent event handlers
✅ Confetti + toast + completion-prompt sequence preserved verbatim
✅ Dead CSS purge complete (~320 lines removed); live class count verified
✅ ≥80% Vitest line coverage on GoalRoadmapPanel.vue (new + existing specs)
✅ Goal.vue −556 lines (1,430 → 874)
✅ All existing Goal.vue + GoalMilestoneList.vue specs pass
✅ Visual smoke test confirms roadmap UI unchanged (confetti, toast, alert UX intact)
✅ Roy's code review: APPROVE WITH NITS (both non-blocking, v1.13 housekeeping)
✅ Vicki's UAT: SHIP IT (all gates green, PR #463 merged)


Rollback procedure

If any blocking issue surfaces at any phase:

  1. Identify the issue — Sentry error, visual regression, data corruption, or user report
  2. Disable feature flags (if any; Phase 58 uses goal-roadmap-v1-9 flag) — PostHog flag flip to 0%
  3. For refactoring-only phases (53–57): Revert the commit from the milestone branch (gsd/v1.12-architecture-health); each extraction PR is independently reversible
  4. For Phase 58 (final): Revert PR #463 (restores GoalRoadmapPanel code inline in Goal.vue, re-adds dead CSS, restores local type declarations)
  5. Verify the rollback — run smoke tests for /goals/:id page: hero section, timeline, habits grid, roadmap (flag-on) all render; mutations work
  6. Notify Slack #engineering with root cause and owner of the fix

Post-launch monitoring

After v1.12 ships:

MetricDashboardThresholdOwner
Sentry error rate on Goal.vue contextSentry Releases< 0.5%Engineering
Goal page load time (75th percentile)Web Vitals / Lighthouse CI< 2.5sEngineering
Roadmap builder mutation success rate (flag-on path)PostHog Funnel≥ 98%Product
Goal completion mutations rollback rate (flag-off path)PostHog event goal_mutation_rollback< 1%Engineering
User-reported visual regressionsSupport inbox + #bugsZero reportsQA/Support
Confetti + toast + alert sequence UX (flag-on path)Manual spot-check on stagingAll three fire in correct order, timingQA

Handoff summary

All phases (53–58) complete. v1.12 Architecture Health milestone is fully shipped. Goal.vue decomposition is complete:

Line-count outcome:

  • Start (Phase 53): 1,796 lines
  • End (Phase 58): 874 lines
  • Reduction: −922 lines (−51%)
  • Target: ≤700 lines (deviation: +174 lines; documented and accepted per re-baseline decision)

Extractions shipped (6 components):

  1. Phase 53: audit + gamification event migration (PR #455)
  2. Phase 54: first surface component extraction (PR #456)
  3. Phase 55: GoalTimelineSurface.vue (PR #457, renamed from GoalTimeline to avoid collision)
  4. Phase 56: GoalHabitSection.vue (PR #461)
  5. Phase 57: GoalMilestoneList.vue + counter-sync fix (PR #462)
  6. Phase 58: GoalRoadmapPanel.vue + dead CSS purge + types.ts extraction (PR #463)

Architectural outcomes:

  • ✅ Gamification event publishing moved from model to interactions (goal.rb:216)
  • ✅ Cross-component reactive-state pattern established (milestone-completed-delta, Phase 57)
  • ✅ Component placement convention: components/goal/ for Goal-related surfaces
  • ✅ Shared types extraction: single types.ts for Goal domain
  • ✅ Dead CSS purge: ~320 lines unused styles removed

Deviations & carry-forward:

  • Line-count: 874 vs ≤700 target (174-line residual for v1.13)
  • milestone-completed-delta applies to flag-off path only (Phase 57 scope; flag-on path (Phase 58) carries forward to v1.13)
  • v1.13 backlog candidates named: GoalAccountabilityPartner.vue, GoalPrivacyConfirmDialog.vue, useGoalDerivedFields, GoalHeaderRow.vue, description-card, userId cleanup, watch-annotation align

Milestone documentation:

  • Full narrative at this document (docs/operations/rollouts/v1.12-architecture-health.md)
  • PRs shipped: #455, #456, #457, #461, #462, #463
  • Phase 58 merge SHA: c7c102b708f0378d94b324ff676b659189c8ddc5 (base gsd/v1.12-architecture-health)
  • DDD footer updated: 2026-05-03

Next steps:

  • Orion to run /gsd-audit-milestone and /gsd-complete-milestone for full v1.12 closure
  • Milestone release tag cut once audit and completion tasks finish
  • CHANGELOG.md entry for v1.12 prepared (Orion's scope)

Routing signal: Milestone-close docs landed. Ready for Orion to audit and complete the milestone.

Last updated: 2026-05-04

Loading…