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

9.3 KiB

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):

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:

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:

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:
    /**
     * 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:

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:

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