Documentation source
Contributing
Coding standards, testing expectations, file organization, auth patterns, and quality rules for the Sprinter Platform.
# Contributing
This guide covers the standards and conventions for contributing to the Sprinter Platform. These rules are enforced across the codebase and apply to all contributors -- human and AI alike.
## Coding standards
### Use shadcn/ui for all interactive UI
Every button, input, select, card, dialog, and interactive element must come from `@/components/ui/`. Never use raw HTML `<button>`, `<input>`, or `<select>` elements.
```tsx
// Correct
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
// Wrong
<button onClick={handleClick}>Save</button>
<input type="text" onChange={handleChange} />
```
### No hardcoded colors
Never use Tailwind color classes like `text-blue-600`, `bg-red-50`, or `text-green-800`. Use CSS variables from the design system:
| Need | Class |
|---|---|
| Primary text/bg | `text-primary`, `bg-primary` |
| Muted backgrounds | `bg-muted`, `text-muted-foreground` |
| Borders | `border` |
| Success status | `text-status-success`, `bg-status-success/10` |
| Error status | `text-status-error`, `bg-status-error/10` |
| Warning status | `text-status-warning`, `bg-status-warning/10` |
| Info status | `text-status-info`, `bg-status-info/10` |
All neutral chrome colors must be zero-chroma: `oklch(X 0 0)`. Only `--primary` has a hue (deep navy: `oklch(0.35 0.05 260)`).
### No "entities" in user-facing copy
In user-facing UI, use "records", "data", "data types", or "items" instead of "entities". The word "entity" is fine in admin pages, code comments, variable names, and API routes.
### Use `cn()` for conditional classes
Always use the `cn()` utility from `lib/utils.ts` for conditional class merging:
```tsx
import { cn } from "@/lib/utils";
<div className={cn("p-4", isActive && "bg-primary text-primary-foreground")} />
```
### Entity types are DB-driven
Never hardcode entity type slugs (like `"opportunity"` or `"company"`) in platform code (`features/` except `features/custom/`). Entity types are defined in the database. If platform behavior depends on a specific entity type, make it configurable via agent config or tenant settings.
### Zod validation on all API routes
Every POST, PUT, and PATCH handler must validate input with a Zod schema at the route boundary:
```typescript
import { z } from "zod";
import { apiErrorResponse } from "@/lib/api-utils";
const updateSchema = z.object({
title: z.string().min(1),
content: z.record(z.unknown()).optional(),
});
export async function PATCH(request: Request) {
const body = await request.json();
const parsed = updateSchema.safeParse(body);
if (!parsed.success) {
return apiErrorResponse("Invalid input", 400);
}
// ... use parsed.data
}
```
## Component guidelines
### 200-line limit
Components must stay under 200 lines. If a component approaches that limit, extract sub-components into the same directory:
```
features/entities/components/
entity-detail/
entity-detail.tsx # Main component (~150 lines)
entity-header.tsx # Extracted header
entity-sidebar.tsx # Extracted sidebar
entity-field-list.tsx # Extracted field list
```
### Accessibility
- Every icon-only button must have an `aria-label`
- Custom interactive elements need `tabIndex` and `onKeyDown` handlers
- Use semantic HTML where possible (even with shadcn/ui wrappers)
### Card padding convention
The `Card` component uses `py-4 gap-3` (vertical) + `px-4` on children (horizontal) for uniform padding. Block components should not add manual padding overrides -- use Card defaults.
## Auth patterns
### Always use the auth adapter
Import auth functions from `@/features/tenant/auth` -- never call Supabase auth directly:
```typescript
import { getUserId, requireAuth, requireAdmin, hasPermission, requirePermission } from "@/features/tenant/auth";
// API route: require authentication
export async function GET() {
const context = await requireAuth(); // throws 401 if not authenticated
// context.userId, context.tenantId, context.role available
}
// API route: require admin
export async function DELETE() {
const context = await requireAdmin(); // throws 403 if not admin
}
// API route: check specific permission
export async function PATCH() {
const allowed = await hasPermission("entities.team.update");
if (!allowed) return apiErrorResponse("Forbidden", 403);
}
```
### Supabase client selection
- **Authenticated client** (`createClient()`) -- for user-scoped reads/writes. RLS applies.
- **Admin client** (`createAdminClient()`) -- only for cross-user/system operations: tenant management, user provisioning, background jobs, extraction.
```typescript
// Correct: authenticated client for user operations
import { createClient } from "@/lib/supabase/server";
const supabase = await createClient();
// Correct: admin client for system operations
import { createAdminClient } from "@/lib/supabase/server";
const supabase = createAdminClient();
```
### Tenant scoping
All database operations must be tenant-scoped:
```typescript
import { getTenantContext } from "@/features/tenant/context";
import { DEFAULT_TENANT_ID } from "@/features/tenant/constants";
const { tenantId } = await getTenantContext();
// Always filter by tenant_id
const { data } = await supabase
.from("entities")
.select("*")
.eq("tenant_id", tenantId);
```
Never hardcode the default tenant UUID. Use `DEFAULT_TENANT_ID` from `features/tenant/constants.ts`.
## Testing
### Co-located tests
Every new file with exported logic must have a co-located `.test.ts` file:
```
features/entities/lib/
scoring.ts
scoring.test.ts
features/tools/
registry.ts
registry.test.ts
```
### What to test
| File type | What to test |
|---|---|
| Pure functions | Input/output, edge cases, error conditions |
| Zod schemas | Valid inputs pass, invalid inputs fail, optional fields |
| Constants/enums | All expected values present, cross-referencing |
| Server actions | Mock Supabase, verify query construction, error handling |
| Custom tools | Schema validation + execute function (use `captureTool()` pattern) |
| Bridge/transform functions | Input shape to output shape, filtering logic, empty inputs |
### Running tests
```bash
pnpm test # Run Vitest unit + integration tests
pnpm typecheck # TypeScript type checking
pnpm e2e # Playwright browser tests
pnpm lint # ESLint
pnpm build # Full production build
```
### Test patterns
```typescript
import { describe, it, expect, vi } from "vitest";
describe("myFunction", () => {
it("should handle normal input", () => {
expect(myFunction("hello")).toBe("HELLO");
});
it("should handle edge cases", () => {
expect(myFunction("")).toBe("");
expect(myFunction(null as unknown as string)).toBeNull();
});
});
```
Use `as unknown as Type` instead of `as any` when mocking in tests.
### Regression assets
Every escaped bug must leave behind a regression asset at the appropriate layer:
- **Unit test** -- for logic bugs in pure functions
- **Integration test** -- for bugs in server actions or API routes
- **E2E test** -- for bugs in user flows
- **Visual test** -- for layout or rendering bugs
- **Failure-mode test** -- for error handling bugs
## File organization
### Feature modules
Each feature module is self-contained with a consistent internal structure:
```
features/entities/
types.ts # Types, Zod schemas, constants
server/
actions.ts # Server actions (DB operations)
components/
entity-list.tsx # UI components
entity-detail.tsx
lib/
scoring.ts # Pure functions
scoring.test.ts
hooks/
use-entities.ts # Client-side hooks
extraction/
types.ts # Extraction-specific types
extract-field.ts # Core extraction logic
run-extraction.ts # Orchestration
```
### App routes
The `app/` directory contains thin route handlers. Business logic belongs in feature modules:
```typescript
// app/(app)/opportunity/page.tsx -- THIN
import { EntityListPage } from "@/features/entities/components/entity-list-page";
export default function OpportunityPage() {
return <EntityListPage />;
}
```
### Import boundaries
- **Platform code** (`features/` except `features/custom/`) must never import from `features/custom/`
- **Custom code** (`features/custom/`) can import from platform features freely
- **App routes** import from features, never the reverse
- **Components/ui** are pure primitives with no feature imports
## Shared utilities
Use canonical shared utilities instead of duplicating logic:
| Utility | Location | Purpose |
|---|---|---|
| `cn()` | `lib/utils.ts` | Conditional class merging |
| `slugify()` | `lib/utils.ts` | String to URL-safe slug |
| `humanize()` | `lib/utils.ts` | Slug to human-readable text |
| `apiErrorResponse()` | `lib/api-utils.ts` | Consistent API error responses |
| `CHART_COLORS` | `lib/chart-colors.ts` | Shared chart color palette |
| `getUserId()` | `features/tenant/auth.ts` | Current user ID (zero network calls) |
| `requireAuth()` | `features/tenant/auth.ts` | Auth check + tenant context |
| `getActiveTenantId()` | `features/tenant/context.ts` | Active tenant ID |
| `DEFAULT_TENANT_ID` | `features/tenant/constants.ts` | Default tenant UUID |
| `ROLE_IDS` | `features/tenant/roles.ts` | Role UUID constants (including `guest`) |
| `parseAgentConfig()` | `features/agents/types.ts` | Safe parse of agent config JSONB |
| `TOOL_GROUPS` | `features/agents/types.ts` | Tool group constants |
| `formatDisplayValue()` | `features/tools/lib/format.ts` | Format values for display |
| `formatFileSize()` | `features/documents/lib/format.ts` | Human-readable file sizes |
| `recordAnalyticsEvent()` | `features/analytics/record.ts` | Fire-and-forget event tracking |
| `EXTRACTION_SOURCES` | `features/entities/types.ts` | Extraction source constants |
## Quality rules summary
These are non-negotiable across the codebase:
1. **No explicit `any`** -- use `unknown`, generics, or proper types. In tests, use `as unknown as Type`.
2. **No hardcoded colors** -- CSS variables only. Zero-chroma neutrals (`oklch(X 0 0)`), navy primary.
3. **Components under 200 lines** -- extract sub-components when approaching the limit.
4. **Co-located tests** -- every file with exported logic gets a `.test.ts` file.
5. **Zod at the boundary** -- every API route validates input with Zod.
6. **Auth via adapter** -- never import Supabase directly for auth checks.
7. **Tenant scoping** -- every DB query filtered by `tenant_id`.
8. **Entity writes log activity** -- create an activity record on create/update/delete.
9. **No "entities" in frontend copy** -- use "records", "data", "data types", "items".
10. **Entity types are DB-driven** -- never hardcode slugs in platform code.
11. **AI SDK v6 only** -- `streamText`, `convertToModelMessages`, `DefaultChatTransport`.
12. **Run `pnpm db:types` after migrations** -- keep TypeScript types in sync with the schema.
## Pre-push checklist
Before pushing any change:
```bash
pnpm lint # Must pass
pnpm typecheck # Must pass
pnpm test # Must pass
pnpm build # Must pass
```
For UI changes, visual verification is also required at desktop (1280x720) and mobile (375x667) viewports. No force-pushing allowed.
## Adding a custom tool
The most common extension point. Four steps:
1. **Create the definition** at `features/custom/tools/[name]/definition.ts`:
```typescript
import { registerTool } from "@/features/tools/registry";
import { z } from "zod";
const inputSchema = z.object({
revenue: z.number().describe("Annual revenue in dollars"),
costs: z.number().describe("Implementation costs"),
});
registerTool({
slug: "my-calculator",
name: "My Calculator",
description: "Calculates something useful",
category: "analysis",
inputSchema,
execute: async (input) => {
const roi = (input.revenue - input.costs) / input.costs;
return { roi, paybackMonths: Math.ceil(12 / roi) };
},
});
```
2. **Create tests** at `features/custom/tools/[name]/definition.test.ts` using `mockToolRegistry()` and `captureTool()`.
3. **Register the import** in `features/custom/tools/index.ts`.
4. **(Optional) Add custom UI** -- author a `FormSpec` for the input (`<slug>/form-spec.ts`) and/or a `ViewSpec` for the output, registered via `registerFormSpec("tool:<slug>", spec)` / `registerViewSpec("tool:<slug>", spec)` (from `@/features/interactivity`) in `features/custom/tools/ui.ts`. Without custom UI, the generic form and output renderers handle everything automatically from the Zod schema. (The legacy `registerToolUI()` API is fully retired — FormSpec is the sole tool-input path; see [Tool System](/docs/features/tool-system).)
The tool automatically appears in the `/tools` library, gets its own page at `/tools/[slug]`, and is available to all agents in chat.