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
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:
_INTERNALSobjects 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
InviteUserFormfrominvite-user-form.tsx) - Page components:
export default function ComponentName()at end of file - Form components: follow
[Resource]Formnaming (e.g.,InviteUserForm,SignInForm,UserForm)
Code Style
Formatting:
- TypeScript strict mode enabled (
"strict": trueintsconfig.json) - No explicit formatter config (ESLint handles style)
- Indentation: 2 spaces (inferred from existing code)
Linting:
- ESLint:
eslint.config.mjswith 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:
- Node.js built-ins (e.g.,
fs,path) - Third-party packages (e.g.,
next/server,zod,vitest) - Type imports (e.g.,
import type { ... } from '...') - Local imports from
@/*(using path alias) - Local imports from
./(relative, less common)
Path Aliases:
- Configured as
"@/*": ["./*"]intsconfig.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/catchblocks - API routes: catch errors and return
NextResponse.json({ error, message }, { status }) - Standard status codes:
500for runtime errors,503for missing/bad config,401/403from 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
nulloverundefinedfor missing values - Use discriminated unions for success/error returns in critical paths (see analyzer pipeline)
Module Design
Exports:
- Prefer
exportat declaration point rather than grouped re-exports - One main export per file (exception: barrel files in
components/ui/) - Internal utilities prefixed with underscore:
_INTERNALSobject for test access
Barrel Files:
components/ui/index.tsexports all shadcn primitiveslib/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-tableviacomponents/admin/DataTable.tsxwrapper - Example:
<DataTable columns={columns} data={data} />
Modals:
- Use
components/admin/DetailModal.tsxfor entity details - Follows card + tabs pattern (formatted/raw)
Navigation:
- Use
components/navigation/app-navigation.tsx(NavigationMenufrom Radix) - Dropdowns prefer
@radix-ui/react-dropdown-menuover submenus
Toasts:
- Use
sonnerlibrary: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()withzodResolver(), then<Form>wrapper from shadcn
Charts:
- Use
rechartsfor data visualization (e.g.,<BarChart>,<LineChart>)
What NOT to Introduce
Forbidden:
- No ORMs (Prisma, TypeORM, etc.) — use
postgresClientsingleton and manual transforms - No server actions (
'use server') — use API routes called viafetch()from clients - No additional state libraries (SWR, react-query, TanStack Query) — match local
useState+fetchpattern - 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:
- Number it sequentially: if last is
041_create_engagement_tables.sql, next is042_*.sql - Use
IF NOT EXISTSfor CREATE statements - Use
ON CONFLICT DO NOTHINGfor seed data INSERT - Never drop columns or tables without explicit guard
- 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