# Coding Conventions

**Analysis Date:** 2026-02-26

## Naming Patterns

**Files:**
- `kebab-case` for all TypeScript/TSX source files: `round-engine.ts`, `filtering-dashboard.tsx`, `ai-errors.ts`
- `kebab-case` for route directories under `src/app/`: `(admin)/admin/juries/`, `[roundId]/`
- Exception: Next.js reserved names remain as-is: `page.tsx`, `layout.tsx`

**Components:**
- `PascalCase` for component functions: `FilteringDashboard`, `EmptyState`, `JuriesPage`
- Page components follow the suffix pattern `XxxPage`: `JuriesPage`, `RoundDetailPage`, `AuditLogPage`
- Sub-components within a file follow `XxxSection`, `XxxCard`, `XxxDialog`

**Functions:**
- `camelCase` for all functions and methods: `activateRound`, `resolveEffectiveCap`, `createTestUser`
- Service functions are named by operation + domain: `activateRound`, `closeRound`, `batchTransitionProjects`
- Boolean functions prefixed with `is`, `has`, `should`, `can`: `shouldRetry`, `isParseError`, `shouldLog`

**Variables:**
- `camelCase` for all local variables
- `SCREAMING_SNAKE_CASE` for module-level constants: `BATCH_SIZE = 50`, `SYSTEM_DEFAULT_CAP`, `VALID_ROUND_TRANSITIONS`
- Enum-like lookup objects in `SCREAMING_SNAKE_CASE`: `ERROR_PATTERNS`, `LOG_LEVELS`

**Types:**
- `type` keyword preferred over `interface` per CLAUDE.md — but both exist in practice
- `interface` is used for component props in some files (e.g., `ButtonProps`, `EmptyStateProps`), `type` used in others
- Prisma-derived types use `type` aliases with `z.infer<typeof Schema>`: `type EvaluationConfig = z.infer<typeof EvaluationConfigSchema>`
- Prop types: `type XxxProps = { ... }` (preferred in most components), `interface XxxProps { ... }` (used in deliberation, some UI components)
- Export complex input types from `src/types/competition.ts`: `CreateCompetitionInput`, `UpdateRoundInput`

## Code Style

**Formatting:**
- Prettier 3.4.2 with `prettier-plugin-tailwindcss` for class sorting
- No `.prettierrc` found — uses Prettier defaults: 2-space indent, double quotes, trailing commas (ES5), 80-char print width
- Single quotes confirmed absent in codebase: all string literals use double quotes
- Tailwind classes sorted automatically by the plugin on format

**Linting:**
- ESLint 9.x with `eslint-config-next` (Next.js configuration)
- Run via `npm run lint` (calls `next lint`)
- No custom rules file found — relies on Next.js default rules

**TypeScript:**
- Strict mode enabled in `tsconfig.json` (`"strict": true`)
- `noEmit: true` — TypeScript used for type checking only, not transpilation
- Target: ES2022
- Module resolution: `bundler` (Next.js Turbopack compatible)
- Path alias: `@/*` maps to `./src/*`

## Import Organization

**Order (observed in client components):**
1. `'use client'` directive (if needed)
2. React/framework hooks: `import { useState, useEffect } from 'react'`
3. Next.js imports: `import { useRouter } from 'next/navigation'`, `import Link from 'next/link'`
4. tRPC client: `import { trpc } from '@/lib/trpc/client'`
5. UI libraries: shadcn/ui components `import { Button } from '@/components/ui/button'`
6. Icons: `import { Loader2, Save } from 'lucide-react'`
7. Internal utilities/helpers: `import { cn } from '@/lib/utils'`
8. Internal components: `import { FilteringDashboard } from '@/components/admin/round/...'`
9. Types: `import type { EvaluationConfig } from '@/types/competition-configs'`

**Order (observed in server/service files):**
1. `import { z } from 'zod'` (validation)
2. `import { TRPCError } from '@trpc/server'` (errors)
3. tRPC router/procedures: `import { router, adminProcedure } from '../trpc'`
4. Internal services/utilities: `import { logAudit } from '@/server/utils/audit'`
5. Type imports at end: `import type { PrismaClient } from '@prisma/client'`

**Path Aliases:**
- Use `@/` prefix for all internal imports: `@/components/ui/button`, `@/server/services/round-engine`
- Never use relative `../../` paths for cross-directory imports
- Relative paths (`./`, `../`) only within the same directory level

## React Component Conventions

**Server vs Client Components:**
- Default to **Server Components** — do not add `'use client'` unless needed
- Layouts (`layout.tsx`) are server components: they call `await requireRole()`, fetch data directly from Prisma, and pass to client wrappers
- Pages that use tRPC hooks, `useState`, or browser APIs must be `'use client'`
- The pattern: server layout fetches session/editions → passes to client wrapper → client components handle interactive state

**Client Component Pattern:**
```tsx
'use client'

import { useState } from 'react'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'

type XxxProps = {
  competitionId: string
  roundId: string
}

export default function XxxPage() { ... }
// Sub-components in same file as local functions (not exported)
function XxxSection({ competition }: XxxSectionProps) { ... }
```

**Server Layout Pattern:**
```tsx
import { requireRole } from '@/lib/auth-redirect'
import { prisma } from '@/lib/prisma'

export default async function AdminLayout({ children }: { children: React.ReactNode }) {
  const session = await requireRole('SUPER_ADMIN', 'PROGRAM_ADMIN')
  const data = await prisma.program.findMany({ ... })
  return <ClientWrapper data={data}>{children}</ClientWrapper>
}
```

**Props with Params (Next.js 15):**
```tsx
type PageProps = {
  params: Promise<{ roundId: string; projectId: string }>
}
export default function Page({ params: paramsPromise }: PageProps) {
  const params = use(paramsPromise)  // React.use() for async params
  ...
}
```

## tRPC Router Conventions

**Procedure Selection:**
- `adminProcedure` for CRUD on competition/round/jury entities
- `protectedProcedure` for shared read access across roles
- `juryProcedure` for jury-only operations
- Role checks within procedure body use `userHasRole(ctx.user, 'ROLE')` for per-entity authorization

**Input Validation:**
- All inputs validated with Zod `.input(z.object({ ... }))`
- Use `.min()`, `.max()`, `.regex()` for strings
- Use `.int().positive()` for ID/count integers
- Use `.optional().nullable()` for optional fields with null support
- Inline schema definition (not shared schema objects) per router

**Mutation Pattern:**
```typescript
create: adminProcedure
  .input(z.object({
    name: z.string().min(1).max(255),
    slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
  }))
  .mutation(async ({ ctx, input }) => {
    // 1. Check business rules (conflict, not found)
    const existing = await ctx.prisma.xxx.findUnique({ where: { slug: input.slug } })
    if (existing) {
      throw new TRPCError({ code: 'CONFLICT', message: '...' })
    }
    // 2. Perform operation
    const result = await ctx.prisma.xxx.create({ data: input })
    // 3. Audit log (for mutations)
    await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'CREATE', ... })
    // 4. Return result
    return result
  })
```

**Error Codes:**
- `NOT_FOUND` — entity doesn't exist
- `CONFLICT` — duplicate slug/unique constraint
- `FORBIDDEN` — user lacks permission for specific entity
- `UNAUTHORIZED` — not logged in (handled by middleware)
- `BAD_REQUEST` — invalid business state (e.g., no active form)

## Error Handling

**tRPC Routers (user-facing errors):**
- Always throw `TRPCError` with `{ code, message }` — never plain `throw new Error()`
- Message should be human-readable: `'Competition not found'`, not `'Competition_NOT_FOUND'`
- Use `findUniqueOrThrow` / `findFirstOrThrow` for implicit 404s on required relations

**Service Layer (internal errors):**
- Services return result objects `{ success: boolean, errors?: string[] }` — they do NOT throw
- Callers check `result.success` before proceeding
- Error message pattern: `error instanceof Error ? error.message : 'Unknown error during X'`
- Non-fatal side effects (notifications, emails) are wrapped in separate try/catch and logged but never propagate

**AI Services:**
- Use `classifyAIError()` from `src/server/services/ai-errors.ts` for all OpenAI errors
- Wrap AI calls with `withAIErrorHandling(fn, fallback)` for unified error + fallback handling
- All AI errors logged with service tag: `console.error('[AI Assignment] failed:', ...)`

**Client-Side:**
- Mutation errors displayed via `toast.error(err.message)` from Sonner
- Success via `toast.success('...')` with query invalidation: `utils.xxx.yyy.invalidate()`
- Loading states tracked via `mutation.isPending` and `query.isLoading`

## Logging

**Framework:** Custom structured logger at `src/lib/logger.ts`

**Usage:**
```typescript
import { logger } from '@/lib/logger'
logger.info('RoundEngine', 'Round activated', { roundId, competitionId })
logger.error('Storage', 'Upload failed', error)
logger.warn('Filtering', 'Non-fatal error in document check', retroError)
```

**Tag Convention:** `[ServiceName]` prefix in brackets — `'RoundEngine'`, `'AIFiltering'`, `'Storage'`

**Direct console usage** (still common in routers, not yet migrated to logger):
- Tagged format: `console.log('[FeatureName] message', data)`
- Always uses bracket tag: `'[Filtering]'`, `'[Assignment]'`, `'[COI]'`

**Log Level Defaults:**
- Development: `debug` (all logs)
- Production: `warn` (warns and errors only)
- Override via `LOG_LEVEL` env var

## Comments

**When to Comment:**
- All exported service functions get a JSDoc-style comment explaining purpose and invariants
- Inline comments for non-obvious business logic: `// re-include after rejection`, `// Bounded to admin max`
- Section header separators in large files using box-drawing chars: `// ─── Section Name ──────`
- `// =====` separators for major logical sections in long files

**JSDoc/TSDoc:**
- Used on exported functions in services and utilities
- Standard `/**` block with plain description — no `@param`/`@returns` annotations in most code
- Routers use `/** procedure description */` above each procedure for documentation

**TODO Comments:**
- Present but sparse — only 3 found in entire codebase (deliberation page, award router)
- Format: `// TODO: description`

## Function Design

**Size:** Service functions can be long (100-200+ lines) for complex state machines; router procedures typically 20-60 lines

**Parameters:**
- Services accept `(entityId: string, actorId: string, prisma: PrismaClient)` — explicit prisma injection for testability
- Router procedures destructure `{ ctx, input }` — never access `ctx.prisma` outside routers

**Return Values:**
- Queries return data directly or throw `TRPCError`
- Mutations return the created/updated record
- Services return typed result objects: `RoundTransitionResult`, `BatchProjectTransitionResult`
- Async service results always typed: `Promise<RoundTransitionResult>`

## Module Design

**Exports:**
- Single responsibility: each service file exports one domain's functions
- Named exports preferred over default exports for services and utilities
- Default exports used only for React components (`export default function Page()`)

**Barrel Files:**
- Used sparingly — only for chart components (`src/components/charts/index.ts`), form steps, and storage utilities
- Most imports are direct path imports: `import { FilteringDashboard } from '@/components/admin/round/filtering-dashboard'`

**Prisma Transactions:**
- Use `ctx.prisma.$transaction(async (tx) => { ... })` for multi-step mutations
- Always pass `tx` (transaction client) through nested operations
- Sequential array syntax `ctx.prisma.$transaction([op1, op2])` for simple atomic batches

---

*Convention analysis: 2026-02-26*
