# 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<T>()`)
- 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<T>` for optional object shapes

**Return Values:**
- Async functions always return `Promise<T>` 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: `<DataTable columns={columns} data={data} />`

**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 `<Form>` wrapper from shadcn

**Charts:**
- Use `recharts` for data visualization (e.g., `<BarChart>`, `<LineChart>`)

## 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*
