✨ Code Quality & Standards
✨ Code Quality & Standards
📋 Summary
The code is disciplined and consistent, and its measured quality is above what is typical for a product of this age and size. The type checker runs in strict mode with zero errors, the linter reports zero warnings, duplicated code is low (2.3%), and the escape hatches developers use to silence the tools are rare and almost all justified. Unusually, the team has turned its own coding rules into automated tests. One example is a test that fails the build if a database write's result is never checked. The main weaknesses are a handful of very large, very complex functions and screens that will be costly to change safely. Request validation is done by hand rather than with a schema library. Error reporting to production is close to non-existent. Some unreachable code is still in the tree. Comments are exceptionally plentiful and written as a narrative history, which is a real asset for understanding why the code is as it is, but it also adds reading load and goes stale.
🎯 Rating
🟢 ●●●●○ 4 / 5 — Green. Strict typing, clean lint, low duplication and self-enforcing quality gates are strong signals. The score is held back by concentrated complexity (52 functions with cyclomatic complexity over 20), hand-rolled validation, missing observability and leftover dead code.
🔍 Findings
Q1. Measured baselines (from the shared brief, not re-run)
| Measure | Result | Reading |
|---|---|---|
tsc --noEmit | exit 0 | Clean |
| ESLint | 0 errors / 0 warnings; CI fails on lint | Clean and enforced |
| jscpd (min 70 tokens) | 135 clones, 1,479 duplicated lines, 2.30% (1.71% by tokens), over 449 files | ⚪ Low; under 5% is generally considered good |
| lizard | 2,621 functions, mean CCN 2.82; 52 > 20, 9 > 50 | Healthy average with a long tail |
Comments (cloc, src/) | 12,361 comment lines vs 44,380 code lines (about 28 comment lines per 100 code) | Very high |
Q2. Compiler and linter configuration
[Observed]tsconfig.json:"strict": true(:7),skipLibCheck,isolatedModules,allowJs, target ES2017. It does not enablenoUncheckedIndexedAccess,exactOptionalPropertyTypes,noImplicitReturns,noUnusedLocalsornoUnusedParameters, so array and record indexing is typed as always present.[Observed]eslint.config.mjsuses only the stockeslint-config-nextcore-web-vitalsandtypescriptpresets. There are no project-specific rules, no import-ordering or complexity rules, anddesign/,scripts/ande2e/are excluded on purpose. A clean lint therefore means "meets Next's defaults", not a bespoke standard.[Observed]Typed database access:src/types/database.ts(1,150 lines) types the Supabase client (SupabaseClient<Database>).[Inferred]It is hand-maintained or generated outside CI, since no generation script runs in the pipeline.src/lib/schema-columns.test.tscompensates by checking queried and written columns againstsupabase/baseline.sql.
Q3. Escape hatches
| Construct | Count in src/ | Where / why |
|---|---|---|
eslint-disable* | 27 | 24 × @next/next/no-img-element (storage-signed URLs, each with a reason, per CLAUDE.md policy); 2 × react-hooks/exhaustive-deps; 1 × no-explicit-any |
@ts-ignore / @ts-expect-error / @ts-nocheck | 0 / 0 / 0 | — |
any (: any, as any, any[]) | 1 real use | src/app/qa/store.ts:714 asDb = (c) => c as any (test harness) |
as unknown as | 82 (79 outside tests) | 73 are in the QA harness (app/qa/store.ts 44, app/qa/_lib/campaign-shell.tsx 8, etc.); 6 in product code (e.g. workspace/ask-sheet.tsx 2, upload/[token]/page.tsx 1) |
Non-null assertions x!. | 0 | — |
[Observed] The product code is close to free of type escapes. The double-casts are concentrated in the harness that fakes the Supabase client.
Q4. Error-handling discipline
-
[Observed]src/lib/write-results.test.ts(135 lines) walks every non-test.ts/.tsxundersrc/app(excludingsrc/app/qa) and fails if:- an awaited write (
insert/update/upsert/delete) captures nothing; - a DELETE route or its harness-store mirror does not count affected rows;
- a user-client
updateends in.single().
A deliberate exception must be declared on the line above as
// unread-write: <reason>. There are 13 such declarations. Limitation: it scanssrc/apponly, so helpers insrc/libthat write (e.g.notify.ts,rounds-server.ts,review-notify.ts) are not covered. - an awaited write (
-
[Observed]src/lib/schema-columns.test.ts(142 lines) refuses any selected, filtered or written column missing fromsupabase/baseline.sql, and asserts how many write literals it read so the scan cannot pass vacuously. -
[Observed]Insrc/app/api: 178 destructures readerrorand 151 take{ data }alone. Most of the latter are authorisation or lookup reads where a null fails closed, e.g.api-auth.ts. There are 14tryblocks in the API tree. Errors go back to the client verbatim (error: x.message) about 121 times. That is honest for users but leaks DB detail (see 09-security F9). 19 handlers callawait request.json()without a.catch, so a malformed body becomes an unhandled 500 rather than a 400. -
[Observed]Write sequences are rolled back by hand where a multi-row change must be all-or-nothing (e.g. theWriteshelper inframes/card-writes.ts). There is no DB transaction or RPC for these, so atomicity rests on compensating writes. -
[Observed]There are noerror.tsxorglobal-error.tsxboundaries anywhere insrc/app(find src/app -name error.tsxreturns nothing), so a server-component exception shows Next's default error page.
Q5. Logging and observability
[Observed]11console.*calls in non-test code (9error, 2warn). Only 2 are in API routes (admin/invite,feedback). The rest are inlib/activity.ts,lib/review-notify.ts, the dev auto-sign-in and two pages. Most failures are therefore either returned to the client or swallowed (.catch(() => {})on emails, e.g.forgot-password/route.ts:58).[Observed]There is no structured logger, error tracker, APM or log drain inpackage.json(no Sentry, pino, winston, OpenTelemetry or Datadog). Theapp_errorstable exists in the schema but has no writer (onlytypes/database.tsand the QA store mention it).activity_log(vialogActivity/track) is a product-usage audit trail, not operational telemetry. The repo says so itself indesign/COMPLIANCE.md§8.[Inferred]Production incidents can be diagnosed only from Vercel's default function logs and the Supabase dashboard.
Q6. Input validation
[Observed]There is no schema-validation library (no zod, valibot, yup or joi inpackage.json). Validation is hand-rolled per route: 122typeofchecks insrc/app/api, length clamps (.trim().slice(0, 240)), allow-lists (ALLOWED_FIELDS,EDITABLE_FIELDSinad-units/[unitId]/fields.ts,VALID_ROLES,isDiscipline) and a sharedUUID_RE(src/app/api/track/actions.ts).[Inferred]The checks are generally careful, but there is no single contract per endpoint, so request shapes are documented only in prose (docs/API.md) and nothing guarantees that a new field is validated. A concrete miss is in 09-security F4:brief_textis sanitised on one route and accepted raw on another.
Q7. Complexity and very large files
Top 12 non-test source files by line count (wc -l, src/):
| Lines | File | Note |
|---|---|---|
| 3,349 | src/app/qa/store.ts | QA harness store; contains matches, CCN 138 (the worst function in the repo) |
| 1,373 | src/app/(app)/campaign/[id]/review/review-view.tsx | Review area UI |
| 1,218 | src/app/(app)/campaign/[id]/ads/ad-unit-panel.tsx | Ad-unit detail panel |
| 1,150 | src/types/database.ts | Types (expected to be large) |
| 970 | src/app/(app)/workspace/admin/admin-view.tsx | Admin UI |
| 828 | src/lib/copy-deck-parser.ts | Parser (85% covered by tests) |
| 827 | src/app/(app)/campaign/[id]/ads/ad-units-grid.tsx | Grid |
| 756 | src/app/(app)/workspace/campaign/[id]/brief/brief-view.tsx | Brief editor |
| 629 | src/app/(app)/workspace/campaign/[id]/handover/final-grid.tsx | Final grid |
| 617 | src/app/(app)/campaign/[id]/members/members-page.tsx | Members |
| 572 | src/app/(app)/campaign/[id]/utms/utm-generator.tsx | UTMs |
| 554 | src/app/(app)/new-campaign-modal.tsx | Classic modal |
High-complexity hot spots from the brief: for-you.ts forYouItems CCN 109; ad-unit PATCH 66; brief PATCH 64; review/files/[fileId] PATCH 59; render-push POST 53; jobs POST 52; cron purge-review GET 44. [Inferred] These are the business-critical write paths. Combined with 0.9% line coverage of src/app/api (brief), they are the riskiest code to modify. Several UI components exceed 800–1,300 lines and mix data fetching, state and rendering.
Q8. Duplication: worst offenders (jscpd JSON report)
| Duplicated lines | File pair | Nature |
|---|---|---|
| 78 | workspace/campaign/[id]/layout.tsx ↔ qa/_lib/campaign-shell.tsx | Harness mirrors the production shell |
| 46 | campaigns/[id]/media-strategy/route.ts ↔ campaigns/[id]/wrapup/route.ts | Same list-CRUD scaffold |
| 46 | campaigns/[id]/review/files/route.ts ↔ upload/[token]/register/route.ts | Registration logic copied "minimally" (comment at register/route.ts:6-8) |
| 44 | workspace/campaigns/page.tsx ↔ qa/campaigns/page.tsx | Harness mirror |
| 40 | campaigns/[id]/plan/route.ts ↔ qa/store.ts | Harness re-implements route logic |
| 39 | admin/users/[id]/route.ts ↔ campaigns/[id]/route.ts | Inline admin-check boilerplate |
| 37 | admin/users/[id]/route.ts ↔ tasks/[id]/route.ts | Same |
| 36 | admin/ad-formats/[id]/route.ts ↔ admin/campaign-fields/[id]/route.ts | Local requireStaff copies |
| 27 | media-strategy/route.ts ↔ questions/route.ts | List-CRUD scaffold |
Files with the most duplicated lines overall: admin/users/[id]/route.ts (120), qa/store.ts (115), workspace/campaign/[id]/layout.tsx (108), ad-units/[id]/route.ts (100), plan/route.ts (100).
[Observed] Two systemic patterns drive this. First, the QA harness re-implements route behaviour in store.ts, a parallel implementation that must be kept in sync by hand. scripts/harness-contract.js checks only that routes are matched, not that behaviour is. Second, authorisation boilerplate: 25 inline role === 'admin' checks and 7 local requireStaff/requireAdmin functions exist alongside isStaff in api-auth.ts.
Q9. Dead and commented-out code
-
[Observed]CLAUDE.md says the checklist and build files are unreachable and "to be deleted". They are still present:src/app/(app)/campaign/[id]/checklist/(checklist-view.tsx432 +page.tsx35 lines)src/app/(app)/campaign/[id]/build/(build-view.tsx328 +page.tsx26)- redirect stubs
assets/page.tsx(12) andmembers/page.tsx(12) e2e/checklist.spec.ts(80)
That is about 925 lines. They are reachable only as source, because
next.config.tsredirects their URLs. (members/members-page.tsxis not dead: it is imported bycampaign-header.tsxandworkspace/.../header-tile.tsx.) -
[Observed]Further deliberately orphaned code is kept as "groundwork": the brief, review and sign-off pipeline (/timeline,/debrief) and the Studio schema left behind by migration 059 (tables with no readers). See CLAUDE.md "Parked decisions". -
[Observed]Commented-out code is nearly absent: 5 lines match// const|let|await|return|import|if (|export|function. There are 0TODO/FIXME/HACK/XXXmarkers. -
[Observed]One declared dependency is unused (google-auth-library, never imported), plusdate-fnsper module 08. The feature flagSHOW_ASK = false(src/lib/flags.ts:10) keeps the whole Ask sheet compiled but inert.
Q10. Naming, readability and comment style
[Observed]Naming is consistent and domain-driven (canEditArea,carriesLink,purgableAfterHandover,copyPatchFromParsed). Single sources of truth are named and referenced, e.g. "ONE list" intrack/actions.tsand "the one rule" inplacement.ts.[Observed]Comments are unusually dense (about 28 per 100 code lines) and narrative. They record decisions, past bugs and their dates in prose. Examples: the header ofad-units/[unitId]/route.ts, 30-line essays at the top ofpreview/[token]/custody/route.tsandwrite-results.test.ts, and migration numbers cited inline about 250 times ((0NN)). CLAUDE.md (832 lines) continues the same voice.[Inferred]Strength: the "why" behind non-obvious rules is preserved next to the code, which mitigates the squashed git history (single commit). Weaknesses: (a) the reading cost is high; (b) prose decays. 09-security found CLAUDE.md claiming email names are escaped and that harness routes 404 outside QA, and both are untrue in code. (c) Some comments describe history rather than current behaviour, which a new maintainer must filter.
🧾 Evidence
- Facts reused from the shared brief: tsc, lint, jscpd totals, lizard, cloc and coverage figures.
cat tsconfig.json eslint.config.mjsgrep -rn "eslint-disable" src | wc -l→ 27, with breakdown by rule.@ts-ignore/@ts-expect-error/@ts-nocheck→ 0.as unknown as→ 82 (6 outsideapp/qaand tests).as any→ 1.grep -rnE "console\.(log|warn|error|info|debug)" src→ 11.- In
src/app/api:const { data } = await→ 151;{ … error … } = await→ 178;try {→ 14;error: x.message→ 121;await request.json()without catch → 19;typeof→ 122;role (!==|===) 'admin'→ 25; localrequire(Staff|Admin)definitions → 7. python3over/tmp/claude-0/dd/jscpd/jscpd-report.json, aggregating duplicated lines by file pair and by file.find src -name '*.ts*' ! -name '*.test.*' | xargs wc -l | sort -rnfor the top files.lsandwc -lofsrc/app/(app)/campaign/[id]/{checklist,build,assets,members}ande2e/checklist.spec.ts;grepfor importers ofmembers-page.- Read:
src/lib/write-results.test.ts,src/lib/api-auth.ts,src/lib/supabase/*.ts,src/app/api/campaigns/[id]/ad-units/[unitId]/route.ts(header),frames/card-writes.ts,package.json.
⚠️ Risks
| # | Risk | Severity | Effort |
|---|---|---|---|
| C1 | Concentrated complexity in core write paths (CCN 50–110) with about 1% API test coverage makes regressions likely when changing approval, import or brief logic | 🟠 High | L |
| C2 | No error tracking or structured logs; production failures are invisible unless a user reports them | 🟠 High | S–M |
| C3 | Hand-rolled request validation; inconsistencies across routes have already produced a security gap (brief sanitisation) | 🟡 Medium | M |
| C4 | The QA harness duplicates route logic (store.ts, 3,349 lines), so behaviour can drift between the tested harness and production | 🟡 Medium | M |
| C5 | Very large UI components (800–1,400 lines) slow onboarding and review | 🟡 Medium | M–L |
| C6 | Narrative documentation drifts from code (two false claims found); readers may trust prose over behaviour | 🟡 Medium | S (ongoing) |
| C7 | Dead or parked code (about 925 lines of unreachable pages plus parked pipelines and tables) and unused dependencies | ⚪ Low | S |
| C8 | Authorisation boilerplate duplicated (25 inline admin checks), so a future rule change must be made in many places | ⚪ Low | S |
| C9 | Missing noUncheckedIndexedAccess; 19 unguarded request.json(); no error boundaries | ⚪ Low | S |
✅ Recommendations
| Priority | Action | Effort |
|---|---|---|
| P1 | Add an error tracker (e.g. Sentry) with route and server-component instrumentation, plus error.tsx/global-error.tsx boundaries. Wire or drop app_errors. | S–M |
| P1 | Introduce a schema library (zod or valibot) and a small route() wrapper that does auth + body parse + validation, returning 400s. Migrate the highest-risk write routes first (ad-unit PATCH, brief, campaign PATCH, members, import-copy). | M |
| P1 | Break up the CCN > 50 functions (forYouItems, ad-unit PATCH, brief PATCH, review/files/[fileId] PATCH, render-push, jobs) into tested pure helpers in src/lib, following the pattern already used for copy-import.ts/approval-events.ts. | L |
| P2 | Replace inline admin checks and local requireStaff copies with one requireAdmin in api-auth.ts. Factor the list-CRUD scaffold (media-strategy/questions/wrapup/products). | S |
| P2 | Extend write-results.test.ts to src/lib. Enable noUncheckedIndexedAccess and fix the fallout. Add a lint rule for complexity (warn > 25) and max-lines for new files. | S–M |
| P2 | Reduce harness drift: either generate harness handlers from the real routes against an in-memory DB, or add contract tests that run identical cases against both. | M |
| P3 | Delete the unreachable checklist and build pages, redirect stubs, e2e/checklist.spec.ts and unused dependencies (google-auth-library, date-fns), as CLAUDE.md already decided. | S |
| P3 | Adopt a comment convention: keep "why", move dated history into ADRs or the changelog, and add a periodic doc-vs-code check (the two false claims in Q10 are a start). | S |