A broad repo scan for architectural friction — shallow modules, implicit interfaces, duplicated choreography — with sketches for turning them into deep modules.
2026-06-10 · four parallel codebase surveys (Journal, Planner, packages, cross-cutting) · key claims verified against source · checked against CONTEXT.md and ADR-0001…0006
Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
SyncProvider (ADR-0002)AuthMethod polymorphism (ADR-0005)completeAuth, ConnectedServiceManager, capability seams (Importer / RoutePusher / WebhookReceiver).unknown end-to-endmap, uiapps/journal/app/routes/api.sync.komoot.import.ts:34 apps/journal/app/jobs/komoot-bulk-import.ts
Verified: the route enqueues creds: service.credentials — the raw JSONB — in the pg-boss payload. This bypasses withFreshCredentials, the ConnectedServiceManager's whole reason to exist:
markNeedsRelink never fires on failure.The Wahoo path goes through the manager correctly; Komoot is the one defector from the seam.
Jobs carry only the serviceId. The handler resolves fresh credentials through ConnectedServiceManager at execution time — identical to every other capability caller.
unknown end-to-end high leveragepackages/jobs/src/types.ts apps/journal/app/jobs/ (~14 handlers) · ~7 enqueue sites
Verified: JobDefinition.handler is (jobs: Job<unknown>[]) => Promise<unknown>.
item.data as SomePayload; every enqueue passes a bare string queue name + unchecked object.unknown exists for a documented contravariance reason — but that's an implementation constraint leaking into the interface.enqueueOptional swallows queue failures: "saved but fan-out never queued" looks like success.A JobPayloads map type ('notifications-fanout': { activityId: string }, …) with typed enqueue<K> and typed handler registration. The contravariance cast happens once, inside the package — not fourteen times at the edges.
apps/planner/app/lib/use-yjs.ts use-routing.ts use-waypoint-manager.ts use-elevation-data.ts + components (SessionView, ProfileSelector, ElevationChart, …)
Verified: ~15 string-keyed entries (geojson, coordinates, segmentBoundaries, surfaces, highways, maxspeeds, …) read at ~30 raw call sites.
routeData.get("surfaces") as string | undefined and re-parse JSON.parseJsonArray copy-pasted in two hooks with different signatures; waypoint extraction written four times.The document schema is the Planner's most important interface — enforced by convention only.
A routeData schema module: typed read/write of the whole document state, JSON encoding internal — analogous to what waypoint-ymap.ts already does well for waypoints. Hooks and components consume a RouteDataState, never raw Y.Map keys.
apps/planner/app/components/SaveToJournalButton.tsx apps/planner/app/components/ExportButton.tsx
One buildGpxData(yjs) module — naturally layered on candidate 3 — consumed by both buttons and any future export path.
createRoute / createActivity duplicate the GPX choreography high leverageapps/journal/app/lib/routes.server.ts (~24–78) apps/journal/app/lib/activities.server.ts (~68–122) apps/journal/app/lib/gpx-save.server.ts
Deepen the gpx-save module to own the full validate-and-derive step: one function returning parsed GPX + stats (accepting precomputed stats), leaving callers with only entity-specific row shapes. Extends ADR-0006 rather than contradicting it.
Worth grilling: should basic validation move down into packages/gpx, which already owns parsing and stat computation?
packages/types/src/index.ts packages/api/src/routes.ts (Zod) packages/db/src/schema/journal.ts (Drizzle)
Route exists three times: hand-written interface (types), Zod RouteSummary/RouteDetail (api), Drizzle columns (db) — each with different fields and nullability.api.v1.routes._index.ts constructs response JSON manually, not through the schema).RouteListResponse.Pick one canonical source — likely derive TS types from the Drizzle schema, have the api Zod schemas reference them — and validate responses against the contract at the seam (typed response helpers, or validation in the v1 handlers).
packages/api start guarding real traffic instead of just the schemas themselves.routes.$id.edit.server.ts push-action.server.ts:54 activities.server.ts (mutators that assume the caller checked) · others
route.ownerId !== user.id is re-checked in some loaders, re-checked in some lib functions, absent in others where the precondition is implicit.A loadOwnedRoute(routeId, userId) (and activity twin) as the single enforcement point, returning a branded type that mutators require — making bypass a compile error rather than a code-review catch.
completeAuth gave session minting.api.sync.connect.$provider.ts api.sync.callback.$provider.ts api.sync.push.$provider.$routeId.ts oauth-state.server.ts
An OAuth flow module owning initiate (returns redirect with cookies set) and complete (consumes request, returns exchange result + decoded state). Routes shrink to thin adapters.
map and ui cleanuppackages/map (79 lines: re-exports + a 35-line MapView) packages/ui (162 lines)
Both fail the deletion test in the telling direction — deleting them moves almost nothing:
map adds an import-ambiguity tax: apps import from map and map-core inconsistently (~21 vs ~19 sites).@trails-cool/ui is imported only by the two apps' root.tsx — for the stylesheet. Button/Input/Card have zero consumers; both apps roll their own buttons inline.map the only import surface for apps.ui or commit to it as a real design system — the half-state is the worst option.e2e/ — seed-route boilerplate in 5+ files · auth helpers defined inside auth.test.ts · http://localhost:3000/3001 hardcoded in 10+ files
A real e2e/lib/ fixtures module: seedRoute(), registerAndLogin(), base-URL constants, authenticator setup — imported by every spec.
Result<T,E>-everywhere error-handling rewrite — too sweeping; the narrower wins live inside candidates 5 and 7.| # | Candidate | Why it's first-tier |
|---|---|---|
| 1 | Komoot credentials bypass | Closest to a real defect — credentials at rest, no refresh, seam violated |
| 3 | Planner routeData schema module | Highest-leverage refactor — 30 call sites, ~7-file blast radius per attribute |
| 2 | Typed job seam | Best testability-per-effort — one registry, fourteen handlers gain types |
| 7 | Branded ownership loading | Best safety-per-effort — bypass becomes a compile error |
Then: 5 & 6 (one source of truth for GPX stats and domain types), 8 (before the next OAuth provider lands), 4 / 9 / 10 as opportunistic cleanups.