wulf-pulse/.planning/codebase/CONVENTIONS.md
lorentz 9658640c04 fix(04-01): restore phase 2/3 work lost by worktree soft-reset
The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
2026-05-03 18:01:14 -04:00

263 lines
9.3 KiB
Markdown

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