# Copilot Instructions

- Read `AGENTS.md` before any analysis or implementation in this workspace — starting with its "Caspian Core Contracts" section, the first-party digest of how Caspian actually works (feature gates, Jinja/PulsePoint brace dialects, authoring model, props passing, the closed PulsePoint template surface, data flows). It is the required first read for every task; do not skip it and implement from framework intuition.
- Keep repo-wide always-on Copilot guidance in this file. Use `.github/instructions/**/*.instructions.md` for narrower task-, file-, library-, or implementation-specific guidance when that extra context should not load on every request.

## Document Ownership

- This file owns repo-wide always-on rules for the workspace.
- `AGENTS.md` should focus on task routing, runtime cross-checking, and packaged-doc maintenance rather than repeating full rule blocks from this file.
- When packaged docs need to point AI from a feature guide to the controlling runtime file, prefer `node_modules/caspian-utils/dist/docs/core-runtime-map.md` instead of duplicating the full module map in multiple pages.
- When packaged docs need to point AI from a PulsePoint feature or directive to the controlling browser behavior, prefer `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` instead of duplicating the full browser feature map in multiple pages.

## Component-First Page Composition (Highest Priority)

This is the top architectural requirement for this workspace. Treat it as a hard rule that outranks convenience, and apply it before writing any route, layout, or page markup.

- Build pages from components, not from one large block of HTML. A route's page template (the `html(r"""...""")` string in `src/app/**/index.py`) should read like a short composition of `x-*` component tags, not a wall of markup. When a page would otherwise carry a long stretch of HTML, that markup must move into a component instead of living in the page.
- Separate every page into meaningful chunks and give each chunk its own component. Typical chunks are a top menu / topbar, header, sidebar / nav rail, hero, toolbar, content sections, cards, lists, forms, footer, and any repeated block. Each chunk owns its own long markup inside its component file, so the page content stays small and readable.
- Default to single-file Python components authored with inline `html(...)` (import `html` from `casp.component_decorator`, return `html("""...""", **context)`) for each focused chunk. Single-file means the component's Python, markup, and small PulsePoint script live together; it does not mean the whole page, full dashboard, or every tab panel should be collapsed into one Python file.
- Split component files by responsibility the way you would split React components. If a page has tabs, create focused components such as `OverviewTab.py`, `ActivityTab.py`, and `SettingsTab.py` instead of one oversized `DashboardTabs.py` that contains every panel. If a section has its own form, table, toolbar, or card list, make that section a component and pass data, flags, callbacks, or labels as props.
- Put these page-chunk components in `src/components/` (or a route-local component folder when they are truly single-route), import them into the route's `index.py` with normal Python imports, and render them as kebab-cased `x-*` tags. Keep the single-root contract in both the page and each component.
- When the user asks to build or extend a page, plan the chunk breakdown first (for example: top menu component, sidebar component, content section component), create those components, then assemble them in the route. Do not start by pasting a full HTML page into the route template and only later consider extraction; component-first is the starting point, not a cleanup step.
- If you find an existing page or single-file component with multiple unrelated responsibilities, prefer splitting it into focused chunk components as part of the work rather than adding more markup to it.

## Global Rules

- Use this decision order: `caspian.config.json` for optional feature enablement, app runtime and app-owned code for current project behavior, matching workspace instruction files under `.github/instructions/**/*.instructions.md` for task-specific implementation guidance, installed `casp` runtime for framework internals, and packaged markdown docs for Caspian feature discovery and task routing.
- As the app grows, prefer `src/components/` for reusable application UI and reserve `src/lib/` for reusable non-UI code such as services, validators, adapters, and shared helpers.
- Read `./caspian.config.json` almost immediately before making feature, tooling, scaffolding, or file-placement decisions. Treat it as the workspace feature gate for flags such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, `websocket`, and `componentScanDirs`.
- Treat `caspian.config.json` as the single source of truth for whether optional Caspian features are enabled in the current workspace. Use feature-specific docs, files, and commands only after the matching flag is confirmed as enabled.
- If a feature is disabled and the user wants it, ask whether they want to enable it first, then update `caspian.config.json` and follow `npx casp update project` so framework-managed files align with the new feature set.
- When `.github/instructions/**/*.instructions.md` files exist, treat them as workspace-local file instructions for specific libraries, component systems, icon sets, integrations, and implementation rules. Read the matching instruction before deciding how to implement work in that area, but do not let it override `caspian.config.json`, app code, or installed runtime behavior.
- Treat `node_modules/caspian-utils/dist/docs/**` as packaged Caspian docs that teach AI how Caspian features work and where to look next. Their presence does not mean the feature is enabled in the current project.
- Use `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` for fast PulsePoint feature lookup before editing browser-side behavior or generating advanced PulsePoint patterns.
- Use `node_modules/caspian-utils/dist/docs/websockets.md` before changing `@socket()` handlers, `pp.socket(...)` clients, the shared socket endpoint, socket origin checks, socket auth/RBAC, broadcast pools, or a fallback native `WebSocket` client.
- For current repo behavior, trust `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, and `src/app/**` over generic Caspian docs.
- For framework internals, trust `.venv/Lib/site-packages/casp/**` over generic or older upstream guidance.
- When packaged docs conflict with project code or installed runtime, the project code, `caspian.config.json`, and installed runtime win. Keep the packaged docs feature-oriented and point AI back to the project files that decide actual enablement and behavior.
- When `prisma/schema.prisma` changes, exactly two commands are required, in order. **Step 1 — sync the database, pick one:** `npx prisma migrate dev` (development default, creates and applies a migration) or `npx prisma db push` (migration-less direct sync). **Step 2 — always:** `npx ppy generate`, the **only** command that regenerates the Python ORM the app imports (`src/lib/prisma/**`, `settings/prisma-schema.json`). Do not confuse the two generators: `npx prisma generate` builds the **Node/TypeScript** `@prisma/client` used only by `prisma/seed.ts` — it writes zero Python and is never a substitute for `npx ppy generate`. If the change affects seed flow or `prisma/seed.ts`, the optional seed steps (`npx prisma generate`, then `npx prisma db seed`) go between step 1 and step 2. Treat `npx prisma db seed` as a destructive data operation: it may clean tables and replace existing records, including production data if pointed at the wrong database. Before running it, tell the user exactly which command you intend to run, explain that it can delete or overwrite database data, confirm the current datasource when practical, and wait for the user's explicit approval.
- Reuse the existing Python database layer in `src/lib/prisma/**`; do not create a second app-owned database abstraction unless the user explicitly asks for one.
- When `caspian.config.json` has `prisma: true`, all Python-side database reads and writes must go through the generated Prisma Python ORM exposed from `src/lib/prisma/**`. Do not bypass it with ad hoc sqlite/postgres drivers, hand-written fetch helpers, JSON files as active stores, browser-side database fetches, or custom HTTP endpoints that reinvent the ORM. Use raw SQL only through Prisma as a narrow fallback when the generated ORM cannot express the query clearly.
- Treat `src/lib/prisma/__init__.py`, `src/lib/prisma/db.py`, `src/lib/prisma/models.py`, and `settings/prisma-schema.json` as generated outputs owned by `npx ppy generate`; do not create or hand-edit them manually.
- Treat `package.json` scripts as opt-in operations. Do not run `npm run dev`, `npm run build`, `npm run static`, `npm run static:serve`, or other npm scripts unless the user explicitly asks, the task genuinely requires that exact script, or deployment preparation needs `npm run build`.
- This workspace supports static HTML export (SSG, like Next.js `output: export`) as an app-owned build convention, not a shipped Caspian feature and not gated by a `caspian.config.json` flag. `npm run static` runs `npm run build && uv run python settings/build-static.py`; keep it composed on `npm run build` (Tailwind **plus** `projectName`) so `settings/files-list.json` is regenerated before `settings/build-static.py` walks that route index — do not reduce it to `tailwind:build` only, or a newly added route/component can be exported from a stale index. `npm run static:serve` runs `settings/serve-static.py`, which serves only `static/`, binds loopback `127.0.0.1` by default, and auto-selects a free port by walking upward from a preferred default (8000, overridable via `PORT`; `HOST`/`PORT_TRIES` also apply) so an occupied port never aborts the preview. Read the port the serve command prints; do not assume 8000 or read `settings/bs-config.json` for it (that file is the dev BrowserSync source of truth, not the static preview). Pre-render a dynamic route by exporting `static_paths` from its `index.py`; auth-gated, non-200, and non-HTML routes are skipped by design. Warn users that `pp.rpc()`, auth, WebSockets, streaming, and per-request server data are inert in a static export. See `node_modules/caspian-utils/dist/docs/static-export.md`.
- Use `npm run build` for deployment prep or an explicit build request, not as the default validation step for routine route, feature, or documentation edits.
- **This workspace has an app-level quality gate, and running it is mandatory — not optional.** Any time you create, edit, or delete app-owned Python (`main.py`, `src/**`) — whether fixing a bug, adding a new file, refactoring, or implementing a feature — you must run `npm run check` (which calls `uv run python settings/check.py`) and get it fully green before treating the change as done. **A change is not complete while the gate reports anything.** It type checks with `pyright`, lints with `ruff`, and runs `pytest` in one pass, prints each problem as `path:line:col [tool:code] message`, and exits non-zero; fix every reported location and re-run until it passes clean. Do not report work as finished, hand it back, or move on to the next task on the assumption that it passes — actually run it and confirm green output first. Write app-owned Python to pass type checking (annotate parameters and returns, avoid untyped `Any` drift) and add or extend tests in `tests/` for the behavior you change. See `### tests/**/*.py and settings/check.py`.
- **Know the gate's boundary: `npm run check` validates Python only** (`pyright` + `ruff` + `pytest` over `main.py`, `src/**`, and `settings/*.py`). It does **not** validate authored markup (the templates inside `index.py`, `layout.py`, and component `.py` files), `globals.css`/Tailwind, or `public/js/**` — a broken template, an invalid `x-*` tag, a single-root violation, or a PulsePoint error will pass the gate and only surface at render time. So a green gate means "the Python is sound," not "the page works." When you change templates, components, styles, or browser JS, verify them by actually loading the affected route in the browser (use the BrowserSync URL from `./settings/bs-config.json`) and checking it renders without console errors — do not treat a passing `npm run check` as proof that front-end work is correct.
- **To see browser-side errors, run `npm run logs` — never start a second `npm run dev`.** The dev terminal usually belongs to the developer, so its stdout is invisible to you. `npm run dev` starts by deleting `.casp/` and `caches/`, so launching your own copy corrupts the running server's state, takes different ports, and rewrites `settings/bs-config.json`. Instead, PulsePoint's browser errors are appended to `.casp/browser-log.jsonl` and rendered by `npm run logs` (also printed at the end of every `npm run check`, where it never affects the exit code). Read its verdicts literally: `CLEAN` means that route was opened and rendered without error; a route **absent from the listing was never opened**, which is no signal rather than a pass; and `dev server is NOT running` means the entries are leftover history, not current state. After a fix, get the route exercised again and re-run `npm run logs`; the flip to `CLEAN` is your confirmation. **What counts as exercising it depends on the status:** a mount error is cleared by a reload, but `NEEDS RECHECK` means an error thrown from an event handler, which only re-running that interaction (click/submit) can clear — a reload never will, and must not be reported as if it did. **Never diagnose from the raw `.casp/browser-log.jsonl`:** it is append-only history, so errors fixed minutes ago are still on disk and will send you hunting a bug that no longer exists — any later `load` or `resolved` event for the same route makes them historical. `UNCONFIRMED` means an error with no matching load in this log (a tab left open across a dev restart); ask for a reload before treating it as live. Details in `AGENTS.md`.
- Let the running dev stack own generated outputs such as `public/css/styles.css`, `settings/component-map.json`, `settings/files-list.json`, `__pycache__/`, and `.pyc` files. Treat those as generated artifacts rather than authored source.
- Never treat `__pycache__/` directories or `.pyc` files as files to edit, regenerate on purpose, or keep in the final diff.
- Treat `settings/component-map.json` and `settings/files-list.json` as generated outputs owned by `settings/component-map.ts` and `settings/files-list.ts`; inspect them when needed, but do not hand-edit them.
- When `caspian.config.json` has `mcp: true`, treat `src/lib/mcp/mcp_server.py` as the app-owned FastMCP server and `src/lib/mcp/fastmcp.json` as the default MCP config. Use `npm run mcp` or `fastmcp run src/lib/mcp/fastmcp.json`; do not assume root `fastmcp.json` auto-discovery.
- Keep auth policy in `src/lib/auth/auth_config.py` and keep auth bootstrap, middleware wiring, and provider registration in `main.py`.
- Treat `casp.runtime_security` in `.venv/Lib/site-packages/casp/runtime_security.py` as package-owned runtime support for safe public-file serving: `PublicFilesMiddleware` maps every existing nested `public/**` file to its root-relative URL without per-directory routes, handles only `GET`/`HEAD`, rejects traversal and symlink escape, and falls through for missing files. It also owns restricted inline-media handling for configured user-upload directories, production session-secret enforcement, production-safe error messaging, fail-closed `APP_ENV` resolution via `is_production_environment()`, and baseline response headers including the Content-Security-Policy. Users should not customize this file during normal app work.
- In app-owned starter config like this workspace, routes start public because `src/lib/auth/auth_config.py` sets `is_all_routes_private=False` by default.
- Decide route privacy in `src/lib/auth/auth_config.py` at app setup time: use `is_all_routes_private=True` when only a few routes should stay public, otherwise keep `is_all_routes_private=False` and list the protected routes in `private_routes`.
- In all-private mode, keep public exceptions in `public_routes`; the runtime defaults keep `/` public and keep `auth_routes=["/signin", "/signup"]` public.
- When building or editing sign-in flows, do not implement app-owned `next` parsing or redirect selection inside the sign-in page or sign-in action unless the user explicitly asks to replace Caspian auth behavior. Guest redirects to `/signin?next=...`, authenticated auth-route redirects, and the default post-login destination are already owned by the Caspian runtime plus `src/lib/auth/auth_config.py`, which defaults `default_signin_redirect` to `/dashboard`.
- Do not treat `token_auto_refresh` as the switch that makes routes private. In the current app it only affects sliding-session refresh if `auth.refresh_session()` is called.
- Use PulsePoint as the default reactive frontend layer unless the user requests another stack.
- For first-party Caspian HTML interactivity, use PulsePoint event attributes such as `onclick`, `oninput`, `onsubmit`, state, refs, effects, directives, and `pp.rpc()` before considering standard DOM scripting. Do not start by adding ids, `data-*` wiring, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom client-side state managers for normal reactive UI.
- Treat render ownership as the first PulsePoint performance decision. Put a value in `pp.state(...)` only when changing it must update markup, a bound child prop/context value, or render-dependent effects. Put non-rendering mutable bookkeeping in `pp.ref(...)`: debounce handles, request/version tokens, pagination cursors, previous values, and transient search text that exists only to build a later RPC payload. A ref mutation does not render and must not be used when the template is expected to update.
- A debounce controls frequency, not render cost. Do not debounce a setter that wakes a large page owner merely to launch an RPC. Store the latest query in a ref, debounce the RPC call, keep authoritative returned rows in state, and use an incrementing request generation (plus `abortPrevious` when appropriate) so an older response cannot overwrite a newer search. Avoid pre-request state changes such as `setLoading(true)` when they do not change visible UI; each setter is another requested render.
- Keep high-frequency controls in the smallest component boundary that owns them. Split a search/toolbar or frequently edited form from a large list, dialog collection, provider, or page shell when their state does not need to rerender the whole subtree. For a controlled input that genuinely must update state on every keystroke, keep its owner small and use `pp.deferredValue(...)` for an expensive derived consumer when a one-commit lag is acceptable.
- Diagnose before editing the PulsePoint runtime. If authored state explicitly changes and the owner rerenders, first remove redundant state, duplicate loading commits, broad ownership, and request races. If rendered HTML is byte-identical yet a stable large subtree is still traversed, or a small necessary binding change causes disproportionate runtime phases, then investigate reconciliation with `pp.enablePerf()`, `pp.resetPerfStats()`, and `pp.getPerfStats()`. `pp.transition()` exposes pending status but does not time-slice or deprioritize rendering. Never work around a performance problem with manual `querySelector`, listeners, or `innerHTML`.
- For normal forms, treat the HTML submit event as the first choice: bind `onsubmit="{submitForm(event)}"` on the `<form>`, call `event.preventDefault()` in the handler when staying on the page, and build the RPC payload with `Object.fromEntries(new FormData(event.currentTarget).entries())`. Let input `name` attributes define the payload keys and let Python validate, normalize, and decide what to persist. Do not add `pp-ref` to every input or attach an effect-managed submit listener just to build an RPC payload.
- Treat imperative DOM APIs and `pp-ref` element reads as narrow escape hatches for third-party widgets, browser APIs that require direct DOM access, focus/measurement/media/canvas behavior, or one-off integration code. When they are needed, keep them inside the owning PulsePoint component script, usually behind `pp.ref(...)` and `pp.effect(...)`, so PulsePoint still owns the component state and event flow.
- When `caspian.config.json` has `tailwindcss: true`, treat Python `merge_classes(...)` plus browser `twMerge(...)` as the only Tailwind class-merging contract: `merge_classes(...)` emits frontend-ready `{twMerge(...)}` expressions, and authored PulsePoint attribute expressions or scripts may call global `twMerge(...)` directly.
- Treat Caspian component usage as HTML-first: import Python components with normal Python imports and render them as kebab-cased `x-*` tags such as `<x-button />` or `<x-command-dialog />`. The Python import is what makes the tag resolve.
- Components are authored single-file. Return `html(r"""...""", **context)` (import `html` from `casp.component_decorator`) to keep markup, server interpolation, and a PulsePoint `<script>` inline. Inside `html(...)`, `{{ ... }}` is server-side Jinja and `{ ... }` is left for PulsePoint; never use a Python f-string for the markup. Autoescaping is on, so `{{ value }}` is safe for user text and trusted HTML needs `Markup(...)` or `| safe`; a `children` value is auto-safe. Keep each file focused on one responsibility. Split multiple panels, tabs, forms, tables, cards, and toolbars into separate components instead of making one giant single-file component.
- For single-file Python components that receive browser props, treat the Python render as an explicit bridge. Attributes on the parent `x-*` tag arrive as raw string kwargs, including unevaluated PulsePoint expressions such as `"{permOpen}"`; they do not reach `pp.props` merely because the Python signature accepts them. Re-emit browser-facing values on the component's single native root with `attributes = get_attributes({...}, props)`, `<root {{ attributes }}>`, and `html(..., attributes=attributes)`. Otherwise `pp.props` is silently empty or missing those keys with no error or warning. Forwarded props are real DOM attributes, so avoid accidental native behavior such as a `title` tooltip by choosing a non-native API name such as `user-name` when appropriate. Follow the full helper and prop-passing contract in `node_modules/caspian-utils/dist/docs/components.md`.
- Use real Python imports for child components everywhere: a module's `x-*` tags resolve from the Components imported into that module (pages and layouts included), which disambiguates same-name components across directories. Runtime resolution precedence is inherited ancestor components, then the module's own Python imports. Slot content resolves in the scope where it was authored, so the module that writes an `x-*` tag must import that component. For directories whose names are not valid identifiers (hyphens, `(group)`), bind via `importlib.import_module(...)` assignment.
- All three import forms resolve, so pick the one that reads best rather than working around a directory layout. Import a name from its own file (`from src.lib.maddex.Button import Button`), import several names from a file that exports several (`from src.lib.maddex.Breadcrumb import Breadcrumb, BreadcrumbItem`), or import straight from a one-component-per-file **directory** without naming each file (`from src.lib.ppicons import Search, ArrowLeft` -> `<x-search />`, `<x-arrow-left />`). That last form is what the generated component directories are built for and is the preferred way to pull in icons. Python binds the _submodules_ there, since a component directory has no `__init__.py` re-exports, so Caspian unwraps a module binding that defines a component under its own file name (`Search.py` -> `Search`). The tag follows the _binding_ name, so `from src.lib.ppicons import Search as MagnifyIcon` renders `<x-magnify-icon />`. Nothing else is unwrapped: a helper module (`utils.py`), an ordinary `import os`, a bare package, or a file whose function is missing the `@component` decorator all still raise `UnknownComponentError` — so that error on a correct-looking import usually means the missing decorator, not the import form. The directory form also reaches only the component named after its file, so a multi-export file's other exports keep the exact-file form: `from src.lib.maddex import BreadcrumbItem` is a plain `ImportError` because there is no `BreadcrumbItem.py`. Ruff sees all three forms as unused imports, but `settings/check.py` suppresses `F401` for any name used as an `x-*` tag, so the gate stays clean.
- For CRUD operations and any browser-initiated reads from the backend, use route or backend `@rpc()` actions on the server and `pp.rpc(...)` from PulsePoint code on the client unless the user explicitly asks for another integration pattern.
- Google and GitHub OAuth ship pre-registered in this starter: `main.py` already calls `Auth.set_providers(GithubProvider(), GoogleProvider())`, and `AuthMiddleware` already handles the `signin/{google,github}` and `callback/{google,github}` paths under `api_auth_prefix` (default `/api/auth`). To add social sign-in, point a link or button at `/api/auth/signin/google` or `/api/auth/signin/github` and set the provider credentials in `.env` (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`). Do not hand-roll an OAuth flow, manual `httpx2`/`authlib` token exchange, or custom callback routes; reuse the shipped providers and let `auth.auth_providers(...)` own redirect, callback, and sign-in.
- For one-way streaming output, including AI/LLM/chat token streams, use Caspian's shipped RPC streaming: write a generator `@rpc()` action that `yield`s chunks (the runtime wraps generators as SSE via `casp.streaming.SSE`) and consume it with `pp.rpc(name, args, { onStream, onStreamComplete, onStreamError })`. When bridging a Python LLM/SDK stream, `async for` over the provider's stream inside the `@rpc()` action and `yield` each token. Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or a WebSocket; reserve WebSockets for genuinely bidirectional channels per the WebSocket rules above.
- For live bidirectional channels, first confirm `caspian.config.json` has `websocket: true`, then use the **named-socket layer**: one `@socket()` function in Python (route-owned in that route's `index.py`, or `src/lib/**` when shared) consumed by `pp.socket(name, args, handlers)` in the owning component script. A hand-written `@app.websocket(...)` endpoint plus a native browser `WebSocket` is the escape hatch for wires the JSON-frame contract cannot carry (binary frames, non-JSON protocols), and then the app owns the origin check, auth delegation, and every limit itself. Do not replace normal CRUD, form submits, uploads, or one-way progress streams with WebSockets.
- When `caspian.config.json` has `websocket: true`, every named socket shares **one** endpoint — `SOCKET_PATH` in `src/lib/websocket/sockets.py`, wired once in `main.py` — and the function is named in a query parameter, so socket names are application-wide and a route never declares its own path. `pp.socket(...)` already knows that endpoint, so do not pass `websocket_path`/`websocket_url` into templates. Keep the socket layer itself under `src/lib/websocket/**`.
- For route creation, every route is one `src/app/**/index.py`: `page()` returns the page markup via `html(...)`, and the same module owns metadata, `@rpc()` actions, auth checks, caching, and redirects. Shared section wrappers live in `layout.py`, whose `layout()` returns the wrapper template (optionally with a props dict). Non-visual routes (redirect-only or action-only) are `index.py` files whose `page()` returns a `Response`.
- Keep route-specific logic in that route's `index.py`. Move code into `src/lib/**` only when it is genuinely reusable across routes, components, integrations, or features; do not extract one-route orchestration just to make it look generic.
- Write every authored route, layout, and component template with one parent HTML element or one imported `x-*` component tag as its root, and keep any `<script>` inside that root rather than after it. A component **may** instead have sibling top-level nodes: that is a fragment (the `<>…</>` shape), the compiler frames it with a `<!--pp:id-->…<!--/pp-->` comment pair, and the browser materializes it into `<pp-fragment style="display: contents">` at mount — so it adds no element to the DOM and is the only shape that survives inside `<tbody>`/`<select>`. **A fragment cannot receive props**: passing any attribute (including `pp-ref`) on its `<x-*>` tag raises `FragmentPropsError`, because there is no root element for forwarding to land on. Use a single native root whenever the component takes props. A route or layout with sibling top-level nodes is instead wrapped in a layout-neutral `<div pp-component style="display: contents">` boundary host, so use plain siblings when a wrapper `<div>` would be meaningless. The same host appears around a component whose authored root is another `x-*` tag, carrying the parent's forwarded props — an extra `display: contents` div in rendered DOM is expected output. Fragment syntax is implicit: never hand-write `<pp-fragment>` or `<!--pp:…-->` markers, which are compiler output.
- When the user asks for a dashboard, admin area, account area, or any grouped child-route section, follow the same mental model as the Next.js App Router: create a parent folder with `layout.py` and place the child routes beneath it. Use a normal folder such as `dashboard/` when the segment should appear in the URL, and use `(group)/` only when it should not.
- In grouped section layouts with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content scroll container that should reset on child-route navigation, usually the main pane. Leave persistent shell scrollers such as sidebars or rails unmarked so SPA navigation can preserve their scroll position.
- **`loading.py` is optional — but when navigation loading UI is wanted, it is the mechanism, not a hand-built spinner.** Most subtrees have no loader and navigate with a plain fade; that is correct default behavior, so do not create `loading.py` files that were not asked for. When a task _does_ ask for a loading state, skeleton, or progress indicator _while moving from one route to another_, the answer is a `loading.py` in the closest subtree folder plus `pp-loading-content="true"` on the pane it should replace in that subtree's `layout.py`. Never hand-roll it with a spinner component, a global `isLoading` store, a `pp:navigation:start`/`pp:navigation:complete` listener, a manual overlay, or a `fetch`-driven page swap: `casp/loading.py` already collects the files, `caspian_config.py` derives each one's URL scope from its folder, and the browser runtime resolves the closest ancestor scope and runs the swap and fade. Contract: `def loading():` is **synchronous and takes no parameters** (an `async def` raises `TypeError`, a missing function raises `AttributeError`); scope is folder-derived, with `(group)` segments stripped (so a loader placed directly inside `(marketing)/` becomes the app-wide `/` fallback, not the group's) and a `[id]` folder never matching a real URL (so dynamic routes put their loader on the static parent); the markup is collected once and injected as raw HTML, so Jinja `{{ }}` interpolates but `<x-*>` tags, `{ }` bindings, and `<script>` do **not** work inside it — plain elements and CSS only; and `pp-loading-transition='{"fadeIn":…,"fadeOut":…}'` inside the loader overrides the 250 ms default. Copy the shipped pair: `src/app/dashboard/loading.py` with the `pp-loading-content="true"` bar in `src/app/dashboard/layout.py`.
- Do not route an **in-page** wait through `loading.py`. An `@rpc()` call, a form submit, a filter refetch, or an upload is ordinary `pp.state` in the owning component, bound to `hidden`/class — `loading.py` only ever fires on route-to-route SPA navigation, never on first paint, a hard refresh, or a static export.
- When a single route needs to affect a wrapping layout, have `page()` return `(html(...), {"dashboard_body_class": ...})` and consume that value as `{{ layout.dashboard_body_class }}` in the wrapping layout template. Return the prop from `layout()` when the same value should apply across a whole subtree.
- For file uploads and file-manager flows, keep browser interaction in route templates, keep upload and delete `@rpc()` actions in the owning `src/app/**/index.py`, keep shared storage and persistence helpers in `src/lib/**`, store metadata in Prisma, and store browser-accessible blobs under `public/uploads/**` when the files should be served directly.
- `public/uploads/**` is protected because `main.py` maps the top-level `uploads` directory to `INLINE_SAFE_UPLOAD_MEDIA_TYPES` in `PublicFilesMiddleware.inline_safe_subdirectories`. Before writing untrusted runtime uploads into any other top-level public directory, add that directory to the same restricted-inline mapping; otherwise trusted first-party public-file behavior would serve executable HTML or SVG inline.
- Local upload helpers should create `public/uploads` on demand when it does not exist yet; do not assume the folder is committed ahead of time.
- When runtime uploads write into `public/uploads/**`, keep the public-root-relative entry `uploads` in `settings/bs-config.ts` `PUBLIC_IGNORE_DIRS` so `npm run dev` does not reload on each upload.
- For logout flows, prefer `pp.rpc("signout")` backed by `@rpc(require_auth=True)` from page-level or component-level UI. Use a dedicated signout route only for plain form POST, no-JavaScript fallback, or other full-navigation edge cases.
- Protect customized `src/lib/auth/auth_config.py` from updater overwrite by adding `./src/lib/auth/auth_config.py` to `excludeFiles` in `caspian.config.json`.
- Treat `pp-component` on routes, layouts, and components as compiler-injected by the Python side; do not add it manually in authored templates unless the task is explicitly about runtime internals. Author owned PulsePoint logic as a plain `<script>` inside the component root.
- `layout()` can be synchronous or async in the installed runtime. Keep async layout work focused on shared layout props or metadata; use `page()` or `@rpc()` when the work belongs to a specific route or user action.
- Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
- In `layout.py`, `layout()` returns `html(r"""...""", **context)` — the same entrypoint pages and components use. It is _deferred_: `children` is the page beneath the layout and does not exist yet, so `html(...)` hands back an unrendered `LayoutTemplate` and the engine renders it later with `children`/`layout`/`metadata` merged in (those three always win over author context). Also accepted: `(html(...), props_dict)`, a props dict alone (passthrough `<slot />` shell), `None`, or a bare template string. A layout that never places its children — no `<slot />` and no `{{ children }}` — raises `LayoutChildrenError` rather than serving an empty shell.
- Never author markup as a Python f-string. `{{ x }}` is server interpolation and `{ x }` is a PulsePoint binding inside `html(...)`; an f-string inverts both, skips autoescaping while still marking the result trusted, and skips the `<x-*>` scope stash. The `templates` gate freezes the existing f-string components in `settings/fstring-components.json` and fails on new ones.
- Do not assume `StateManager` survives across requests unless `request.state.session` is explicitly bridged from `request.session`.
- Route, layout, and component templates must keep exactly one authored top-level parent node so Caspian can inject `pp-component` after component expansion. In source, that parent may be a native HTML element or a single imported `x-*` component tag, but it must resolve to one final HTML root. Keep any owned PulsePoint script inside that same parent.

## BrowserSync URL Source Of Truth

- When AI needs to test or confirm whether a route, server response, or proxy-backed request is working, use `./settings/bs-config.json` as the source of truth for the current BrowserSync URLs.
- Do not assume the proxy stays on the default `http://localhost:5090`; if that port is busy, the active BrowserSync ports may change.
- Prefer confirming the current `local`, `external`, `ui`, and `uiExternal` values in `./settings/bs-config.json` before suggesting a test URL or opening the app in the browser.
- Use this file when frontend console errors or terminal output suggest the wrong local URL, proxy port, or BrowserSync UI port is being used during debugging.

## Path-Specific Rules

### `main.py`

- Treat `main.py` as the repo source of truth for FastAPI setup, auth bootstrap, middleware wiring, route registration, cache defaults, and error handlers.
- `main.py` finalizes every rendered page through `defer_component_roots(...)`. It wraps each outermost `pp-component` root in an inert `<template pp-component>` so the browser never parses raw `{...}` placeholders as live DOM. Before PulsePoint materializes those roots, it captures and empties their plain component scripts so native browser execution cannot race component-scope evaluation; later morph insertions use the same protection. Because of this deferral, `{...}` is safe in any attribute or position (SVG `d`/`viewBox`/`points`, `src`/`href`, form `value`/date/number/color, table/select text). Do not add per-tag workarounds to dodge browser first-paint validation (static-path `hidden` toggles, `data-*` URL holders, `hidden`-gated `<img src>`, or SSR-resolved initial values). Keep `pp-style` (source-file tooling) and the controlled form-field `value`/`checked`/`defaultvalue`/`<textarea>` rewrites (attribute-vs-property correctness); those exist for reasons deferral does not replace.
- `main.py` owns only the wiring of the single named-socket endpoint (`@app.websocket(SOCKET_PATH)`, gated on `websocket: true`). Origin validation, the connection cap, auth delegation, idle timeout, message-size limit, per-connection message rate, the error-frame-then-close behavior, and broadcast (`SocketSender`/`SocketPool`) all live in `src/lib/websocket/sockets.py` — verify there, not in `main.py`.
- Treat `main.py` plus imported package-owned helpers such as `casp.runtime_security` as the runtime source of truth for response-header hardening and public-file behavior.
- Preserve the production effective middleware execution order unless the task explicitly changes request semantics: `SecurityHeadersMiddleware -> PublicFilesMiddleware -> RateLimitMiddleware -> MissingPublicAssetMiddleware -> BodySizeLimitMiddleware -> SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware -> route`. In development, `RequestDiagnosticsMiddleware` is outermost. Existing public-file `GET`/`HEAD` requests stop at `PublicFilesMiddleware`; a miss whose first path segment is a real `public/` directory is 404'd by `MissingPublicAssetMiddleware` (so a bad asset URL never answers with a sign-in redirect), and every other miss falls through the remaining stack.
- Do not move normal file upload or file-manager behavior into `main.py`; keep those actions in the owning route `index.py` and shared helpers in `src/lib/**`.
- Document route param behavior exactly as implemented here.
- Do not use `main.py` alone to infer whether optional features are enabled; confirm that in `caspian.config.json` first.
- Before changing WebSocket behavior, verify `cfg.websocket`, the single named-socket endpoint in `main.py` (`SOCKET_PATH`, `/__pulsepoint/ws`), and the layer in `src/lib/websocket/sockets.py`: auth delegation, idle timeout, message-size limit, per-connection message rate, connection cap, and the error-frame-then-close failure shape. HTTP-only middleware does not automatically protect `scope["type"] == "websocket"` connections, so socket auth lives in that layer, not `AuthMiddleware`.
- Add live channels as `@socket()` functions consumed by `pp.socket(...)`, gating each with `require_auth=`/`allowed_roles=` (delegated to Caspian `Auth` via `Auth.set_request(websocket)` + `is_authenticated`/`get_payload`/`check_role`). Do not re-implement session/`exp`/payload parsing per endpoint, and do not reintroduce the removed public/private channel endpoints, `authorize_websocket(...)`, or `WebSocketConnectionManager`. Keep authenticated and guest traffic in separate `SocketPool`s, and treat the socket session as read-only.

### `src/lib/**/*.py`

- Keep `src/lib/` for app-owned shared non-UI code, service wrappers, validators, adapters, and reusable helpers.
- Prefer `src/components/` for reusable rendered UI instead of placing component modules in `src/lib/`.
- Reuse the generated `src/lib/prisma/` package for Python database access, but do not hand-edit files under `src/lib/prisma/`; regenerate them with `npx ppy generate` after schema changes.
- For file managers, keep shared storage, normalization, and Prisma-backed persistence helpers here while route-owned upload and delete `@rpc()` actions stay in `src/app/**/index.py`.
- When `caspian.config.json` has `mcp: true`, keep app-owned MCP tools in `src/lib/mcp/mcp_server.py` and keep the default FastMCP config in `src/lib/mcp/fastmcp.json`. If those locations change, update `settings/restart-mcp.ts` and the MCP docs together.
- Keep auth policy in `src/lib/auth/auth_config.py`. Keep auth bootstrap and middleware order changes in `main.py`.
- Do not recreate or customize `src/lib/security/runtime_security.py` for normal application work. Runtime security helpers are package-owned in `casp.runtime_security`; app-specific policy should live in app-owned config or route/helper code instead.
- Keep the named-socket layer in `src/lib/websocket/sockets.py`: the `@socket()` registry, `Socket`/`SocketSender`/`SocketPool`, the endpoint handler, and the handshake security (origin check, connection ceiling). Sockets shared by several routes also live under `src/lib/websocket/**`; route-owned sockets stay in the route's `index.py`.

### `src/components/**/*.py`

- Keep `src/components/` as the default home for reusable application UI components and for the page chunks produced by component-first composition (top menus, sidebars, headers, content sections, cards, lists, forms, footers).
- Move shared cards, forms, shells, navigation, and other reusable rendered building blocks here once they are used across routes or features.
- Keep route-owned markup in `src/app/**`, and keep non-UI helpers or services in `src/lib/**`.
- Author every component as a single Python file with inline `html(...)`. Keep the single-root rule. Resolve child `x-*` tags from real Python imports. Prefer one focused component per file unless a file intentionally exports tiny, tightly coupled subcomponents. See `node_modules/caspian-utils/dist/docs/components.md`.

### `tests/**/*.py` and `settings/check.py`

- This is the app's own testing and static-analysis layer, added on top of Caspian; the framework ships no test runner, so treat it as a workspace convention documented here and in `AGENTS.md`, not as a packaged Caspian feature.
- Run the whole gate with the single command `npm run check` (or `uv run python settings/check.py`). It runs `pyright` (type check), `ruff` (lint), and `pytest` (tests) against `main.py`, `src/**`, and `settings/*.py`, prints each problem as `path:line:col [tool:code] message`, and exits non-zero when any check fails. For debugging one tool, use `uv run python settings/check.py --only pyright` (or `ruff` / `pytest`).
- `npm run check` only reports. Auto-fix with `npm run check:fix`, which runs `settings/fix.py` (**format first**, then safe ruff fixes, then the gate). pyright and pytest failures are never auto-fixed.
- **`html(r"""` stays on one line, with the markup starting on the next line.** `ruff format` will not produce that shape — it explodes any call whose first argument is a multiline string when the call has other arguments, in default and preview style alike — so `settings/format.py` rejoins the opening after ruff runs, and iterates the pair to a fixed point (rejoining the opening lets ruff also pull the closing `)` up on a sole-argument call). Consequence to know: running bare `ruff format`, or an IDE format-on-save, re-splits every `html(` opening; `npm run format` puts them back. Do not "fix" this by hand-editing call sites or by adding `# fmt: skip`.
- **Formatting is `npm run format` (`settings/format.py`), and it runs markup before Python.** It formats two surfaces: authored markup inside every `html(r"""...""")` via **djLint**, then app Python via **`ruff format`**. That order matters — reformatting a template changes how many lines its string literal spans, which changes how ruff wraps the enclosing `html(...)` call, so running ruff last is what makes a single pass converge. Prettier is not an option here: it has no Jinja awareness and de-indents `{% for %}` blocks to column 0. Use `npm run format:check` to report without writing (exit 1 if work remains). Do not add a separate markup-formatting script; `--markup` / `--python` already narrow the run.
- **The formatter never trusts djLint — it proves each block first.** djLint is a general HTML formatter, so it will insert a newline between a block tag and an adjacent inline or `<x-*>` tag, which renders as a visible space (a custom element's `display` comes from CSS the formatter cannot see). So every block is formatted, then checked against `settings/_markup_equivalence.py`, a tokenizer that decides whether the result is _guaranteed_ to render identically; only proven blocks are written, and the rest are skipped with a printed reason. `<script>`/`<style>`/`<pre>`/`<textarea>` bodies are masked out before djLint runs, so code and preformatted text are preserved byte-for-byte by construction rather than by proof — djLint otherwise reads `/>` inside a JS regex as a tag delimiter. A skipped block is not a failure and must not be "fixed" by loosening the oracle: it means the reformat would have changed rendering. Coverage is in `tests/test_format.py`; a false _positive_ from the oracle (calling a real change safe) is the only dangerous failure mode, so both directions are pinned there.
- Unused-import (`F401`) removal is handled carefully because component imports look unused to ruff. Single-file components import children used only as `<x-*>` tags in `html(...)` templates (`from .Dialog import DialogContent` → `<x-dialog-content>`); ruff cannot see that, and Caspian resolves the tag from module globals at render time, so deleting the import breaks rendering. Two layers keep it safe: `F401` is `unfixable` in `[tool.ruff.lint]` so a raw `ruff check --fix` never deletes any import; and `settings/fix.py` removes dead imports only from files that contain no `<x-*>`-tag import (component-guarded files are skipped whole and left for the gate). `settings/check.py` likewise suppresses the `F401` reports whose symbol is used as an `x-{camel_to_kebab(name)}` tag, so the gate fails only on genuinely dead imports. The tag detection is shared in `settings/_component_imports.py`. Do not blanket-ignore `F401` or re-enable its autofix globally. See `node_modules/caspian-utils/dist/docs/testing.md`.
- Keep tests in `tests/` app-focused: `main.py` helpers and route behavior (via `starlette.testclient.TestClient` against `main.app`), and `src/lib/**` policy such as `auth_config.py`. Do not test framework internals under `.venv/Lib/site-packages/casp/**`.
- `tests/conftest.py` puts the project root on `sys.path` and sets safe dev env defaults (`APP_ENV`, `AUTH_SECRET`) so importing `main` never fails during tests; extend it rather than duplicating that setup per test file.
- When adding or changing app-owned Python, add or extend the matching test and keep `npm run check` green before finishing. New tests follow `tests/test_*.py`.
- Tooling and config live in `pyproject.toml`: dev tools in `[dependency-groups] dev` (install/refresh with `uv sync --group dev`), type checking in `[tool.pyright]` (`include = ["main.py", "src", "settings/*.py"]` with `exclude = [".venv", "node_modules", "**/__pycache__"]`, so it also analyzes the generated `src/lib/prisma/**` ORM and the `settings/*.py` orchestrator scripts), linting in `[tool.ruff]` (correctness-focused; `E501` line length and `I` import ordering are intentionally not enforced on generated starter code), and tests in `[tool.pytest.ini_options]`.
- `[tool.pyright]` uses `typeCheckingMode = "basic"` (Pylance's default, so the IDE and the gate agree) and sets `reportReturnType = "none"` and `reportAssignmentType = "none"` to mirror the old pyrefly suppressions; those specific type-error kinds are not reported by the gate. Re-enable per-rule when tightening. Do not assume every annotation mismatch is caught.
- Treat `settings/check.py` as the app-owned orchestrator for the gate. Keep it as the single entry point (parses each tool's output into the shared `path:line:col` report). The only sanctioned `package.json` scripts are `check` (report), `check:fix` (format, auto-fix, then report), `format` / `format:check` (`settings/format.py`), and `logs` (browser-log digest); do not add parallel one-off `test`, `lint`, `typecheck`, or markup-formatting scripts when `--only` and `--markup`/`--python` already cover narrowing a run.

### `settings/browser_log.py` and `settings/dev-log-bridge.ts`

- Together these are the front-end feedback channel: the TypeScript half receives browser reports over a BrowserSync middleware and writes both the dev terminal and `.casp/browser-log.jsonl`; the Python half reads that file and reports per-route status. They are development-only and app-owned, so document them here and in `AGENTS.md`, never in the packaged Caspian docs.
- **Do not reduce the log to errors only.** It deliberately records a `load` event per page render, and that is what makes the file trustworthy rather than misleading: a clean reload writes nothing by itself, so without `load` events a fixed error would be reported forever, and an empty file would be indistinguishable from a route nobody opened. Status is derived per route from its most recent load.
- Errors attach to a load by the client-generated `page` id, not by arrival order — two `fetch` POSTs can land out of sequence. Keep that grouping if you touch either half.
- **A reload re-runs mount but never clicks anything, so it is not evidence about every error.** Errors carry a `phase` derived from how long after their page load they arrived (`mount` within `MOUNT_PHASE_MS`, else `interaction`). A later load clears mount errors only; interaction errors carry forward as `NEEDS RECHECK`. Do not "simplify" this back to load-clears-everything — that produced a false `CLEAN` on a route whose `onclick` was genuinely broken.
- **The log is compacted on every `src:` change, not appended forever.** `compactBrowserLog(...)` rewrites it to the session header, a `restart` marker, and still-open errors (the `openErrors` map); resolved history is dropped, survivors are flagged `carried` and dropped at the next compaction. This exists because a dev session that runs for hours never hits the `.casp/` wipe that only a full `npm run dev` performs. Keep the trigger on `src:` changes so regenerated CSS and build output do not churn the log.
- History is not state, and a raw reader cannot tell a fixed error from a live one. The `readme` field on the `session` line and the `resolved` events exist purely to defend against that; do not drop them as redundant.
- An error whose `page` has no `load` in the current log is reported as `UNCONFIRMED`, not as a fresh failure. That is the tab-left-open-across-a-restart case, and calling it a live bug is how an agent ends up "fixing" working code.
- `LogEvent` in `settings/dev-log-bridge.ts` and `build_report(...)` in `settings/browser_log.py` are two ends of one format; change them together and update `tests/test_browser_log.py`.
- The digest is informational inside `npm run check` and must stay that way — it cannot become part of the exit code, because whether a route has been exercised depends on someone opening a browser. `--fail-on-error` exists for callers that opt in.
- The log lives in `.casp/` so that `settings/project-name.ts` (which deletes that directory at the start of every `npm run dev`) truncates it per session for free. Do not relocate it somewhere that survives a restart without adding explicit truncation.

### `settings/build-static.py` and `settings/serve-static.py`

- Treat these as the app-owned static-export tooling (not shipped `casp` runtime). `settings/build-static.py` is the SSG exporter; `settings/serve-static.py` is the preview server. Document behavior against these files, `package.json`, and `settings/project-name.ts`, not against a `casp` module.
- `settings/build-static.py` boots the real app via Starlette `TestClient` and iterates `get_files_index()` (which reads `settings/files-list.json`) to render every static route to `static/<route>/index.html`, then mirrors the complete `public/**` tree into `static/`. Keep its "warn & skip" scope policy: dynamic routes need `static_paths` in their `index.py`, and auth-gated / non-200 / non-HTML routes are reported and skipped rather than written broken. Preserve `APP_ENV=development` so the build needs no production secrets.
- Keep `npm run static` composed as `npm run build && uv run python settings/build-static.py` so `projectName` regenerates `settings/files-list.json` and `settings/component-map.json` before the exporter walks the route index. Do not change it to run only `tailwind:build`.
- `settings/serve-static.py` must keep its robustness and safety contract: serve only `static/`, bind loopback `127.0.0.1` by default (network exposure only via `HOST=0.0.0.0`), auto-select a free port by genuinely binding upward from the preferred start port (default 8000; `PORT`/`PORT_TRIES` overrides) with `SO_REUSEADDR` disabled so an occupied Windows port truly fails instead of silently colliding, fail fast when `static/` is unbuilt, and shut down cleanly on Ctrl+C. Do not reintroduce a hardcoded-port `python -m http.server` one-liner.
- Treat `static/` as generated output; do not hand-edit exported HTML. Fix the source route, component, or asset and re-export.

### `public/js/main.js`

- Treat `public/js/main.js` as the thin browser bootstrap entry point.
- Keep it minimal and point it at the runtime shipped in `public/js/pp-reactive-v2.min.js`.
- Do not duplicate PulsePoint runtime logic here.

### `public/js/pp-reactive-v2.min.js`

- Treat `public/js/pp-reactive-v2.min.js` as the browser-side PulsePoint runtime source of truth for component execution, hooks, refs, directives, SPA navigation, scroll restoration, `pp.rpc(...)`, and `pp.socket(...)` behavior. It is the single minified bundle the app ships; anything else under `public/js/` is development-only build output and must never be cited as the runtime.
- Only the built, minified runtime ships to the application. Do not document, reference, or route AI to a TypeScript authoring tree as if the application consumed it.
- Preserve the current public runtime contract unless the task explicitly changes Caspian frontend behavior.
- At runtime, component logic is discovered from a plain, untyped `<script>` inside each `pp-component` root. PulsePoint captures the source before materialization or morph insertion, prevents native execution, and evaluates it in component scope.
- The current SPA scroll contract is: save scroll positions per history entry, reset window scroll on push navigation, and use `pp-reset-scroll="true"` to opt specific containers into reset behavior. Use `body[pp-reset-scroll="true"]` only when a target route should reset every scrollable surface.
- The current SPA loading contract is: the server embeds every `src/app/**/loading.py` in a hidden registry, one `div[pp-loading-url="<scope>"]` per file; on navigation the runtime walks the destination pathname up toward `/` for the closest matching scope, then replaces the `[pp-loading-content="true"]` element (falling back to `document.body`) with that markup between a fade-out and fade-in. With no matching loader it fades the same region without swapping content, which is the expected behavior for the many routes that have none. Do not add a parallel loading mechanism in app code; if a loader is wanted, add a `loading.py`.

### `src/app/**/*.py` (route and layout templates)

- **These rules govern the markup inside `index.py` and `layout.py`.** Authoring is Python-only: there are no `.html` route, layout, or component files in a Caspian app, so a rule scoped to authored markup applies to the triple-quoted template inside the `.py` file that owns it.

- Compose pages from components first (see "Component-First Page Composition"). Keep the page template a short assembly of `x-*` chunk components (top menu, sidebar, content sections, cards, forms, footer, and other repeated blocks) instead of a long inline HTML body. When a route would carry a long stretch of markup, move that markup into a single-file `html(...)` component and render it as an `x-*` tag here.
- Keep route templates and layouts server-rendered first, with PulsePoint enhancement as the default interactive layer.
- The page markup lives inline in `index.py` (returned from `page()` via `html(...)`) and the layout template lives inline in `layout.py` (returned from `layout()`). The same modules own metadata, `@rpc()` actions, auth checks, caching, and redirects.
- When route templates render reusable Python components as kebab-cased `x-*` tags such as `<x-button />`, import those components with Python imports at the top of the module.
- For route-level reactivity, prefer PulsePoint state, effects, refs, and template directives together with `pp.rpc(...)` instead of manual DOM mutation or ad hoc browser fetch code.
- For route-level buttons, forms, inputs, toggles, menus, filters, uploads, and list updates, bind events directly in the authored HTML with native PulsePoint-handled `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit`. Avoid id-driven `querySelector`/`addEventListener` setup for first-party UI because it duplicates the PulsePoint event and rerender model.
- For simple route-level form submissions, collect the submitted fields with `Object.fromEntries(new FormData(event.currentTarget).entries())` inside the `onsubmit` handler and pass that object directly to `pp.rpc(...)`. Use `pp.state(...)` for pending/error/success UI and controlled non-native widgets; use `pp-ref` only when the handler needs imperative element access such as focus, measurement, file input reset, or third-party integration.
- Preserve standard Jinja template syntax such as `{{ ... }}` in layouts and `pp-*` runtime attributes in rendered HTML.
- Do not author `pp-component="..."` manually in route or layout templates; the Python render pipeline injects it onto the single root element.
- Use a plain `<script>` inside the single route or layout root when it owns PulsePoint logic; no custom script type is required.
- Default authored route and layout templates to one top-level parent node, the same shape used for component templates. In source, that parent may be a native HTML element or a single imported `x-*` component tag. If a script is needed, keep it inside that parent instead of as a sibling top-level node. A **component** that breaks this does not raise — it becomes a props-less fragment, which is rarely intended; a route or layout instead gets a `display: contents` boundary host, so reach for sibling top-level nodes only when a wrapper element would carry no meaning. `TemplateRootError` (`must have exactly one top-level HTML element so Caspian can inject pp-component`) now fires only for a component template with no root at all, or with an unresolvable `x-*` tag as its only root.
- For dashboard, admin, or grouped sections with multiple child routes, prefer folder-level `layout.py` wrappers in `src/app/**` instead of repeating the same shell in each child route.
- For grouped shells with independent sidebar and content scrolling, mark the content pane with `pp-reset-scroll="true"` when that pane should start at the top on each child-route navigation. Do not put the attribute on the whole shell when the sidebar or rail should retain its own scroll.
- If — and only if — a subtree is asked to show a loading state during child-route navigation, add `loading.py` beside that folder's `layout.py` and mark the swapped pane with `pp-loading-content="true"` in the layout. Do not add a spinner component, an `isLoading` state, or a navigation-event listener to the route for this. A subtree with no loader is the normal case, not an omission to fix.
- For upload managers and similar interactive lists, prefer `pp.state(...)` plus `pp-for` over manual DOM painting so rerenders keep the list stable.
- For route-owned live channels, call `pp.socket(name, args, handlers)` inside the owning component script: open it in `pp.effect(..., [])`, keep the handle in `pp.ref(...)`, and close it in the effect cleanup. Reach for a native `new WebSocket(...)` only for a wire the named-socket contract cannot carry.
- Do not assume WebSocket clients live in a dashboard or any fixed route. Put the browser client in whichever route owns that live experience, pass first-render socket values from the matching `index.py`, and use route auth policy plus WebSocket endpoint auth checks intentionally for public, private, or mixed channels.
- Do not assume React, Vue, JSX-first component syntax, HTMX, or another frontend runtime unless the user explicitly requests one.
- **The React analogies in this file and in the packaged docs are scoped to two things only: the `pp.*` hook API inside `<script>`, and how you split components by responsibility. They never license JSX in markup.** A template is plain HTML compiled by PulsePoint. Concretely, never generate: `{cond && (<div/>)}` or `{cond ? <A/> : <B/>}` (use `hidden="{!cond}"`), `{list.map(item => (<tr/>))}` (use `<template pp-for="item in list">` with `key="{item.id}"`), unquoted brace attributes such as `class={...}` or `selected={x === 'y'}` (always quote: `class="{...}"`), `className`, `htmlFor`, camelCase `onClick`/`onChange`, `style={{...}}` object literals, `dangerouslySetInnerHTML`, or `<>…</>` fragments. There is no `pp-if`, `pp-show`, `pp-else`, or `pp-key`. An unquoted brace attribute is invalid HTML — the parser shreds the element, the component root never compiles, and the route renders a blank page with no console error, which is why this must be checked at authoring time. Before finishing a template, confirm it would still be valid HTML with every `{}` deleted. Full contract: `node_modules/caspian-utils/dist/docs/pulsepoint.md` "PulsePoint Is Not JSX", "Complete Directive And API Surface", and "Conditional rendering".

### `prisma/**`

- Treat `prisma/schema.prisma` as the data-model source of truth.
- Treat `prisma.config.ts` as the datasource and migration or seed configuration source of truth.
- After changing `prisma/schema.prisma`, first sync the database: `npx prisma migrate dev` (development default, keeps migration history) or `npx prisma db push` (migration-less direct sync).
- If the schema change affects seed data or `prisma/seed.ts`, run `npx prisma generate`, then ask for explicit user approval before running `npx prisma db seed` because the seed script may delete or replace table data.
- **Always** run `npx ppy generate` after every schema change so the Python ORM files and `settings/prisma-schema.json` stay aligned with Prisma. It is the only command that generates those files.
- The two generators are not interchangeable: `npx prisma generate` writes the Node/TypeScript `@prisma/client` (consumed only by `prisma/seed.ts`), while `npx ppy generate` writes the Python ORM the app imports from `src.lib.prisma`. Running `npx prisma generate` never refreshes the Python side.
- Keep Node-side generation and seeding aligned with `npx prisma generate` and `prisma/seed.ts`.
- Keep Python-side database access aligned with `src/lib/prisma/**`, and treat that directory as generated output rather than a manual editing surface.

### `.venv/Lib/site-packages/casp/**/*.py`

- Treat these files as framework internals.
- Only change them when the task is explicitly about Caspian core behavior, installed-runtime debugging, or documentation that must match the installed implementation.
- If behavior changes here, update the matching docs under `node_modules/caspian-utils/dist/docs/`.
- `casp/runtime_security.py` owns framework-managed safe public-file serving through `PublicFilesMiddleware` and `resolve_safe_public_path`, configured upload attachment-mode behavior, baseline response headers including the CSP (`build_content_security_policy()`, overridable via `CONTENT_SECURITY_POLICY`), production-safe error messages, fail-closed `APP_ENV` resolution, and production session-secret enforcement used by `main.py`.

### `.github/instructions/**/*.instructions.md`

- Treat these files as workspace-local, task-scoped AI instructions for third-party libraries, design systems, icon packs, integrations, and narrowly scoped implementation rules.
- Check for a matching instruction file almost immediately before coding when the task mentions or touches a library or workflow that may have dedicated guidance, for example maddex, ppicons, or another named integration.
- Keep these files specific and discoverable: the filename, `description`, and `applyTo` pattern should make it obvious when the instruction applies.
- Use these files to guide implementation choices and coding style for that surface, but keep actual runtime behavior grounded in `caspian.config.json`, app code, and installed framework code.

### `node_modules/caspian-utils/dist/docs/**/*.md`

- These files are the packaged Caspian documentation layer, not the runtime and not the source of current workspace state.
- Use them to help AI answer three questions: which Caspian feature applies, which project files should be inspected next, and which workflow is appropriate once the feature is confirmed as enabled.
- Use `node_modules/caspian-utils/dist/docs/file-conventions.md` for the general special-file model, then verify the completed Python migration in `main.py` and `.venv/Lib/site-packages/casp/**`: navigation loading UI uses `loading.py`, and global fallback pages use `not_found.py` and `error.py`. This app has no authored `.html` special files.
- Use `node_modules/caspian-utils/dist/docs/websockets.md` when deciding how to document or implement named sockets (`@socket()` / `pp.socket(...)`), the shared socket endpoint, origin checks, per-socket auth/RBAC, the JSON frame contract, and the choice between sockets, RPC, and RPC streaming.
- Verify behavior claims in this order:
  1.  `caspian.config.json`, then `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, `src/app/**`
  2.  `.venv/Lib/site-packages/casp/**`
  3.  the markdown file being edited
- Do not encode the current project's feature flags, file inventory, script list, or temporary status inside the packaged docs. Keep those facts in `.github/copilot-instructions.md`, `AGENTS.md`, or the project code.
- When an optional feature doc is edited, phrase it as feature guidance, for example `when caspian.config.json has mcp: true`, instead of as a project snapshot such as `this workspace has mcp: false`.
- When `caspian.config.json` has `tailwindcss: true`, document the current Tailwind flow as a full replacement: Python `merge_classes(...)` builds frontend `{twMerge(...)}` expressions and browser-side `twMerge(...)` resolves conflicts.
- Keep `index.md` discoverable as the manifest, keep cross-links aligned, and make each feature page explicit about when it applies and what file AI should inspect next.
