/perform/events/:id/draft) and revealed the missing dimension. Two parallel /perform event URL trees exist: FLAT /perform/events/:id/* mounts EventDetailPage (the canonical coach page); NESTED /perform/clubs/.../events/.../ mounts AssessComponents.* (effectively orphan — only reached via internal cascades). Coaches universally use FLAT. So 4 more pages move into the retireable column: DraftEvent, PublishedEvent, SessionManagement, ReviewEvent. Real shared set shrinks to just CreateEvent + CoachEvaluation + 3 pre-eval pages.
Tool inventory + reproducible commands, so the next reader can verify or extend the audit.
All searches scoped to src/ on feat/perform-redesign at HEAD. Backed by ast-grep 0.39.5, ripgrep 14.1.1, fd 10.3.0. No code changes; outputs captured to .agents/scratch/structure-research/ (gitignored).
ast-grep --lang ts -p '{ path: $PATH, $$$ }' across all src/routes/*.ts, then grep'd path:/guard:/component: triplets to flatten into a table.src/routes/components.ts for AssessComponents/CoachComponents/V2Components/TeamComponents; each lazy-import resolves a page to a route mount.AssessComponents.<Name>, counted occurrences in coachRoutes.ts (legacy), v2Routes.ts (new perform), protectedRoutes.ts (athlete-facing). Three counts > 0 = shared; one tier only = legacy or athlete-facing.ast-grep --lang ts -p 'history.push(`$_/assess/$_$$$`)' + history.replace + <Link to=> grep, all filtered to exclude src/features/assess/ + src/routes/. Result: zero external nav into /assess.features/assess/pages/*.tsx: count route mounts (grep'd routes/*.ts) + external importers (grep filtered out features/assess/). Result: every page is routed.features/assess/<sub>/ directory: count external importers via grep -rln "from '@/features/assess/<sub>. Result: services/ 30, types/ 10, others ≤9.routes/components.ts barrels. ~160 are sub-components or orphans.First pass had features/assess/ in the LEGACY tier. The route × importer evidence puts it in SHARED instead — it's actively serving both coach and superadmin route trees, plus the athlete-facing pre-eval flow.
Where any new Perform-area feature should land. Page under pages/v2/<area>/; components from components/v2/<area>/.
pages/v2/perform/ (route targets)components/v2/perform/coach/components/v2/perform/player/components/v2/perform/parent/components/v2/perform/shared/components/v2/perform/report/The Perform Redesign shipped entirely here.
What I previously called "legacy assess." Actually the live event + evaluations module — its pages are mounted at BOTH /perform (coach) AND /assess (superadmin inspect), and its services are imported by 30 external files.
features/assess/services/ — 30 importers (events/, team/, perform v2)features/assess/types/ — 10 importers (shared interfaces)features/assess/pages/* 6 SHARED + 3 athlete pre-evalfeatures/assess/components/ — 9 importersCannot delete. Could RENAME for clarity.
11 superadmin-only /assess routes + 6 pages that ONLY those routes mount. Zero coach exposure. Deleting them removes a parallel inspect surface and ~6 page files of orphan code.
components/shadow/ (102 files) — separate retirementcomponents/v2/coach/ orphans (5 of 6 files)components/v2/player/ orphans (10 of 12)pages/section/Coach/ filesCan delete. Most of pages/section/Coach needs per-file audit.
v2 of this matrix added a column you can't get from route tables alone: effective reachability — does any code path in the coach flow actually navigate to that route? A route mounted but never pushed-to is effectively orphan even when role-gated to coach.
The missing dimension: the /perform tree has BOTH a FLAT URL shape /perform/events/:id/* (mounts EventDetailPage from pages/v2/perform/events/ — the canonical coach event surface delivered in the redesign) AND a NESTED URL shape /perform/clubs/:clubId/teams/:teamId/events/:eventId/* (mounts AssessComponents.*). Every coach-flow caller uses FLAT. The NESTED routes are dual-mounted but unreachable except via DraftEventPage's internal cascades.
| Component (page) | /assess (superadmin) | /perform NESTED | /perform FLAT | Coach reaches? | Status |
|---|---|---|---|---|---|
| CreateEvent | superadmin × 3 | coach × 1 | — | YES — only mount | SHARED · live |
| CoachEvaluation | coach × 2 | coach × 1 | — | YES — coach clicks Evaluate | SHARED · live |
| EventDetailPage (pages/v2/perform/events/) | — | — | coach × 2 (draft + published) | YES — canonical coach event page | CANONICAL |
| DraftEvent | superadmin × 1 | coach × 1 | — | NO — FLAT preempts | EFFECTIVELY ORPHAN |
| PublishedEvent | superadmin × 1 | coach × 1 | — | NO — FLAT preempts | EFFECTIVELY ORPHAN |
| SessionManagement | superadmin × 1 | coach × 1 | — | NO — only via DraftEvent cascade | EFFECTIVELY ORPHAN |
| ReviewEvent | superadmin × 1 | coach × 1 | — | NO — only via DraftEvent cascade | EFFECTIVELY ORPHAN |
| AssessDashboard | superadmin × 1 | — | — | NO | LEGACY |
| PreEvaluationResponses | superadmin × 1 | — | — | NO | LEGACY |
| PlayerAnalytics | superadmin × 1 | — | — | NO | LEGACY |
| ParentDashboard | superadmin × 1 | — | — | NO | LEGACY |
| InvitePlayers | superadmin × 1 | — | — | NO | LEGACY |
| ManagePlayers | superadmin × 1 | — | — | NO | LEGACY |
| PreEvaluationLanding | — | — | /pre-eval × 1 | YES (athlete) | ATHLETE-FACING |
| PreEvaluationQuestions | — | — | /pre-eval × 1 | YES (athlete) | ATHLETE-FACING |
| PreEvaluationComplete | — | — | /pre-eval × 1 | YES (athlete) | ATHLETE-FACING |
How the FLAT preemption works: Stevie's actual coach flow shows the redesign settled on FLAT URLs (/perform/events/:id/draft). Every navigation in the codebase pushes to FLAT — EventList, EventsSectionContainer, CompactEventCard, LinkedEventCard, CreateEventPage's post-create redirect, even features/assess pages push to FLAT. The NESTED route shape (/perform/clubs/.../events/.../*) exists in v2Routes.ts but has no callers outside DraftEventPage's internal state-cascade. Only CreateEvent (mounted at NESTED /events/create) and CoachEvaluation (mounted at NESTED /events/.../evaluate) are still on the coach hot path.
Confirmed via grep: 8+ callers push to /perform/events/:id/draft (FLAT) — EventList, EventsSectionContainer, CompactEventCard, LinkedEventCard, MemberEventDetails, CoachEvaluationPage, CreateEventPage post-create redirect. Only DraftEventPage itself pushes to NESTED paths (and only internally between session/evaluate/review states it'll never reach if nothing lands on it).
Top-down stack with the corrected tiering. features/assess/ moves UP from legacy into a new SHARED tier — it's the live event+eval module, dual-mounted by superadmin /assess and coach /perform routes.
The middle yellow tier is what changed. Previously placed in LEGACY (red); evidence shows it's actually dual-mounted live code. The dashed clay arrow shows the parallel-inspect dependency — superadmin /assess routes point at the same pages the coach /perform routes do.
Same six ranks, but Rank 6's scope shrank dramatically once the route-component matrix surfaced that features/assess/ isn't retireable. Ranks 1–5 unchanged; Rank 6 split into a small "retire superadmin inspect surface" + a separate per-file audit of pages/section/Coach/.
#shame:deprecated itemsTwo pieces of dead code marked deprecated; today they're just clutter.
Nothing for users. Devexp only — grep false-positives + reading past dead code.
git rm two functions; verify zero hits; commit.
public_profiles FK-embed family⬆ Bumped in urgency — these markers live in features/assess/services/ which has 30 external importers. The N+1 is paid TODAY by every coach + athlete that hits an event detail page, the Pulse feed, attendance tracking, or invites.
14 of 20 #shame:blocked markers are the same problem: public_profiles view doesn't carry FK metadata, so PostgREST FK-embed fails and every name+avatar join becomes a two-step query.
PR #2471 dropped blanket SELECT policy + introduced public_profiles view. PostgREST FK-embed only resolves on physical FKs, not view-projected. Code falls back to two-step + JS stitch.
A. SECURITY DEFINER RPC per query — BR1/BR2 pattern.
B. Lift PostgREST FK metadata via grants.
C. useProfileEmbed hook abstraction (hides ugliness, no perf gain).
None for A or C. Ships per rls-stack-sequenced-merge rule with staging soak.
Alex AI assistant — 4 spots using raw Tailwind / inline hex instead of --shot-* tokens. No visible bug today; breaks on theme switch or rebrand.
AlexHeader.tsx:142 — IconButton TailwindAlexTabs.tsx:46 — inactive pill TailwindHelpStepsCard.tsx:41 — inline hexquickChips.ts:90 — unused priorityRankRead surrounding pattern; swap to var(--shot-...). For quickChips: wire or drop.
src/components/shadow/ entirely102 files of Shadow-DOM web-components. Two API styles for the same idea — documented foot-gun. Only 9 files import from it.
Tier 1 (user-facing): Login, PerformPageLayout (every /perform render!), ChildManagement.
Tier 2 (admin/showcase): ShotsArticleEditor, 2 DesignSystem sections.
Tier 3 (legacy coach): EditTeamSettings, TeamFlat, TeamFrameworkSettings.
Build v2/ui equivalents first; migrate Tier 1 with auth-flow regression tests; Tier 2 then 3; delete shadow/ only after all 9 consumers verified gone.
v2/coach + v2/player filesOlder v2 attempt (pre-Perform-Redesign). Verified zero-importer files via grep audit.
Per-file knip audit, delete, run vitest + vite build.
⬇ Down from 20–40 hr (v1) ⬆ Up from 3–4 hr (v2) → now ~5–6 hr (v3). Bigger than v2 because the orphan NESTED /perform/clubs/.../events/.../* routes can also retire — they mount AssessComponents pages no coach ever lands on.
history.push'es inside these pages — wire any that still want to land on FLAT to /perform/events/:id/* instead of nested/club/:clubId/team/:teamId/assess superadmin entry — retire alongside AssessDashboardfeatures/assess/services/ — 30 importers, live event+eval data layerfeatures/assess/types/ — 10 importers, shared interface contractCreateEvent — only mount, coaches hit itCoachEvaluation — only evaluation surface, coaches reach via PublishedEventPage's evaluate buttonEventDetailPage at FLAT URL — the canonical coach event pageThe 4 orphan pages CAN still be reached by direct URL entry — if any superadmin has a bookmark, it breaks. The 6 LEGACY pages are clearly built for a workflow (superadmin inspect tools) — confirm none are still actively used before deletion.
Verification before deletion: grep any production routing analytics for hits on /assess/* and /perform/clubs/.../events/.../{draft,published,session,review} — if hits are zero, safe to retire.
pages/section/Coach/: 217 files, 57 routed via barrels. The other ~160 are sub-components or orphans — separate per-file audit, ~5–10 hr.
features/assess/ → features/event-editing/ (or similar)★ New rank discovered during the audit. features/assess/ is the event-editing + evaluations module, not the "old assess world." The name misleads every reader who's not already in the team's head.
New contributor reads features/assess/services/coach.ts, assumes it's pre-Perform-Redesign and skips it. Misses that 30 other files depend on it. Sees #shame:blocked markers and assumes "doomed legacy"; doesn't realise it's hot path.
A. Rename to features/event-editing/ (one big move + 59 imports to update).
B. Split: services + types into features/events/ (already exists); pages stay where they are; deprecate /assess/* paths.
C. Leave alone + add a top-of-folder README explaining "this is the event editing flow, not legacy assess."
All 14 FK-embed shame markers share this shape. Left: today's two-step query (slow). Right: SECURITY DEFINER RPC (proven BR1/BR2 pattern).
// #shame:blocked - FK embed `profiles!user_id` cannot // resolve through public_profiles (PostgREST limit). const { data } = await supabase .from('team_members') .select(` user_id, profiles!user_id(id, full_name, avatar_url) `) .eq('team_id', teamId); // → fallback fires SECOND query: const { data: profiles } = await supabase .from('public_profiles') .select('id, full_name, avatar_url') .in('id', userIds); return teamMembers.map((tm) => ({ ...tm, profile: profiles.find((p) => p.id === tm.user_id), }));
CREATE OR REPLACE FUNCTION public.get_team_roster_lite(p_team_id uuid) RETURNS TABLE ( user_id uuid, full_name text, avatar_url text, is_coach boolean ) LANGUAGE sql STABLE SECURITY DEFINER -- bypasses RLS, -- so JOIN works AS $$ SELECT p.id, p.full_name, p.avatar_url, (tc.user_id IS NOT NULL) AS is_coach FROM public.team_members tm JOIN public.profiles p ON p.id = tm.user_id LEFT JOIN public.team_coaches tc ON ... WHERE tm.team_id = p_team_id AND tm.status = 'active' AND (...access check inline...); $$; -- Client side becomes one round trip: const { data } = await supabase.rpc( 'get_team_roster_lite', { p_team_id: teamId });
Each #shame:blocked surface gets its own RPC. Access check moves INTO the RPC. One round trip replaces N+1.
Top five things that can go wrong during the cleanup itself.
ShadowTextInput swap breaks auth — every user affected.rls-stack-sequenced-merge: one PR at a time, staging soak between.components/shadow/.features/assess/ (Rank 7) breaks an import the grep missed.vite build catches every TypeScript import. Run vitest run after; revert cleanly if anything fails.