God-file refactor — plan and ledger
Status: Active · Started: 2026-09-04 (owner: "properly take care of all the god files, abide by the architecture patterns app-wide, consolidate shared code")
Rule (ARCH-10)
No hand-written source file over 600 lines. The 55 files over it on 2026-09-04 are baselined in scripts/file-size-baseline.json; yarn check:sizes fails when a baselined file grows or a new file crosses the limit, and --update-baseline only ever records a smaller number. Every split below shrinks the baseline.
Method (every split)
- Characterization test first when the file has none (render per state, assert order/testIDs/copy).
- Extract along the seam the decomposition map names; behaviour identical; exports, testIDs, accessibility labels unchanged.
typecheck· jest for the package ·eslint --max-warnings 0·check:sizes·check:cycles; screen-blueprint pointer updated.- Shared code found during the split moves to its canonical home (domain/utils · presentation/utils · @twomore/ui · feature-local module), never a second copy.
Waves
| Wave | Target | Seam | Status |
|---|---|---|---|
| 1 | post-detail-screen.tsx (858) | already-typed subcomponents → post-detail/ | done 858→318 |
| 1 | directory-venue-detail-screen.tsx (1,675) | section renderers → directory-venue-detail/sections, parts, data hook; local StatTile → @twomore/ui | done 1,676→226 (+30 modules) |
| 1 | record-detail-screen.tsx (1,013) | pure helpers + presentational cards; one data hook | done 1,013→402 |
| 1 | edit-session-screen.tsx (1,007) | five independent field-edit modals → own files | done 1,007→112 |
| 1 | club-members-screen.tsx (935) | row components; roster vs modal actions | done 935→403 |
| 2 | venue-hours/fee/courts composers | one useRowEditor<T>(); isRecord ×3 → one | done |
| 2 | create-club-screen.tsx (1,223) + create-session-screen.tsx (1,149) | shared useWizardNavigation (step nav, resume/reset/destructive exit, edit-step/return-to-review) | shell done; screens continue in A |
| 3 | venue-correction-modal.tsx (1,338) | per-field modules + batch-confirm/submit hooks | done 1,300→374 |
| 3 | create-club-step3.tsx (1,161), step2 (810) | render* functions → components; schedule CRUD stays with its state | done 1,162→450 / 811→443 |
| 3 | add-round-sheet.tsx (1,167) | preview generate/commit pair moves together | done 1,168→375 |
| 3 | session-detail-screen.tsx (1,017) | SectionBlock → components with explicit props | done 1,017→506 |
| SQL | resolve_venue_facts (576) | per-fact private.venue_fact_* helpers + orchestrator, equivalence-proved in the migration (00584) | done, on prod |
| SQL | apply_match_ratings (583), submit_venue_correction (364), ingest_venues_batch (300) | same pattern (00590, 00591) | done locally; prod push deferred |
| Seeder | seed-scenario/shared.ts (4,128) | random · fixtures · db-lifecycle · gen-users/clubs/venues · sessions · composition | done 4,128→128 barrel + 15 modules |
| A | create-club-screen (1,078), create-club-step1 (810), create-club-helpers (617) | step sections → create-club/step1/*; screen review/submit pieces; helpers by concern | done 1,078→576 / 810→281 / 617→162 (e3964ae9) |
| A | create-session-screen (1,030), create-session-step2 (807), create-session-step1 (603) | create-session/{step1,step2}/* sections + screen hooks; reuse edit-session modals | done 1,030→407 / 807→327 / 603→295 (e3964ae9) |
| A | club.entity (991), rotation.strategy (760), session-info-model (691), bracket.rules (668) | sibling modules per concern; original path stays the re-exporting entry (zero consumer changes) | done 991→25 / 760→200 / 691→32 / 668→45 (e3964ae9) |
| B | live-tab (844), use-session-detail-actions (793), club-guests-section (784), feed-list (774), club.supabase (757), public-club-profile (746), use-post-mutations (733), ranking-tab (706), club-detail-screen (692), notification-center (675), club-detail-hero (668), settings-screen (651), profile.supabase (649), club-dues (636), match-board (629), home-tab (619), king-of-court-ladder (617), status-badges (605), query-keys (605) | same method, grouped by package | done (2cef284f + c4c88b5c): round 1 — live-tab 844→103, use-session-detail-actions 793→163, match-board 629→248, king-of-court 617→<600, club-guests 784→435, public-club-profile 746→390, ranking-tab 706→523, club-detail-hero 668→550, club.supabase 757→24, use-post-mutations 733→22, query-keys 605→92, status-badges 605→507 (every Badge wrapper stays in the one canon file the lint rules pin); round 2 — club-detail-screen 692→518, club-dues 636→374, home-tab 619→254, feed-list 774→513 (pure move), notification-center 675→484, settings-screen 651→317, profile.supabase 649→28 |
| C | seeder scenarios dense (1,763), club_growth (1,490), home_misc (892), live_showcase (865), card_showcase (825), home_match_day (741), home_formats (693); doc-router.ts (676) | per-scenario phase modules over the shared lib | done (c4c88b5c): dense 1,763→31, club_growth 1,490→24, home_misc 892→36, live_showcase 865→22, card_showcase 825→27, home_match_day 741→24, home_formats 693→27 (phase modules per scenario; lib/midnight-safe-start.ts deduped); doc-router 676→215. All 39 chain steps + every individual scenario key verified locally (yarn qa:seed --local); one pre-existing fixture bug fixed (club_growth district 분당구 → 성남시, FK clubs_adm2_slug_fkey) |
| — | venue-finder.tsx (857) | filter/view state → use-venue-finder-state.ts; query orchestration → use-venue-finder-data.ts; pure row-shaping → venue-finder-rows.ts; header/browse-list/search-results JSX → venue-finder-{header,browse-list,search-results}.tsx | done 857→380 (2026-09-05, pinned by sessions/__tests__/venue-finder.test.tsx) |
Shared-code consolidation ledger
| Duplicate | Canonical home | Status |
|---|---|---|
StatTile local copy in the venue detail screen | @twomore/ui StatTile | wave 1 |
isRecord ×3 (sessions) | domain/utils/is-record.ts | wave 2 |
| composer row-editor scaffolding ×3 | sessions/src/venue-composer-shared.ts | wave 2 |
| wizard shell state machine ×2 | presentation/hooks/use-wizard-shell.ts (or @twomore/ui wizard-shell) | wave 2 |
TourRefInjector ×3 (home ×2, sessions) | packages/ui (sibling-feature import is forbidden by ARCH-2) | wave 2 |
SignalRow (home, clubs), AttendanceSection (clubs, profile) | packages/ui after a prop diff | wave 2 |
ParticipantRow ×3 (sessions) | sessions/src/components/participant-row.tsx | wave 3 |
purposeLabel ×2 (profile), matchDurationMinutes ×2 (sessions) | profile/src/consent-labels.ts, sessions/src/scorecard/scorecard-helpers.ts | done |
haversine in ingest-venues/util.ts | supabase/functions/_shared/geo.ts | seeder track |
LoadingCard/EmptyCard in venue-search-results-list | @twomore/ui EmptyState/Skeleton | wave 1 |
Enforcement added
- ARCH-10 size ratchet (
check:sizes, inyarn check).
2026-09-09 — the gate had a blind spot: any file named index.ts
check:sizes exempted /index\.ts$ outright, on the reasoning that a barrel is API surface rather than logic. The exemption keyed on the FILENAME, so any amount of live code was invisible to the gate as long as it sat in an index.ts. presentation/cache/index.ts reached 875 lines of cache-write logic without ever failing.
The exemption is now earned by CONTENT: an index.ts is skipped only when every non-comment line is an import/export STATEMENT (matched by shape, so export const X = 1; does not qualify), including multi-line export { … } from '…'; blocks. Turning that on exposed five hidden files:
| file | lines | resolution |
|---|---|---|
packages/app/src/presentation/cache/index.ts | 875 | split into entities.ts / invalidation.ts / dm.ts / bundles.ts; the barrel is 47 lines |
packages/app/src/index.ts | 1939 | three version constants moved to app-version.ts and re-exported; now a pure barrel |
supabase/functions/fetch-weather/index.ts | 1029 | pre-existing, baselined (can only shrink) |
supabase/functions/simulate-activity/index.ts | 1008 | pre-existing, baselined |
supabase/functions/send-push/index.ts | 605 | pre-existing, baselined |
Lesson worth generalizing: an exemption that keys on a name rather than a property is not an exemption, it is a hole.
2026-09-09, later the same day — baseline back to zero. Owner: "no more god files." The three Deno edge functions were split rather than left parked in the ratchet: fetch-weather 1028 → index 269 + 6 modules, simulate-activity 1007 → index 112 + 10 modules, send-push 604 → index 309 + 3 modules, all following the corroborate-venues/venue-maintenance precedent. All three were pure moves and all three had NO tests beforehand, so 60 node tests now cover the extracted pure logic (weather aggregation and playability math, AQI grade parsing, grid dedup, push copy branches, and simulate-activity's production/authorization guards). scripts/file-size-baseline.json is {}.
The baseline is a ratchet, not a parking lot. Adding an entry is accepting debt; the only reason to do it is that a split is genuinely blocked, and that reason belongs in this doc next to the entry.
- Baseline re-locked after every wave; as of 2026-09-05 (post venue-finder split) it holds one file:
scripts/scrape-naver-place.mjs(608, being split by the venue-audit session) —check:sizesreports 0 app files over 600 (baseline not yet re-locked with--update-baseline, so it still shows the shrink as available). - Still planned: SQL function-length report in
yarn metrics(live definitions over 300 lines). - Known debt surfaced by the waves, not in scope:
deno checkonseed-scenarioreports 14 pre-existing type errors (SeedSummaryshapes, fixture unions) — identical before and after the split; the function bundles and runs regardless.