Skip to content

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)

  1. Characterization test first when the file has none (render per state, assert order/testIDs/copy).
  2. Extract along the seam the decomposition map names; behaviour identical; exports, testIDs, accessibility labels unchanged.
  3. typecheck · jest for the package · eslint --max-warnings 0 · check:sizes · check:cycles; screen-blueprint pointer updated.
  4. 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

WaveTargetSeamStatus
1post-detail-screen.tsx (858)already-typed subcomponents → post-detail/done 858→318
1directory-venue-detail-screen.tsx (1,675)section renderers → directory-venue-detail/sections, parts, data hook; local StatTile → @twomore/uidone 1,676→226 (+30 modules)
1record-detail-screen.tsx (1,013)pure helpers + presentational cards; one data hookdone 1,013→402
1edit-session-screen.tsx (1,007)five independent field-edit modals → own filesdone 1,007→112
1club-members-screen.tsx (935)row components; roster vs modal actionsdone 935→403
2venue-hours/fee/courts composersone useRowEditor<T>(); isRecord ×3 → onedone
2create-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
3venue-correction-modal.tsx (1,338)per-field modules + batch-confirm/submit hooksdone 1,300→374
3create-club-step3.tsx (1,161), step2 (810)render* functions → components; schedule CRUD stays with its statedone 1,162→450 / 811→443
3add-round-sheet.tsx (1,167)preview generate/commit pair moves togetherdone 1,168→375
3session-detail-screen.tsx (1,017)SectionBlock → components with explicit propsdone 1,017→506
SQLresolve_venue_facts (576)per-fact private.venue_fact_* helpers + orchestrator, equivalence-proved in the migration (00584)done, on prod
SQLapply_match_ratings (583), submit_venue_correction (364), ingest_venues_batch (300)same pattern (00590, 00591)done locally; prod push deferred
Seederseed-scenario/shared.ts (4,128)random · fixtures · db-lifecycle · gen-users/clubs/venues · sessions · compositiondone 4,128→128 barrel + 15 modules
Acreate-club-screen (1,078), create-club-step1 (810), create-club-helpers (617)step sections → create-club/step1/*; screen review/submit pieces; helpers by concerndone 1,078→576 / 810→281 / 617→162 (e3964ae9)
Acreate-session-screen (1,030), create-session-step2 (807), create-session-step1 (603)create-session/{step1,step2}/* sections + screen hooks; reuse edit-session modalsdone 1,030→407 / 807→327 / 603→295 (e3964ae9)
Aclub.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)
Blive-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 packagedone (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
Cseeder 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 libdone (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}.tsxdone 857→380 (2026-09-05, pinned by sessions/__tests__/venue-finder.test.tsx)

Shared-code consolidation ledger

DuplicateCanonical homeStatus
StatTile local copy in the venue detail screen@twomore/ui StatTilewave 1
isRecord ×3 (sessions)domain/utils/is-record.tswave 2
composer row-editor scaffolding ×3sessions/src/venue-composer-shared.tswave 2
wizard shell state machine ×2presentation/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 diffwave 2
ParticipantRow ×3 (sessions)sessions/src/components/participant-row.tsxwave 3
purposeLabel ×2 (profile), matchDurationMinutes ×2 (sessions)profile/src/consent-labels.ts, sessions/src/scorecard/scorecard-helpers.tsdone
haversine in ingest-venues/util.tssupabase/functions/_shared/geo.tsseeder track
LoadingCard/EmptyCard in venue-search-results-list@twomore/ui EmptyState/Skeletonwave 1

Enforcement added

  • ARCH-10 size ratchet (check:sizes, in yarn 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:

filelinesresolution
packages/app/src/presentation/cache/index.ts875split into entities.ts / invalidation.ts / dm.ts / bundles.ts; the barrel is 47 lines
packages/app/src/index.ts1939three version constants moved to app-version.ts and re-exported; now a pure barrel
supabase/functions/fetch-weather/index.ts1029pre-existing, baselined (can only shrink)
supabase/functions/simulate-activity/index.ts1008pre-existing, baselined
supabase/functions/send-push/index.ts605pre-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:sizes reports 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 check on seed-scenario reports 14 pre-existing type errors (SeedSummary shapes, fixture unions) — identical before and after the split; the function bundles and runs regardless.

Markdown remains the source of truth. Run yarn docs:check before handoff.