> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cfosilvia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AGENTS

# Silvia Project Rules

## Architecture

* **Next.js 15 App Router** — All routing uses the `app/` directory with file-based routing
* **Supabase (PostgreSQL)** as the primary database, **Neo4j** for graph relationships
* **Clerk** for authentication — all protected routes go through middleware
* **Vercel AI SDK** for AI features — prefer this over raw API calls

## Code Style

* TypeScript strict mode is currently OFF but being migrated toward strict — all new code must include proper type annotations, avoid `any`, and use explicit return types on functions
* Path alias: use `@/` for imports from the project root
* Styling: Tailwind CSS with shadcn/ui components in `components/ui/` — do not introduce new CSS frameworks
* Components: functional components only, no class components
* State management: React Context + TanStack React Query — no Redux or Zustand

## Conventions

* API routes live in `app/api/` and use Next.js Route Handlers
* Shared UI components go in `components/`
* Business logic and service clients go in `lib/`
* Custom hooks go in `hooks/`
* Type definitions go in `types/`
* Contexts go in `contexts/`
* Tests use Vitest — add unit tests for new utility functions and hooks in `__tests__/` directories
* Keep `package.json` scripts minimal. A new one needs a named consumer, not a place on a list: see **Package scripts** below
* Every PR is linked to a Linear ticket, and the ticket exists from the start of the work. Begin a working session with `/new-task` (`.agents/skills/new-task/SKILL.md`), which reuses an existing ticket or creates one and moves it to In Progress. Put the ticket id in the PR title, e.g. `Fix X (CFO-8300)`. Larger efforts start with `/new-project`. See `docs/LINEAR-WORKFLOW.md`

## Package scripts

**A script earns its place by having a NAMED CONSUMER, cited by path and line in
the PR that adds it.** Rank the kinds, because they are not equally strong:

* **Sufficient on its own:** a step in `.github/workflows/**`, or a config file
  that invokes it (`playwright.config.ts` runs `npm run e2e:dev` as its
  `webServer`).
* **Not sufficient on its own:** a line in a doc, or a tool's scaffolding
  convention. Both are real, and both can be supplied by the same author in the
  same commit, so either needs a reviewer to agree in the thread.
* **Never counts:** the script's own source file. A header that says "run me with
  `npm run x`" is the script describing itself.

**Ask why the invocation needs a stable NAME, not why it cannot be run
directly.** Almost nothing here cannot be run directly, so that test settles
nothing: measured on 2026-09-11 there are 41 runnable files under `scripts/` and
`package.json` names five of them.
The answers that do carry are that CI and a human must run the identical command,
that the flags are long enough to get wrong, or that a tool looks the name up
itself.

**Delete a script in the PR that removes its last consumer.**

**Recorded because the previous wording invited the opposite repair.** It listed
the seven scripts that existed on 2026-03-13, which was a verbatim snapshot of
`package.json` that day rather than a considered allowlist. It was then edited
twice to track the file rather than to govern it: `dev-turbo` was added in
`c3408765dd` to address a review comment, and removed again in `f80976e72a`.
It still drifted to seventeen entries against seven names without anyone
noticing, and in one case it bent the code instead, with CI invoking a verifier
by path specifically to avoid adding a script the list did not permit. A list of
names is not a policy: it goes stale silently, and the only repair it suggests is
a longer list.

## Clean Code Guidelines

* **Single Responsibility** — each function, component, and module should do one thing well
* **Small functions** — extract logic into well-named helper functions rather than writing long procedural blocks
* **Meaningful names** — variables, functions, and components should clearly describe their purpose. Avoid abbreviations and single-letter names outside of loops
* **No magic numbers or strings** — use named constants for values that have meaning
* **DRY but not premature** — extract repeated logic only when the pattern appears 3+ times. Don't abstract prematurely
* **Early returns** — prefer guard clauses and early returns over deeply nested conditionals
* **Avoid side effects** — functions should be predictable. Keep side effects isolated and explicit
* **Keep components lean** — extract business logic into custom hooks or utility functions. Components should focus on rendering
* **Colocation** — keep related code close together. Helper functions used by one component should live near that component, not in a global utils file
* **Consistent error handling** — handle errors explicitly, never silently swallow them. Use try/catch at API boundaries and propagate errors meaningfully

## TypeScript Patterns & Anti-Patterns

### Prefer

* **Union types over enums** — use `type Status = 'active' | 'inactive'` instead of `enum Status { Active, Inactive }`
* **`interface` for object shapes, `type` for unions and intersections** — be consistent
* **`unknown` over `any`** — when the type is truly unknown, use `unknown` and narrow with type guards
* **Discriminated unions** — for state modeling: `type Result = { status: 'success'; data: T } | { status: 'error'; error: string }`
* **`as const`** — for literal inference on objects and arrays: `const ROLES = ['admin', 'user'] as const`
* **Utility types** — use `Pick`, `Omit`, `Partial`, `Required`, `Record` instead of manually redefining shapes
* **Type narrowing** — use `typeof`, `in`, and custom type guards instead of type assertions
* **Explicit return types on exported functions** — helps catch regressions and serves as documentation
* **`satisfies`** — use `satisfies` to validate a value matches a type while preserving the narrower inferred type

### Avoid

* **`any`** — never use `any` in new code. Use `unknown`, generics, or proper types instead
* **Type assertions (`as`)** — avoid `as` casts. If you need to assert, it usually means the types are wrong
* **`!` non-null assertions** — don't use `!` to silence null checks. Handle the null case explicitly
* **`@ts-ignore` / `@ts-expect-error`** — do not suppress errors. Fix the underlying type issue
* **Overly complex generics** — if a generic type requires more than 2-3 type params or nested conditionals, simplify it
* **`Function` type** — use specific function signatures: `(args: Args) => ReturnType` instead of `Function`
* **`object` or `{}`** — these accept almost anything. Use `Record<string, unknown>` or define the actual shape
* **Barrel exports (`index.ts` re-exports)** — these hurt tree-shaking and slow down builds. Import directly from the source file

## Documentation

Before working on a specific subsystem, check `docs/` for existing documentation. Key docs include:

* `docs/CLERK-AUTH-ONBOARDING-ROUTING.md` — Loop-proof auth/onboarding routing, Clerk session-token claims, write-path invariants
* `docs/ACCOUNT-TYPE-SYSTEM.md` — Account types and classification
* `docs/PLAID-RATE-LIMIT-HANDLING.md` — Plaid API rate limiting strategy
* `docs/BROKERAGE-CASH-SWEEP-DEDUP.md` — Wells Fargo cash-sweep double-count: detection rules, read-time dedup architecture, the `account_effective_balances` view for SQL consumers, snapshots caveat, rollout flag
* `docs/TRANSFER-IN-TRANSIT-DEDUP.md` — bank → brokerage transfers counted twice while the bank debit is pending: the matching rule, the lossless gap guard, every consumer that applies it, rollout flag
* `docs/EARNINGS-CALENDAR-SYSTEM.md` — Earnings calendar implementation
* `docs/REFERRAL-SYSTEM.md` — Referral system logic
* `docs/TAX-TREATMENT-IMPLEMENTATION.md` — Tax treatment handling
* `docs/VALUATION-SURFACES.md` — Full map of every surface that prices/values/converts holdings (FX, minor units, net worth). **Required reading before touching any holding valuation logic** so no surface is missed.
* `docs/SUPABASE-PAGINATION.md` — PostgREST's silent 1,000-row response cap and the `fetchAllPages` helper. **Required reading before adding any read of a per-item × per-day table** (snapshot/history tables) — an unpaged select silently truncates and shows wrong money. Also covers chunking unbounded `.in()` lists.
* `docs/DATABASE-VIEW-RLS-SAFETY.md` — security\_invoker / RLS rules for views, tables, and functions. **Required reading before creating or editing anything in `supabase/migrations/`** — CREATE OR REPLACE VIEW silently drops `security_invoker` and turns a per-user view into a fleet-wide RLS bypass (CFO-7727). Agent trigger wrapper: `.agents/skills/database-migration-safety/SKILL.md`.
* `docs/EXPERIMENTS.md` — **Required reading before any A/B test.** The experiment registry and `useExperiment` hook: exposure rules, control-by-default, QA overrides, and the constraints on agent-authored experiment PRs
* `docs/WATCHLIST.md` — Dashboard watchlist: the `watchlist_items` table, per-plan limits enforced in the database, and the Silvia skills that document it
* `docs/precious-metals-integration.md` — Precious metals pricing integration
* `docs/discourse-integration.md` — Discourse community SSO integration
* `docs/SUPPORTED_CHAINS.md` — Supported blockchain chains
* `docs/PERSONAL-INFO-TOOL.md` — Personal info tool for AI chat
* `docs/blog/agent-rules.md` — Blog architecture, Sanity integration, routing, and SEO conventions
* `docs/CHAT-MAIN-FLOWS.md` — **Required reading before changing chat-page behavior** (switching chats, loading, the composer and Send, streaming). The flows users actually perform, measured in PostHog, and the page-level tests that replay them
* `docs/SILVIA-SKILLS-SYNC.md` — **Required reading before any user-visible UI change.** Silvia AI's knowledge of the UI lives in the separate `CFO-Silvia/skills` repo and goes stale when the UI changes
* `packages/ui-catalog/README.md` — **Read before adding or changing a component Silvia puts in chat (GenUI).** The one schema-described list shared by the server, web and mobile; the prompt section and skills docs are generated from it, never hand-edited
* `docs/MODULUS-CLEANUP-PLAN.md` — **Required reading before deleting anything from `components/ui/` or removing a "dead" file.** Scope is **UI only**: components, pages, and the stories/tests that exist only for them — never hooks, `lib/`, `app/api/`, emails or other logic, even when unreferenced. Records which legacy components Modulus genuinely duplicates, which unreachable files are safe and which only look dead (parked features, work in progress, files other open PRs are editing), the checks every delete PR must run, and the PR-by-PR order
* `.agents/skills/author-blog-posts/SKILL.md` — Agent workflow for authoring blog posts directly in Sanity (draft-first, via the Sanity MCP)
* `docs/LINEAR-WORKFLOW.md` — How agent work is tracked in Linear: team, Bug/Feature/Maintenance categories, assignee and state rules, and why the ticket must be In Progress from the start of the session (it is how task time is measured). Skills: `.agents/skills/new-task/SKILL.md` (`/new-task`) and `.agents/skills/new-project/SKILL.md` (`/new-project`)

Always read the relevant doc before modifying these systems — they contain important context not obvious from the code alone.

## Silvia AI Skills — UI Knowledge Sync

Silvia (the AI agent users chat with) answers "how do I…" questions **only**
from `silvia-navigation-skills/SKILL.md` in the `CFO-Silvia/skills` repo — not
from this codebase. If a UI element isn't documented there, Silvia tells users
she can't help and files a support ticket.

**Whenever a change touches user-visible UI** (pages, sidebar, flows, warnings,
banners, plan gating, button/tab names, GenUI components), check whether the
skills repo needs a matching update, and if so open a companion PR in
`CFO-Silvia/skills` and link it from your PR description. Full checklist and
deploy notes: `docs/SILVIA-SKILLS-SYNC.md`.

## Chat page — test the flows users actually perform

A change to chat-page behavior (switching chats, loading a chat, the composer
and Send, streaming or re-attaching, the history panel) must:

1. Re-measure the affected flows in PostHog with the queries in
   `docs/CHAT-MAIN-FLOWS.md`, and replay each one in a page-level test at the
   user's speed, asserting what the user can do (for example, Send enabled
   right after switching and typing). Testing only the property the change
   improves is not enough.
2. Keep `app/(app)/chat/[[...sessionId]]/__tests__/chat-main-flows.test.tsx`
   passing.
3. State in the PR any live check that was blocked. A blocked check is not a
   passed one.

**Why:** CFO-9358 made switching chats instant and its tests measured how fast
a chat appeared. None typed after switching, so a half-second disabled Send
reached production (CFO-9375, CFO-9379).

## Next.js Dynamic Imports

Every `dynamic()` call **must** include a `loading` fallback:

```ts theme={null}
// ✅ correct
const MyWidget = dynamic(() => import('@/components/my-widget'), {
  loading: () => <div className='h-full' />,
})

// ✅ correct — ssr:false also needs loading
const MyWidget = dynamic(() => import('@/components/my-widget'), {
  ssr: false,
  loading: () => <div className='h-full' />,
})

// ❌ wrong — no loading prop
const MyWidget = dynamic(() => import('@/components/my-widget'))
const MyWidget = dynamic(() => import('@/components/my-widget'), { ssr: false })
```

**Why:** Without `loading`, a `dynamic()` component suspends on first render and bubbles up to the nearest `<Suspense>` boundary. On the dashboard that boundary has `fallback={<DashboardSkeleton>}`, causing the entire page to flash back to skeleton when any lazy widget first mounts. A local `loading` prop catches the suspension at the component level so it never reaches the page boundary.

## User Communications — HARD RULE

* **NEVER send, schedule, or ship code that sends emails, SMS, push
  notifications, or any other outbound message to users.** No exceptions for
  experiments, RCTs, re-engagement flows, or "exactly-once" sends, and no
  exceptions behind environment gates (`VERCEL_ENV === 'production'` is not a
  safeguard). A feature request that implies messaging users is NOT
  authorization — the send itself requires explicit, verbatim sign-off from
  Jacob before any code is written. When in doubt, stop and ask.
  (Operator verdict 2026-07-15, issued on the RCT day-1 re-engagement email:
  PR #2761 was merged to staging and immediately reverted via #2822 for this
  reason.)

## Security

* Never commit `.env` files or secrets
* All encryption keys use dedicated env vars — never hardcode
* API routes that need auth should verify Clerk session or internal API tokens
* Webhook endpoints must validate signatures

### Never log an identifier in the clear

**A log line may describe an identifier. It may never reproduce one.** Route
every email address, phone number, or name through `lib/log-redact.ts`
(`maskEmail`, `emailLogShape`, `maskPhone`, `hasValue`) before it reaches
`console.*`.

`clerk_id` is the correlation key for debugging — it is already in the log
line, it is not secret, and it is enough to find the row. The address itself
adds nothing a debugger needs.

**Why:** `all_users.email` is encrypted at rest, but the Clerk webhook logged
`plaintext_email` and `/api/user-info` logged `decrypted_email`, so every
affected address sat in Vercel runtime logs in the clear. Log access alone was
enough to recover user email addresses — which is exactly how one
investigation read them. That is CWE-532, and it undoes the database
encryption entirely. Two guards in `lib/__tests__/log-redact.test.ts` scan the
repo and fail the build on a new one.

## Database

* Use Supabase client from `lib/supabase/` for all database operations
* Migrations live in `supabase/migrations/`
* Do not use raw SQL in application code — use the Supabase SDK

## Theming & semantic color tokens

All UI colors go through the semantic token system — **read
[docs/THEMING.md](docs/THEMING.md) before writing any styles.** Never add
hardcoded color values (`bg-[#hex]`, `dark:text-[#hex]`, inline hex styles,
SVG hexes, gradient hex stops) to themable UI; use the tokens defined in
`app/themes.css` (classes like `bg-surface-card`, `text-content-title`,
`border-edge`, or `rgb(var(--token))` for inline styles). The live light and
dark themes are staging-frozen: styling that should only affect an
experiment variant must be scoped via `data-theme-variant` rules in
`app/themes.css`, never by changing shared styles.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.