# Coding Conventions **Analysis Date:** 2026-05-03 ## Naming Patterns **Files:** - kebab-case for all files and directories (e.g., `postgres-client.ts`, `invite-user-form.tsx`, `entity-sync.ts`) - Nested directories use kebab-case (e.g., `lib/services/analyzer/`, `components/admin/users/`) **Functions:** - camelCase for all functions (e.g., `getAutotaskClient()`, `transformCompany()`, `extractExplicitFromText()`) - Factory functions prefixed with `get` (e.g., `getAutotaskClient()`, `getDattoRmmClient()`) - Helper functions suffixed with descriptive intent (e.g., `relTime()`, `deriveSigningKey()`) - Private/internal functions prefixed with underscore: `_INTERNALS` objects expose internals for test access **Variables:** - camelCase for all variables (e.g., `isLoading`, `setData`, `ticketNumber`) - Constants in UPPER_SNAKE_CASE (e.g., `MAX_EXPLICIT_LINKS`, `TICKET_NUMBER_REGEX`) - Database column names are always snake_case (e.g., `company_name`, `is_active`, `created_at`) **Types:** - PascalCase for all type names (e.g., `ClassificationRule`, `WorkflowExecution`, `TicketData`) - Single-letter generics are acceptable (e.g., `queryEntity()`) - Union types as literal strings (e.g., `type RuleType = 'branch_routing' | 'ticket_type'`) **Components:** - PascalCase exported from kebab-case files (e.g., export `InviteUserForm` from `invite-user-form.tsx`) - Page components: `export default function ComponentName()` at end of file - Form components: follow `[Resource]Form` naming (e.g., `InviteUserForm`, `SignInForm`, `UserForm`) ## Code Style **Formatting:** - TypeScript strict mode enabled (`"strict": true` in `tsconfig.json`) - No explicit formatter config (ESLint handles style) - Indentation: 2 spaces (inferred from existing code) **Linting:** - ESLint: `eslint.config.mjs` with Next.js config (`eslint-config-next/core-web-vitals`, `eslint-config-next/typescript`) - No additional custom rules beyond Next.js defaults - Type checking: `npx tsc --noEmit --pretty` (must pass before commit) ## Import Organization **Order:** 1. Node.js built-ins (e.g., `fs`, `path`) 2. Third-party packages (e.g., `next/server`, `zod`, `vitest`) 3. Type imports (e.g., `import type { ... } from '...'`) 4. Local imports from `@/*` (using path alias) 5. Local imports from `./` (relative, less common) **Path Aliases:** - Configured as `"@/*": ["./*"]` in `tsconfig.json` - Use `@/lib/...`, `@/components/...`, `@/app/...` always - Never use relative paths like `../../../` for imports **Example import block** (from `/opt/stacks/pulse/components/admin/users/invite-user-form.tsx`): ```typescript import { useState } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Loader2, Send } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; ``` ## Error Handling **Pattern:** - All async functions use `try/catch` blocks - API routes: catch errors and return `NextResponse.json({ error, message }, { status })` - Standard status codes: `500` for runtime errors, `503` for missing/bad config, `401`/`403` from auth helpers - Error messages: include `error instanceof Error ? error.message : 'fallback message'` **Example from `/opt/stacks/pulse/app/api/companies/route.ts`:** ```typescript export async function GET(request: NextRequest) { try { const result = await postgresClient.query('SELECT * FROM companies ...'); return NextResponse.json({ companies: result.rows.map(transformCompany) }); } catch (error) { console.error('Error fetching companies from database:', error); return NextResponse.json( { error: error instanceof Error ? error.message : 'Failed to fetch companies' }, { status: 500 } ); } } ``` **Client-side:** Use try/catch with `.json()` nulling: ```typescript const [overviewRes, trendsRes] = await Promise.all([ fetch('/api/dashboard/overview', { cache: 'no-store' }), fetch('/api/dashboard/trends', { cache: 'no-store' }), ]); if (!overviewRes.ok) { const body = (await overviewRes.json().catch(() => ({}))) as { error?: string }; throw new Error(body.error ?? `HTTP ${overviewRes.status}`); } ``` ## Logging **Framework:** Plain `console` (no structured logging library) **Patterns:** - `console.error()` for exceptions caught in try/catch (usually in API routes and services) - Include context: `console.error('Failed to fetch companies:', error)` - No `console.log()` for debugging (remove before commit per linter checks) ## Comments **When to Comment:** - Explain *why*, not what (code shows the what) - Non-obvious logic or business rules - Performance-critical sections - Workarounds or hacks (mark with `// HACK:` or `// NOTE:`) **JSDoc/TSDoc:** - Used sparingly on complex functions - Example from `lib/services/analyzer/link-discovery.ts`: ```typescript /** * Marks refs in a RELATED TICKETS: block as high confidence */ export function extractExplicitFromText(text: string, source: string) { ... } ``` - Not required for simple getters/setters or obvious functions ## Function Design **Size:** - Keep functions focused: one responsibility per function - Aim for <50 lines for page components, <30 for utilities - Complex operations broken into smaller helpers **Parameters:** - Prefer object parameters for >3 arguments - Don't use `any` — use specific types - Use `Partial` for optional object shapes **Return Values:** - Async functions always return `Promise` explicitly - Prefer `null` over `undefined` for missing values - Use discriminated unions for success/error returns in critical paths (see analyzer pipeline) ## Module Design **Exports:** - Prefer `export` at declaration point rather than grouped re-exports - One main export per file (exception: barrel files in `components/ui/`) - Internal utilities prefixed with underscore: `_INTERNALS` object for test access **Barrel Files:** - `components/ui/index.ts` exports all shadcn primitives - `lib/types/` has domain-specific barrel files (e.g., `lib/types/workflow.ts`, `lib/types/autotask.ts`) - Avoid deep nesting — import from files, not directories unless barrel exists ## Database Transformations **Pattern:** All columns are `snake_case` in database. API responses transform to `camelCase`. **Example from `/opt/stacks/pulse/app/api/companies/route.ts`:** ```typescript function transformCompany(row: any) { return { id: Number(row.id), companyName: row.company_name, companyType: row.company_type, isActive: row.is_active, // ... all snake_case → camelCase }; } ``` No ORM is used — all transforms are manual per handler. ## Shared Components & Libraries **UI Components:** - shadcn primitives live in `components/ui/` - Feature-specific components in sibling directories (e.g., `components/dashboard/`, `components/admin/`) - Icons: Always use `lucide-react` (e.g., `import { Loader2, Send } from 'lucide-react'`) **Tables:** - Use `@tanstack/react-table` via `components/admin/DataTable.tsx` wrapper - Example: `` **Modals:** - Use `components/admin/DetailModal.tsx` for entity details - Follows card + tabs pattern (formatted/raw) **Navigation:** - Use `components/navigation/app-navigation.tsx` (`NavigationMenu` from Radix) - Dropdowns prefer `@radix-ui/react-dropdown-menu` over submenus **Toasts:** - Use `sonner` library: `import { toast } from 'sonner'` - Patterns: `toast.success()`, `toast.error()`, `toast.info()` **Forms:** - Use `react-hook-form` + Zod for validation - Only in admin/auth forms — NOT in every page - Pattern: `useForm()` with `zodResolver()`, then `
` wrapper from shadcn **Charts:** - Use `recharts` for data visualization (e.g., ``, ``) ## What NOT to Introduce **Forbidden:** - No ORMs (Prisma, TypeORM, etc.) — use `postgresClient` singleton and manual transforms - No server actions (`'use server'`) — use API routes called via `fetch()` from clients - No additional state libraries (SWR, react-query, TanStack Query) — match local `useState` + `fetch` pattern - No change to authentication (Better Auth is final) - No editing of committed migrations — always create new numbered ones **Rationale:** - Keeps codebase lean and explicit - Reduces abstraction overhead - Makes data flow (DB → API → Client) visible ## Migrations **Creating a new migration:** 1. Number it sequentially: if last is `041_create_engagement_tables.sql`, next is `042_*.sql` 2. Use `IF NOT EXISTS` for CREATE statements 3. Use `ON CONFLICT DO NOTHING` for seed data INSERT 4. Never drop columns or tables without explicit guard 5. Include audit columns: `created_at`, `updated_at`, `synced_at`, `is_deleted`, `deleted_at` **Example structure:** ```sql CREATE TABLE IF NOT EXISTS new_table ( id BIGINT PRIMARY KEY, name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO new_table (id, name) VALUES (1, 'Example') ON CONFLICT DO NOTHING; ``` **Note:** Migrations are applied in alphabetical order. Existing duplicates (002, 004, 009) exist; respect that order. --- *Convention analysis: 2026-05-03*