MVP Feature-Parity Audit (2026-06-15)
Three background agents swept the legacy app (tmp/range_day) + the legacy DB backup against the new app (server RPCs, web clients, roadmap) under the bar: a legacy feature users rely on, absent from the new app, is an MVP gap (the feature-parity-is-the-bar principle). This note tracks the gaps, their dispositions, and the decisions James owed — all resolved as of 2026-06-19 (deferred 2026-06-15, recorded here so the rationale isn’t lost).
Decisions (James) — ALL 4 RESOLVED as of 2026-06-19
- Course-run scoring — ✅ RESOLVED 2026-06-19: MVP. The only parity gap needing new schema/proto (a course-run grouping on scores). Backup-verified as a live, actively-used workflow — 521 runs by 223 users, last run 2026-06-16 (the backup’s final day); low breadth (0.3% of the base) but the target-customer workflow (organized teams running a course of fire), used right up to cutover → ships at MVP. The run-grouping backfill (linking the 521 legacy runs’ stage scores into runs + standings) is data-touching → on the cutover gate; builds at Phase 2 (#6). → course-scoring.
- Influencer follow/unfollow — ✅ RESOLVED 2026-06-18: KILL. 2 follow rows ever (single influencer), 0 dedicated screens, abandoned. No skip-list needed — the audit’s framing assumed the sync sees these events, but it doesn’t:
influencer_followersis not outbox-captured (docs/migration/02_triggers.sqlhas zero influencer refs — not a write-hot table) andusers.is_influenceris absent from the ETL’sUSER_COLUMNS, so both are already dropped in the data path. Realized as a documented exclusion note in02_triggers.sql(where someone would look for “why isn’t this captured?”). No work item. - hit_factors — ✅ RESOLVED 2026-06-19: Scope B (full parity), MVP. KEEP the calculator (rebuild) and the saved performance log, and MIGRATE the 26,380 rows 1:1 to
hit_factor_entries— reversing the earlier “KILL the data-carry” lean (the rows are a real performance log, not scratch calcs). The migration is part of the hit-factor-calculator build (migration-etlfinding #6); the ETL skipshit_factorsuntil then. - No-shoot zone — ✅ RESOLVED 2026-06-18. Re-graded
migration-etlfinding #6 LOW → MVP-parity;aggregate_zonesnow carries the legacy no-shoot count (readingnsCount/noShootCount) as an always-emittedNo-Shootzone, headline points unchanged → migration-noshoot-zone (done, #103). Field-level-gated byrangeday db migration-dry-run.
Gaps → disposition
| Gap | Disposition | Item |
|---|---|---|
| Squad (MSD) scoring entry | MVP — client UI only (server done) | squad-scoring-entry |
| Course-run scoring | ✅ MVP, resolved 2026-06-19 (Decision #1) — new course-run grouping (schema/proto); data-touching, gates cutover | course-scoring |
| MSD / course leaderboards | MVP (MSD may already rank) | msd-course-leaderboards |
| No-shoot in score breakdown | ✅ MVP-parity, done 2026-06-18 (Decision #4) | migration-noshoot-zone |
| Native avatar + drill-image display | MVP — mobile-track | filed by PR-4 worker (2 items) |
| Web avatar + drill-image | in progress | migration-media PR-4 |
| hit-factor calculator | ✅ Scope B, MVP, resolved 2026-06-19 (Decision #3) — KEEP calculator + saved log | hit-factor-calculator |
| Influencer follow | ✅ KILL, resolved 2026-06-18 (Decision #2) — not outbox-captured → documented exclusion | none |
| hit_factors 26k rows | ✅ MIGRATE 1:1 (Scope B, Decision #3) — reverses the earlier KILL; lands with the hit-factor-calculator build (#6) | hit-factor-calculator |
| Peer DMs | Not a gap — legacy never had peer DMs (the “Messages” inbox = challenge notifications, already a documented skip) | none |
Confirmed at parity (not gaps)
Friends, leaderboards (per-drill/friends/org), range bag, scheduled range days, events, achievements, content feed, dry-fire, single-shooter scoring, favorites, friend QR, share overlay, locations.
Filing status
All 4 decisions resolved (2026-06-19). The items were authored by the architect 2026-06-15 under James’s one-time direct-write exception (decisions embedded as open questions); each was finalized as its decision landed — #4 no-shoot (migration-noshoot-zone, done, #103) · #2 influencer (KILL → documented exclusion in 02_triggers.sql, no item; #106, skip-table-count follow-up #107) · #3 hit_factors (hit-factor-calculator, Scope B, #111) · #1 course-scoring (course-scoring, MVP, stays draft; #113). The remaining data-path builds (hit_factors un-skip, course-run grouping) land with their features at Phase 2 (#6); the other draft items (squad entry, MSD/course leaderboards) promote draft→ready in the build phase.
Forensic deep-dive (round 2, 2026-06-15) — page-by-page sweep
The first pass was time-boxed; round 2 ran 5 agents over the entire legacy page/component/custom_code/dependency tree (drills+scoring, profile+social, calculators+dry-fire, home+events+nav, hidden custom_code). Trigger: the shooter-drill-authoring + vector-stage-builder gap had gone weeks uncaptured, so we forensically re-checked everything. Home/events/nav (agent D) returned no new gaps (onboarding never existed in legacy; home-hub→tab-shell is a recorded redesign).
Captured this turn (items created, status: draft)
- shooter-drill-authoring — shooter-side custom-drill create/edit on PWA + native (legacy let any shooter author from their phone; new app authors only on dashboard). ~16k legacy custom drills.
- stage-diagram-builder — rebuild the legacy vector stage editor (
dependencies/r_d_image_creatorv3_ncaqvt). Backup-verified 2026-06-16: 2,362 of 16,098 custom drills carry a vector design (custom_stagejson[]), by 990 distinct users — ~5× the flat-photo path (464). Vector-only storage (no rendered PNG) → these don’t display post-cutover without the builder or a renderer. MVP-vs-fast-follow = open scope call.
New gaps from the sweep — disposition owed (James)
| # | Gap | Evidence | Rec. disposition |
|---|---|---|---|
| F1 | View a friend’s profile — their shared drills/quals/courses + achievements (the payoff of the friend graph; list has no tap-through) | legacy pages/profile/user_page; new friends list is flat, no detail screen | MVP — highest consequence |
| F2 | Qualifier/Assessment as a first-class category — catalog browse split + “Past Quals” score filter | legacy live_fire_drills “Assessment and Quals” tile; scores_page “PAST QUALS” (category='qualifier') | MVP — train-to-standard users |
| F3 | Community-drill browsing/discovery — data layer done (visibility/source_drill_id), no browse UI | legacy drillss “Community Drills” section; drill-builder says “no community browsing UI” | MVP |
| F4 | Drill instructional video — playback (YouTube/stream) + offline download/cache | legacy drill_video_online_offline, drill_description_page download button | Scope call — sizeable; MVP vs Pro |
| F5 | Score-history trend chart / progress graph | legacy custom_code/widgets/trend_chart.dart; mobile-score-history is marked done with “Hit factor trend display” but shipped list-only | MVP — reopen the falsely-done item |
| F6 | Score grouping by drill name on history list | legacy completed_drills_p_s .unique(drillName); new = flat list | MVP-ish |
| F7 | Range-bag curated “suggested items” gear picker + store links | legacy add_item_to_range_bag ~10 T.REX products w/ itemLink | MVP — low effort, affiliate touchpoint |
| F8 | Range-bag packing checklist (per-item check-off) + reset-to-defaults | legacy range_bag_widget itemClicked; orphaned range_bag_complete achievement (migration 0009) depends on a check state the new model can’t produce | Borderline + consistency bug |
| F9 | Drill loadout / per-drill gear list on drill detail | legacy drill_description_page “LOADOUT”; core required_items is an undecided [ ] | Borderline (range-bag promised a future item, never filed) |
| F10 | Global/community average hit-factor benchmark on drill detail | legacy drill_description_page:926 “Global average:”; leaderboard-api deferred it | Borderline |
| F11 | Par-timer RANDOMIZE par-window toggle (random beep, anti-anticipation mode) | legacy dry_fire_par_timer isRandomizeActive; new randomizes start-delay only | Small MVP |
| F12 | Unsaved-changes navigation guard on builders/forms | legacy custom_code/widgets/drill_pop_guard.dart “Leave drill?” | Small polish |
| F13 | Marketing-email consent + “Sales” pref | legacy edit_user_profile opt_in_emails + sales_notifications | Product/legal call — is marketing email in-scope at all? |
Dispositions resolved (2026-06-25)
Triaged with fresh per-gap agents (state re-checked against current code/docs/git), then filed.
Filed as work items: F1 friend-profile-view (MVP) · F2 drill-qualifier-category (MVP — server side already exists) · F3 community-drill-browsing (MVP) · F5+F6 score-history-trends (MVP; F5 reopens the mobile-score-history “trend display” overclaim) · F7 range-bag-suggested-items (fast-follow) · F8 range-bag-packing-checklist (fast-follow; also resolves the orphaned range_bag_complete achievement) · F11 par-timer-randomize (fast-follow).
Not filed — already owned or not a gap:
- F4 (drill instructional video) — already deferred: playback in mobile-dry-fire, offline caching in mobile-media. Consolidate there; no new item.
- F9 (drill loadout /
required_items) — defer post-MVP; the migrate-or-drop data call is migration Decision D3 (drills.loadout, 2026-06-22-migration-completeness-audit). Tracked with D3; sibling of F7. - F10 (global average hit-factor) — already deferred in leaderboard-api (no proto field/RPC). No new item.
- F12 (unsaved-changes nav guard) — defer post-MVP polish; the PWA queues to IndexedDB so real data-loss risk is low. Recorded here, not filed.
- F13 (marketing-email consent) — not a gap: no marketing-email infrastructure exists; becomes a compliance item if/when marketing email is built (push-service defers
sales_notificationsindefinitely).
Consistency / doc fixes (not feature items)
mobile-score-history.mdisstatus: donebut the “Hit factor trend display” requirement was shipped as a list — reopen (→ F5).ROADMAP-LEGACY.mdMobile line formobile-range-bagreads “suggested items per drill” as ✅ done; the item actually defers it (→ F7). Doc correction.- PWA
share_overlay.rsis text-only, butpwa-media-dryfireclaims canvas image compositing — roadmap-claimed-not-implemented (media cluster; worker-relevant).
Recorded decisions re-surfaced (NOT gaps — already dispositioned, listed to re-confirm)
- Challenge accept/decline dropped (recorded:
challenge-api,mobile-challenges— participants added immediately). - Offline video caching deferred post-MVP (
mobile-media); instructional video playback deferred (mobile-dry-fire). - Content-feed runnable-drill card (image/video/link/drill_id) discarded → generic
action_url(migration-etl,content-feed-api). - Branch.io deep linking rejected → native Universal/App Links (
native-app-config). - Influencer follow dropped (= pending Decision #2 above).