π§Ή Technical Debt & Maintainability
π§Ή Technical Debt & Maintainability
π Summary
Midshift carries a moderate, mostly well-understood level of technical debt. Roughly a third of the application's source files are touched by at least one measurable debt signal (high complexity, duplication, dead code, or an unmaintained library), but most of that is light: duplication overall is low (2.3%) and the typical function is simple. The debt that matters is concentrated in a few places: a critically out-of-date framework version, a handful of very large request handlers that mix many rules together, permission and write logic that is not applied in one consistent place, hand-run database changes, and a large test double that has to be kept in step with every server route. Unusually, the code contains no "TODO" or "FIXME" markers at all; the debt is instead recorded in prose in the project's own notes (CLAUDE.md), which is thorough but easy for a new team to miss. A new team could change this code with reasonable safety in the pure business-logic layer, which is well tested, and with considerably less safety in the server routes, which have almost no automated unit tests.
π― Rating
π‘ βββββ 3 / 5 β Amber. Debt is moderate and largely documented, but the riskiest parts (authorisation, fat route handlers, manual migrations, vulnerable framework pin) sit exactly where test protection is thinnest.
π Findings
1. Debt inventory
| # | Debt item | Category | Evidence | Tag |
|---|---|---|---|---|
| D1 | Next.js exact-pinned at 16.2.0 with critical advisories; no automated update mechanism | Outdated dependency | Module 8 Β§A2 | [Observed] |
| D2 | xlsx 0.18.5 frozen on npm since 2022, parses uploads server-side | Unmaintained dependency | Module 8 Β§A1; src/app/api/campaigns/[id]/products/route.ts:60 | [Observed] |
| D3 | Unused date-fns, google-auth-library | Dead dependency | Module 2 Β§1 | [Observed] |
| D4 | Fat route handlers: ad-unit PATCH (CCN 66), brief PATCH (64), review-file PATCH (59), render-push POST (53), jobs POST (52), purge cron (44) β validation, authorisation, persistence and side effects in one function | Structural | Module 2 Β§5 | [Observed] |
| D5 | forYouItems (CCN 109) β the "what needs me" feed in one function | Structural | src/lib/for-you.ts:94 | [Observed] |
| D6 | No data-access layer; Supabase queried directly from 154β168 files | Coupling | Modules 2 Β§7, 13 | [Observed] |
| D7 | The documented "writes go through API routes" rule is not universal: browser components write to Postgres directly (e.g. utm-generator.tsx, ad-units-grid.tsx, new-campaign modals), bypassing the server-side discipline gate and approval-reset logic | Inconsistent pattern | Module 3 | [Observed by Module 3] |
| D8 | Authorisation assembled per route from helpers in src/lib/api-auth.ts (3.6% unit-test coverage); the QA store re-implements the same rules (src/app/qa/store.ts:761β822) | Duplication of critical logic | Modules 4, 7 | [Observed] |
| D9 | No database transactions β one .rpc() call in the codebase; multi-row changes rely on hand-written undo; last-write-wins on concurrent edits | Data integrity | Module 3 | [Observed by Module 3] |
| D10 | Unbounded queries silently capped at Supabase's 1,000-row default (for-you.ts, purge cron archived branch) | Latent defect | Module 3 | [Observed by Module 3] |
| D11 | 92 hand-applied migrations; per-deployment state recorded only in prose; 088 and 091 written but not run; README/NEW_DEPLOYMENT give out-of-date baseline instructions (baseline is 001β090 + 092, docs say 001β061) | Operational | supabase/baseline.sql:2; README.md:70β71; Module 12 | [Observed] |
| D12 | QA harness (β4,500 lines; store.ts 3,349 lines, CCN 1,074 summed) must mirror all 125 route handlers; enforced by scripts/harness-contract.js | Maintenance tax | Module 2 Β§7 | [Observed] |
| D13 | Dead code acknowledged but not removed: src/app/(app)/campaign/[id]/checklist/, build/, e2e/checklist.spec.ts | Dead code | ls confirms present; CLAUDE.md "Parked decisions" says "to be deleted" | [Observed] |
| D14 | Orphaned schema kept on purpose: studio_crits, studio_crit_comments, campaign_members.is_creative_approver, the brief/review pipeline tables, legacy account_manager in RLS policies | Dead schema | CLAUDE.md "Migrations", "Parked decisions"; supabase/baseline.sql:46β50 | [Observed] |
| D15 | Role vocabulary drift: app accepts global role reviewer (src/types/database.ts:3, src/app/api/admin/invite/route.ts:7) but the user_role enum lacks it (supabase/baseline.sql:46β50) | Latent defect | Module 1 | [Observed] |
| D16 | "Bridge" code awaiting a migration: ratios parsed from format names until 088 runs (src/lib/artwork-aspect.ts) | Temporary workaround | CLAUDE.md "088" | [Observed] |
| D17 | Long-lived one-year signed storage URLs persisted in DB columns; cannot be revoked and will expire | Design debt | Module 13 | [Observed by Module 13] |
| D18 | Process documentation tailored to AI coding sessions and an external claude.ai "board" | Knowledge debt | Modules 10, 12 | [Observed] |
| D19 | CI uses npm ci --legacy-peer-deps as a flagged workaround | Build | .github/workflows/ci.yml | [Observed] |
2. Marker count
grep -rn for TODO, FIXME, HACK, XXX, @deprecated across src, supabase, scripts, e2e: 0 occurrences of each. [Observed] This does not mean there is no debt; it means debt is tracked in CLAUDE.md and docs/HANDOVER.md ("The queue, in order", "Parked decisions") rather than in code. A new team should migrate those into an issue tracker. [Inferred]
3. Maintainability β how safely could a new team change this?
| Factor | Assessment | Basis |
|---|---|---|
| Test protection β domain logic | Good | src/lib 67% line coverage; parser 85%, asset matcher 98%, UTM 100% (Module 7) |
| Test protection β server routes | Poor | src/app/api 0.9% unit coverage; protected only by the journey suite against a mock store, not against real Postgres/RLS (Module 7) |
| Test protection β UI wiring | Good, but local-only | ~780 journey tests (467 of 483 run here passed), not in CI |
| Modularity | Adequate | Clean src/lib pure functions; fat routes and direct DB access elsewhere |
| Documentation | Unusually rich but partly stale | Module 12: 9 of 16 sampled claims drifted |
| Type safety / lint | Good | tsc and ESLint clean |
| Custom guard rails | Strong | write-results.test.ts, schema-columns.test.ts, harness contract, scrub |
| Hotspots | Unknown | No churn data (squashed history) |
Verdict: a competent team could take this over, provided it first adds tests around authorisation and the high-CCN route handlers before refactoring them. [Inferred]
4. Debt quantification
Method. Of the 395 non-test TypeScript files under src/, count a file as materially affected if it has at least one of: a function with CCN > 20 (lizard), participation in a clone β₯ 70 tokens (jscpd), being acknowledged dead code, or being the consumer of an unmaintained dependency.
| Signal | Files |
|---|---|
| Contains a function with CCN > 20 | 43 |
| Participates in a duplicated block (jscpd) | 108 (incl. test and harness files) |
| Acknowledged dead code | 4 |
Uses an unmaintained dependency (xlsx) | 1 |
| Union, non-test files | 131 of 395 β 33% |
Estimate: about one third of source files are touched by measurable debt, but the severity-weighted share is much lower. Most duplication hits are small (2.3% of lines overall) and many sit in the QA harness mirroring production pages (e.g. workspace/campaign/[id]/layout.tsx β qa/_lib/campaign-shell.tsx, 78 lines). The materially risky debt is concentrated in roughly 15β20 files: the six fat route handlers, for-you.ts, api-auth.ts, the browser components that write directly, and the QA store. [Inferred β judgement, method as above]
Worst duplicated pairs (jscpd, lines): workspace/campaign/[id]/layout.tsx β qa/_lib/campaign-shell.tsx 78; media-strategy/route.ts β wrapup/route.ts 46; review/files/route.ts β upload/[token]/register/route.ts 46; workspace/campaigns/page.tsx β qa/campaigns/page.tsx 44; plan/route.ts β qa/store.ts 40. [Observed]
5. Prioritised remediation backlog
| # | Item | Severity | Effort | Blocks / risks if left |
|---|---|---|---|---|
| 1 | Upgrade Next.js β₯ 16.4; move to caret ranges + Renovate | π΄ Critical | S | RCE / auth-bypass exposure (Modules 8, 9) |
| 2 | Fix in-job privilege escalation and approval-reset gaps found in Modules 4 and 9 | π High | SβM | Sign-off integrity β the product's core promise |
| 3 | Route all browser-side writes through API routes, or prove RLS enforces the same discipline rules | π High | M | Bypass of discipline gate and approval reset |
| 4 | Unit-test api-auth.ts and the six fat handlers, then extract validation / authorisation / persistence | π High | M | Safe change of the most complex code |
| 5 | Replace xlsx | π High | SβM | Prototype pollution / ReDoS on uploads |
| 6 | Automate migrations and record per-deployment schema state; correct baseline instructions | π High | M | Silent schema drift across client deployments |
| 7 | Introduce Postgres functions (RPC) for multi-row writes needing atomicity | π‘ Medium | M | Partial writes under failure |
| 8 | Paginate or bound the 1,000-row-capped queries | π‘ Medium | S | Silent truncation as clients grow |
| 9 | Reconcile reviewer role with the DB enum | π‘ Medium | S | Admin action fails at runtime |
| 10 | Delete dead pages/specs; plan a migration dropping orphaned Studio tables and account_manager references | βͺ Low | S | Confusion, attack surface |
| 11 | Replace persisted one-year signed URLs with on-demand signing | π‘ Medium | M | Unrevocable links; breakage after expiry |
| 12 | Move the debt register from CLAUDE.md into an issue tracker; rewrite handover docs for humans | βͺ Low | S | Knowledge loss |
| 13 | Remove unused dependencies and the --legacy-peer-deps flag | βͺ Low | S | Supply-chain surface |
π§Ύ Evidence
grep -rn "TODO|FIXME|HACK|XXX|@deprecated"β 0 each.node /tmp/β¦/debt.jsoverlizard.csvandjscpd-report.jsonβ 43 complex files, 108 duplicated-block files, union 131 of 395 non-test files.find src -name "*.ts" -o -name "*.tsx" | grep -v .test. | wc -lβ 395.ls "src/app/(app)/campaign/[id]/"βchecklist/,build/present;ls e2e/checklist.spec.tspresent.supabase/baseline.sqllines 2 and 46β50;README.mdlines 70β71;src/types/database.ts:3.- Cross-references: Modules 2 (complexity), 3 (architecture), 4 (business rules), 5 (duplication), 7 (coverage), 8 (dependencies), 12 (documentation).
β οΈ Risks
| Risk | Severity | Effort |
|---|---|---|
| Most complex, least-tested code is the authorisation and write path | π High | M |
| Inconsistent write path (server vs browser) undermines the business rules the product sells | π High | M |
| Manual migrations across many single-tenant deployments | π High | M |
| Debt register lives in prose, not a tracker | βͺ Low | S |
β Recommendations
| Recommendation | Priority | Effort |
|---|---|---|
| Execute backlog items 1β2 before or immediately after close | P0 | SβM |
| Execute items 3β6 in the first 60 days | P1 | M |
| Execute items 7β11 within 90 days | P2 | M |
| Items 12β13 as housekeeping | P3 | S |