create-caspian-app 1.0.21 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,13 +50,25 @@ npm run dev
50
50
 
51
51
  ## What "Reactive Python" looks like
52
52
 
53
- A route is a folder. The markup lives in `index.html`, the server logic in `index.py`. Templates are **plain HTML** with `{expression}` interpolation and a small `<script>` for state.
53
+ A route is a folder with one file: `index.py`. The markup lives inline as a template handed to `html(...)`, next to the server logic. Templates are **plain HTML** with `{expression}` interpolation and a small `<script>` for state.
54
54
 
55
- ### Route template — `src/app/todos/index.html`
55
+ ### The route — `src/app/todos/index.py`
56
56
 
57
- ```html
58
- <!-- @import { Badge } from "../../components/ui/Badge.py" -->
57
+ ```python
58
+ from casp.component_decorator import html
59
+ from casp.layout import Metadata
60
+ from casp.rpc import rpc
61
+ from casp.validate import Rule, Validate
62
+ from src.components.ui.Badge import Badge
63
+
64
+ metadata = Metadata(
65
+ title="Todos",
66
+ description="A tiny Caspian todo list.",
67
+ )
59
68
 
69
+
70
+ async def page():
71
+ return html(r"""
60
72
  <section>
61
73
  <x-badge variant="default">Tasks: {todos.length}</x-badge>
62
74
 
@@ -105,24 +117,7 @@ A route is a folder. The markup lives in `index.html`, the server logic in `inde
105
117
  }
106
118
  </script>
107
119
  </section>
108
- ```
109
-
110
- ### Backend — `src/app/todos/index.py`
111
-
112
- ```python
113
- from casp.layout import Metadata, render_page
114
- from casp.rpc import rpc
115
- from casp.validate import Rule, Validate
116
- from src.lib.prisma import prisma
117
-
118
- metadata = Metadata(
119
- title="Todos",
120
- description="A tiny Caspian todo list.",
121
- )
122
-
123
-
124
- async def page():
125
- return render_page(__file__)
120
+ """)
126
121
 
127
122
 
128
123
  @rpc()
@@ -157,35 +152,31 @@ That is the whole loop: no API routes, no client, no serializer. `pp.rpc("create
157
152
 
158
153
  Your directory structure under `src/app` is your URL structure.
159
154
 
160
- | File | URL |
161
- | ------------------------------- | ------------- |
162
- | `src/app/index.html` | `/` |
163
- | `src/app/about/index.html` | `/about` |
164
- | `src/app/blog/posts/index.html` | `/blog/posts` |
155
+ | File | URL |
156
+ | ----------------------------- | ------------- |
157
+ | `src/app/index.py` | `/` |
158
+ | `src/app/about/index.py` | `/about` |
159
+ | `src/app/blog/posts/index.py` | `/blog/posts` |
165
160
 
166
161
  ```
167
- src/app/users/[id]/index.html -> /users/123 (dynamic segment)
168
- src/app/docs/[...slug]/index.html -> /docs/a/b/c (catch-all)
169
- src/app/(auth)/login/index.html -> /login (route group, no URL segment)
170
- src/app/dashboard/layout.html -> wraps every /dashboard/* page
162
+ src/app/users/[id]/index.py -> /users/123 (dynamic segment)
163
+ src/app/docs/[...slug]/index.py -> /docs/a/b/c (catch-all)
164
+ src/app/(auth)/login/index.py -> /login (route group, no URL segment)
165
+ src/app/dashboard/layout.py -> wraps every /dashboard/* page
171
166
  ```
172
167
 
173
168
  #### Route file conventions
174
169
 
175
- | File | Owns |
176
- | ---------------- | -------------------------------------------------------------------------------- |
177
- | `index.html` | The page's authored markup (single root element) |
178
- | `index.py` | `metadata`, `page()`, route-owned `@rpc()` actions, redirects, first-render data |
179
- | `layout.html` | A section wrapper containing `<slot />` |
180
- | `layout.py` | Layout-level props and server logic |
181
- | `loading.html` | Navigation loading state for the route |
182
- | `not-found.html` | 404 UI |
183
- | `error.html` | Error UI |
184
-
185
- `page()` renders the sibling template with `render_page(__file__, context)`. It can also return a
186
- `(page_html, layout_props)` tuple, whose dict keys become `{{ layout.* }}` in a parent layout.
170
+ | File | Owns |
171
+ | ---------------- | ------------------------------------------------------------------------------------------------------------- |
172
+ | `index.py` | The page: `page()` returning `html(...)` markup, `metadata`, route-owned `@rpc()` actions, redirects, first-render data |
173
+ | `layout.py` | The section wrapper: `layout()` returning the shell template (with `<slot />`) plus optional props and server logic |
174
+ | `loading.html` | Navigation loading state for the route |
175
+ | `not-found.html` | 404 UI |
176
+ | `error.html` | Error UI |
187
177
 
188
- > **Rule:** `index.py` never inlines page HTML. Markup belongs in `index.html`.
178
+ `page()` returns `html(r"""...""", **context)`. It can also return a
179
+ `(page_html, layout_props)` tuple, whose dict keys become `{{ layout.* }}` in a parent layout.
189
180
 
190
181
  ---
191
182
 
@@ -237,7 +228,7 @@ Never handwrite runtime-managed attributes (`pp-component`, `pp-owner`, `pp-ref-
237
228
 
238
229
  #### The single-root rule
239
230
 
240
- Every route, layout, and component template must have **exactly one** top-level element (or one imported `x-*` root), with any owned `<script>` **inside** that root. Caspian injects `pp-component` on the final root and errors if it cannot find one. `<!-- @import ... -->` comments sit above the root and do not count as content.
231
+ Every route, layout, and component template must have **exactly one** top-level element (or one imported `x-*` root), with any owned `<script>` **inside** that root. Caspian injects `pp-component` on the final root and errors if it cannot find one.
241
232
 
242
233
  ---
243
234
 
@@ -264,7 +255,7 @@ Component `<script>` blocks are plain JavaScript. The `pp` object mirrors React
264
255
  | `pp.optimistic(passthrough, reducer?)` | Optimistic UI that reconciles against a confirmed value |
265
256
  | `pp.props` | Props bag derived from the rendered root's attributes |
266
257
 
267
- Runtime utilities: `pp.createContext`, `pp.mount`, `pp.redirect`, `pp.rpc`, `pp.enablePerf`, `pp.disablePerf`, `pp.getPerfStats`, `pp.resetPerfStats`.
258
+ Runtime utilities: `pp.createContext`, `pp.mount`, `pp.redirect`, `pp.rpc`, `pp.socket`, `pp.enablePerf`, `pp.disablePerf`, `pp.getPerfStats`, `pp.resetPerfStats`.
268
259
 
269
260
  React APIs with **no** PulsePoint equivalent: `forwardRef`, `memo()` as a wrapper, `lazy`, `Suspense`, `useInsertionEffect`, `useActionState`, `useFormStatus`, free-function `startTransition`.
270
261
 
@@ -342,47 +333,27 @@ def UserCard(user=None, **props):
342
333
 
343
334
  > Use a raw string (`r"""…"""`) when the inline `<script>` contains backslashes (regex, `\n`).
344
335
 
345
- #### Template-backed (`render_html`) — for large markup
346
-
347
- `Counter.py`:
336
+ #### Importing and rendering
348
337
 
349
338
  ```python
350
- from casp.component_decorator import component, render_html
351
-
352
- @component
353
- def Counter(label: str = "Clicks") -> str:
354
- return render_html(__file__, {"label": label})
355
- ```
356
-
357
- `Counter.html`:
339
+ from casp.component_decorator import html
340
+ from src.components.Container import Container
341
+ from src.components.ui.Button import Button
342
+ from src.components.Breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbLink
358
343
 
359
- ```html
360
- <div>
361
- <h3>{{ label }}</h3>
362
- <button onclick="setCount(count + 1)">{count}</button>
363
-
364
- <script>
365
- const [count, setCount] = pp.state(0);
366
- </script>
367
- </div>
368
- ```
369
-
370
- #### Importing and rendering
371
-
372
- ```html
373
- <!-- @import Container from "../components" -->
374
- <!-- @import { Button, Card as UserCard } from "../components/ui" -->
375
- <!-- @import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "../components/Breadcrumb.py" -->
376
344
 
345
+ def page():
346
+ return html(r"""
377
347
  <x-container class="py-10">
378
348
  <x-button variant="outline">Continue</x-button>
379
349
  </x-container>
350
+ """)
380
351
  ```
381
352
 
382
353
  - `Container` → `<x-container />`, `CommandDialog` → `<x-command-dialog />`.
383
- - If one Python file exports several components, import them **from that file path**, not from the folder.
384
- - `@import` is a file-level directive: it must sit **above** the authored root, never inside it.
385
- - For component-to-component composition, prefer real Python imports inside the component module; a component's own `x-*` tags resolve from its module imports.
354
+ - A module's `x-*` tags resolve from the Component objects imported into that module the Python import **is** the registration.
355
+ - If one Python file exports several components, import them **from that module**, not from per-name modules that do not exist.
356
+ - Slot content resolves in the scope where it was authored, so the module that writes an `x-*` tag must import that component.
386
357
 
387
358
  #### Props: every prop the template reads must be re-emitted on the root
388
359
 
@@ -557,15 +528,59 @@ Every optional capability is gated by one flag in `caspian.config.json`. **That
557
528
  | `typescript` | TypeScript frontend tooling and the Vite build path |
558
529
  | `prisma` | Prisma schema, migrations, and the generated Python ORM |
559
530
  | `mcp` | A FastMCP server mounted into the same app (`/mcp`) |
560
- | `websocket` | App-owned FastAPI `@app.websocket(...)` endpoints and socket helpers |
531
+ | `websocket` | Named sockets `@socket()` in Python, `pp.socket()` in the browser |
532
+
533
+ #### WebSockets — named sockets
534
+
535
+ Use RPC for ordinary reads, writes, uploads, and SSE streams. Reach for a socket only for **long-lived bidirectional** channels: chat, collaboration, presence, multiplayer state.
536
+
537
+ An rpc is a question with one answer, and an rpc stream is an answer that arrives in pieces. A **named socket** is the third shape — both sides may speak, at any time, for as long as the page is open. It is the socket counterpart of `@rpc()`/`pp.rpc()`: one decorated Python function, one browser call.
538
+
539
+ ```python
540
+ from src.lib.websocket.sockets import Socket, socket
541
+
542
+ @socket()
543
+ async def echo(label: str, socket: Socket):
544
+ while (text := await socket.recv()) is not None:
545
+ if not await socket.send(f"{label}: {text}"):
546
+ break # The browser is gone.
547
+ ```
548
+
549
+ ```html
550
+ <script>
551
+ const sock = pp.ref(null);
552
+
553
+ pp.effect(() => {
554
+ sock.current = pp.socket(
555
+ "echo",
556
+ { label: "you" },
557
+ {
558
+ onMessage: (value) => append(value),
559
+ onError: (error) => setStatus(error.message),
560
+ },
561
+ );
562
+ return () => sock.current.close();
563
+ }, []);
564
+ </script>
565
+ ```
566
+
567
+ Open the socket inside `pp.effect(..., [])`, keep the handle in `pp.ref(...)`, and close it in the effect's cleanup. The handle exposes `send(value)`, `close(code?, reason?)`, and `readyState`; handlers are `onOpen`, `onMessage(value)`, `onError(error)`, `onClose({ code, reason, wasClean })`.
561
568
 
562
- #### WebSockets
569
+ **The wire:**
563
570
 
564
- Use RPC for ordinary reads, writes, uploads, and SSE streams. Reach for WebSockets only for **long-lived bidirectional** channels: chat, collaboration, presence, multiplayer state.
571
+ - Every socket connects to **one endpoint**, wired once in `main.py` and gated by the flag. The function is named in a query parameter, so socket names are **application-wide** the function's own name, and a duplicate is refused at registration.
572
+ - Arguments do **not** travel in the URL (a URL is logged by every proxy on the way) but as the connection's **first frame**: one JSON object, exactly the payload `pp.rpc` would have posted. The client sends it automatically on open. Keys are filtered against the handler's signature, like RPC.
573
+ - Every frame after that is one JSON value, in either direction.
574
+ - There is no status line inside an open connection, so **failure is a frame**: `{"error": "..."}` — that key alone — followed by a close. The client routes it to `onError`, never `onMessage`, and the server refuses to send that shape as an ordinary message.
575
+ - A handler that returns is a conversation that ends: the connection closes. `await socket.send(...)` returning `False` means the browser is gone — a signal to stop, not an error to report.
565
576
 
566
- Endpoints are app-owned in `main.py`; reusable session/auth/broadcast helpers live in `src/lib/websocket/`. In the browser, use PulsePoint for state and lifecycle but the **native** `WebSocket` for the transport keep the socket in `pp.ref(...)` and close it in a cleanup effect.
577
+ **Broadcast:** `socket.sender()` returns a `SocketSender` the sending half, detached from the conversation and safe to hold in shared state. A room is a `SocketPool` of senders, which prunes connections whose browser has left as it broadcasts. Keep authenticated and guest traffic in **separate pools** so a private broadcast can never fan out to a guest connection.
567
578
 
568
- **Sockets authorize themselves.** The HTTP middleware stack early-returns on `scope["type"] == "websocket"`, so `AuthMiddleware` does not protect them. Every endpoint must run its own origin + auth guard, and authenticated and guest traffic belong in separate broadcast pools.
579
+ **Auth is declared per socket, not per endpoint.** `@socket()` is public, `@socket(require_auth=True)` needs a session, `@socket(allowed_roles=[...])` adds RBAC — all delegating to Caspian's `Auth`. This matters because the HTTP middleware stack early-returns on `scope["type"] == "websocket"`: `AuthMiddleware` never sees a handshake, so the socket endpoint authorizes each connection itself, alongside the anti-CSWSH origin check, connection ceiling, message-size limit, per-connection message rate, and idle timeout. The socket session is read-only — mutations are not persisted back to the cookie over a WebSocket.
580
+
581
+ A hand-written `@app.websocket(...)` route with a native browser `WebSocket` remains the escape hatch for wires the JSON-frame contract cannot carry (binary frames, non-JSON protocols) — and then the app owns every one of those checks itself.
582
+
583
+ A socket declared in a route's `index.py` registers when that route first renders, which is necessarily before the browser can open it. A socket shared by several routes belongs in `src/lib/**`.
569
584
 
570
585
  #### MCP
571
586
 
@@ -583,8 +598,9 @@ Caspian ships fail-closed defaults. Things worth knowing before you change them:
583
598
  - **RPC payload keys are filtered against the function signature.**
584
599
  - **`/uploads` serves user content in attachment mode**; only real image types render inline. First-party `/css`, `/js`, and `/assets` stay inline.
585
600
  - **CSRF protection, strict Origin validation, HttpOnly cookies, security headers, and page rate limiting** are on by default.
601
+ - **Sockets authorize themselves.** The HTTP middleware stack early-returns on `scope["type"] == "websocket"`, so `AuthMiddleware` never sees a handshake. The named-socket endpoint runs the origin check, connection cap, and per-socket auth itself — HTTP route privacy does not extend to a socket.
586
602
 
587
- Security-relevant environment variables: `MCP_AUTH_TOKEN`, `RATE_LIMIT_PAGES` (default `200/minute`), `CONTENT_SECURITY_POLICY` (replaces the default policy wholesale), `MAX_WEBSOCKET_CONNECTIONS`, `MAX_WEBSOCKET_MESSAGES_PER_WINDOW`, `WEBSOCKET_RATE_WINDOW_SECONDS`, and `WEBSOCKET_ALLOWED_ORIGINS` (**required in production** — the same-origin fallback is derived from the client-supplied `Host` header and is development-only).
603
+ Security-relevant environment variables: `MCP_AUTH_TOKEN`, `RATE_LIMIT_PAGES` (default `200/minute`), `CONTENT_SECURITY_POLICY` (replaces the default policy wholesale), `MAX_WEBSOCKET_CONNECTIONS`, `MAX_WEBSOCKET_MESSAGE_BYTES`, `MAX_WEBSOCKET_MESSAGES_PER_WINDOW`, `WEBSOCKET_RATE_WINDOW_SECONDS`, `WEBSOCKET_IDLE_TIMEOUT_SECONDS`, and `WEBSOCKET_ALLOWED_ORIGINS` (**required in production** — the same-origin fallback is derived from the client-supplied `Host` header and is development-only).
588
604
 
589
605
  ---
590
606
 
@@ -601,21 +617,18 @@ my-app/
601
617
  │ └── seed.ts
602
618
  ├── src/
603
619
  │ ├── app/ # File-system routes
604
- │ │ ├── layout.html # Root layout
605
- │ │ ├── layout.py
606
- │ │ ├── index.html # Home page
607
- │ │ ├── index.py
620
+ │ │ ├── layout.py # Root layout (template + props from layout())
621
+ │ │ ├── index.py # Home page (markup + logic in one file)
608
622
  │ │ ├── globals.css
609
623
  │ │ ├── error.html
610
624
  │ │ ├── not-found.html
611
625
  │ │ └── users/[id]/
612
- │ │ ├── index.html # /users/:id
613
- │ │ └── index.py
626
+ │ │ └── index.py # /users/:id
614
627
  │ ├── components/ # Reusable UI (@component)
615
628
  │ └── lib/ # Non-UI code
616
629
  │ ├── auth/auth_config.py
617
630
  │ ├── prisma/ # Generated Python ORM — do not edit
618
- │ ├── websocket/ # Socket helpers (when websocket: true)
631
+ │ ├── websocket/ # Named sockets: @socket(), Socket, SocketPool
619
632
  │ └── mcp/ # FastMCP server (when mcp: true)
620
633
  ├── public/ # Static assets, incl. the PulsePoint runtime and uploads
621
634
  └── settings/ # Dev stack config and generated indexes
@@ -625,7 +638,7 @@ my-app/
625
638
 
626
639
  - **Route-owned** logic (first-render query, route `@rpc()` actions, redirects, route validation) stays in that route's `index.py`. Move it to `src/lib/**` only when it is genuinely shared.
627
640
  - **Reusable UI** goes in `src/components/`; **helpers, services, adapters** go in `src/lib/`.
628
- - **Compose pages from components.** A route's `index.html` should read as a short assembly of `x-*` chunks (topbar, sidebar, header, sections, forms, footer), not a wall of markup. Plan the breakdown before writing the route.
641
+ - **Compose pages from components.** A route's page template should read as a short assembly of `x-*` chunks (topbar, sidebar, header, sections, forms, footer), not a wall of markup. Plan the breakdown before writing the route.
629
642
  - **Generated, never hand-edited:** `src/lib/prisma/**`, `settings/prisma-schema.json`, `settings/files-list.json`, `settings/component-map.json`, `public/css/styles.css`, `__pycache__/`.
630
643
 
631
644
  ---
@@ -697,8 +710,11 @@ Policy is **warn & skip**: dynamic routes are pre-rendered only when their `inde
697
710
  npx ppicons add Rocket
698
711
  ```
699
712
 
713
+ ```python
714
+ from src.lib.ppicons.Rocket import Rocket
715
+ ```
716
+
700
717
  ```html
701
- <!-- @import { Rocket, ChevronDown } from "../lib/ppicons" -->
702
718
  <x-rocket class="w-6 h-6 text-primary" />
703
719
  ```
704
720
 
@@ -708,8 +724,11 @@ npx ppicons add Rocket
708
724
  npx maddex add button card dialog
709
725
  ```
710
726
 
727
+ ```python
728
+ from src.lib.maddex.Button import Button
729
+ ```
730
+
711
731
  ```html
712
- <!-- @import { Button } from "../lib/maddex/Button.py" -->
713
732
  <x-button variant="outline">Continue</x-button>
714
733
  ```
715
734
 
@@ -14,12 +14,12 @@
14
14
 
15
15
  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.
16
16
 
17
- - Build pages from components, not from one large block of HTML. A route's `src/app/**/index.html` 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.
17
+ - 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.
18
18
  - 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.
19
19
  - 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.
20
20
  - 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.
21
- - 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 with top-of-file `<!-- @import ... -->` directives above the route root, and render them as kebab-cased `x-*` tags. Keep the single-root contract in both the page and each component.
22
- - 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 `index.html` and only later consider extraction; component-first is the starting point, not a cleanup step.
21
+ - 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.
22
+ - 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.
23
23
  - 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.
24
24
 
25
25
  ## Global Rules
@@ -66,21 +66,21 @@ This is the top architectural requirement for this workspace. Treat it as a hard
66
66
  - 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.
67
67
  - 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.
68
68
  - 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.
69
- - Treat Caspian component usage as HTML-first in the current runtime: import Python components with `<!-- @import ... -->` and render them as kebab-cased `x-*` tags such as `<x-button />` or `<x-command-dialog />`.
70
- - Components may be authored single-file. Return `html("""...""", **context)` (import `html` from `casp.component_decorator`) to keep markup, server interpolation, and a PulsePoint `<script>` inline instead of a same-name `.html` via `render_html(...)`. 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. Prefer single-file `html(...)` for small and medium components, but 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.
69
+ - 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.
70
+ - 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.
71
71
  - 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`.
72
- - Inside single-file components, use real Python imports instead of `<!-- @import ... -->` comments for child components: a component's own `x-*` tags resolve from the components imported into its Python module, which disambiguates same-name components across directories. Do not put an import comment inside the `html("""...""")` string. Runtime resolution precedence is inherited ancestor components, then the component's own Python imports, then a local `@import`, but the authoring pattern for single-file components is Python imports. Slot content resolves in the scope where it was authored, so the template that writes an `x-*` tag must import that component.
72
+ - 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.
73
73
  - 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.
74
74
  - 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.
75
75
  - 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.
76
76
  - For live bidirectional channels, first confirm `caspian.config.json` has `websocket: true`, then use app-owned FastAPI WebSocket endpoints in `main.py` plus native browser `WebSocket` clients inside the owning PulsePoint route template. Do not replace normal CRUD, form submits, uploads, or one-way progress streams with WebSockets.
77
77
  - When `caspian.config.json` has `websocket: true`, WebSocket endpoint paths are project-defined in `main.py`; do not assume any default socket path or route folder exists in every Caspian project. Keep shared socket helpers under `src/lib/websocket/**` when session extraction, auth payload validation, connection tracking, or broadcast behavior is reused.
78
- - For route creation, keep page markup in `src/app/**/index.html`. If a route is UI-only, `index.html` alone is sufficient. Add `src/app/**/index.py` only as a companion when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side behavior. Keep shared section wrappers in `layout.html` and use `layout.py` only for shared props or metadata. Do not place route HTML in `index.py` or layout HTML in `layout.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
78
+ - 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`.
79
79
  - 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.
80
80
  - Treat the single-root template contract as a hard requirement, not a style preference: every authored route, layout, and component HTML file must have exactly one parent HTML element or one imported `x-*` component tag as its root. Do not leave sibling top-level markup, and do not place a `<script>` after the root element. If a script is needed, keep it inside that same root.
81
- - 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.html`, add `layout.py` only when that section needs shared props or metadata, 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.
81
+ - 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.
82
82
  - 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.
83
- - When a single route needs to affect a wrapping layout, have `page()` return `(render_page(__file__, page_context), {"dashboard_body_class": ...})` and consume that value as `{{ layout.dashboard_body_class }}` in `layout.html`. Use `layout.py` when the same prop should apply across a whole subtree.
83
+ - 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.
84
84
  - 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.
85
85
  - `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.
86
86
  - 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.
@@ -90,9 +90,9 @@ This is the top architectural requirement for this workspace. Treat it as a hard
90
90
  - 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.
91
91
  - `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.
92
92
  - Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
93
- - In `layout.py`, return a dict for standard `{{ layout.* }}` props. Use `render_layout(__file__, {...})` only when that layout should consume direct local variables such as `{{ my_class }}` instead of `{{ layout.my_class }}`.
93
+ - In `layout.py`, `layout()` returns the raw wrapper template string (compiled later with `children`/`layout`/`metadata` in scope), `(template, props_dict)`, a props dict alone (passthrough `<slot />` shell), or `None`.
94
94
  - Do not assume `StateManager` survives across requests unless `request.state.session` is explicitly bridged from `request.session`.
95
- - Route, layout, and component HTML 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, and keep top-of-file `<!-- @import ... -->` directives above it.
95
+ - 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.
96
96
 
97
97
  ## BrowserSync URL Source Of Truth
98
98
 
@@ -113,8 +113,8 @@ This is the top architectural requirement for this workspace. Treat it as a hard
113
113
  - 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/**`.
114
114
  - Document route param behavior exactly as implemented here.
115
115
  - Do not use `main.py` alone to infer whether optional features are enabled; confirm that in `caspian.config.json` first.
116
- - Before changing WebSocket behavior, verify `cfg.websocket`, the app's endpoint registration, the `authorize_websocket(...)` guard in `src/lib/websocket/websocket_security.py`, idle timeout, maximum message size, close codes, and connection cleanup. HTTP-only middleware does not automatically protect `scope["type"] == "websocket"` connections, so socket auth lives in that guard, not `AuthMiddleware`.
117
- - Authorize sockets with the single `authorize_websocket(...)` guard, which runs the origin check then delegates to Caspian `Auth` (`Auth.set_request(websocket)` + `is_authenticated`/`get_payload`/`check_role`). Add channels by calling that guard with `require_auth=`/`roles=`; do not re-implement session/`exp`/payload parsing per endpoint. Keep authenticated and guest broadcast pools separate, and treat the socket session as read-only.
116
+ - 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`.
117
+ - 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.
118
118
 
119
119
  ### `src/lib/**/*.py`
120
120
 
@@ -125,21 +125,21 @@ This is the top architectural requirement for this workspace. Treat it as a hard
125
125
  - 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.
126
126
  - Keep auth policy in `src/lib/auth/auth_config.py`. Keep auth bootstrap and middleware order changes in `main.py`.
127
127
  - 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.
128
- - Keep reusable WebSocket helpers under `src/lib/websocket/**` when they are shared across socket endpoints or route clients. Common shared helpers include the `authorize_websocket(...)` guard (origin + `Auth`-delegated auth), origin utilities, connection managers, payload normalization, and broadcast fan-out.
128
+ - 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`.
129
129
 
130
130
  ### `src/components/**/*.py`
131
131
 
132
132
  - 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).
133
133
  - Move shared cards, forms, shells, navigation, and other reusable rendered building blocks here once they are used across routes or features.
134
134
  - Keep route-owned markup in `src/app/**`, and keep non-UI helpers or services in `src/lib/**`.
135
- - Author components as a single Python file with inline `html(...)` by default for small and medium UI, or as a `.py` plus same-name `.html` via `render_html(...)` for large markup or long scripts. Keep the single-root rule in both forms. Resolve child `x-*` tags from real Python imports in single-file components rather than `<!-- @import ... -->`. Prefer one focused component per file unless a file intentionally exports tiny, tightly coupled subcomponents. See `node_modules/caspian-utils/dist/docs/components.md`.
135
+ - 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`.
136
136
 
137
137
  ### `tests/**/*.py` and `settings/check.py`
138
138
 
139
139
  - 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.
140
140
  - 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`).
141
141
  - `npm run check` only reports. Auto-fix with `npm run check:fix`, which runs `settings/fix.py` (safe ruff fixes, then the gate). pyright and pytest failures are never auto-fixed.
142
- - 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(...)`/`render_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`.
142
+ - 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`.
143
143
  - 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/**`.
144
144
  - `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.
145
145
  - 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`.
@@ -171,12 +171,12 @@ This is the top architectural requirement for this workspace. Treat it as a hard
171
171
  ### `public/js/main.js`
172
172
 
173
173
  - Treat `public/js/main.js` as the thin browser bootstrap entry point.
174
- - Keep it minimal and point it at the runtime shipped in `public/js/pp-reactive-v2.js`.
174
+ - Keep it minimal and point it at the runtime shipped in `public/js/pp-reactive-v2.min.js`.
175
175
  - Do not duplicate PulsePoint runtime logic here.
176
176
 
177
- ### `public/js/pp-reactive-v2.js`
177
+ ### `public/js/pp-reactive-v2.min.js`
178
178
 
179
- - Treat `public/js/pp-reactive-v2.js` as the browser-side PulsePoint runtime source of truth for component execution, hooks, refs, directives, SPA navigation, and `pp.rpc(...)` behavior.
179
+ - 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.
180
180
  - 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.
181
181
  - Preserve the current public runtime contract unless the task explicitly changes Caspian frontend behavior.
182
182
  - 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.
@@ -184,11 +184,10 @@ This is the top architectural requirement for this workspace. Treat it as a hard
184
184
 
185
185
  ### `src/app/**/*.html`
186
186
 
187
- - Compose pages from components first (see "Component-First Page Composition"). Keep `index.html` 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.
187
+ - 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.
188
188
  - Keep route templates and layouts server-rendered first, with PulsePoint enhancement as the default interactive layer.
189
- - Keep visible page and layout markup in `index.html` and `layout.html`. Treat `index.py` and `layout.py` as backend companions for metadata, `page()` or `layout()`, `@rpc()` actions, auth checks, caching, redirects, and other server-side preparation, not as places to author visible HTML.
190
- - When a route renders UI, author that markup in the route's `index.html` even if the route also has an `index.py` companion.
191
- - When route templates import reusable Python components, render them as kebab-cased `x-*` tags such as `<x-button />` after top-of-file `<!-- @import Button from "..." -->` directives. The import comments belong above the single route root, not inside it.
189
+ - 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.
190
+ - 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.
192
191
  - 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.
193
192
  - 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.
194
193
  - 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.
@@ -196,7 +195,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
196
195
  - Do not author `pp-component="..."` manually in route or layout templates; the Python render pipeline injects it onto the single root element.
197
196
  - Use a plain `<script>` inside the single route or layout root when it owns PulsePoint logic; no custom script type is required.
198
197
  - Keep authored route and layout templates to exactly one top-level parent node, the same constraint 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. AI must follow this the same way React components return one parent node, otherwise Caspian raises `must have exactly one top-level HTML element so Caspian can inject pp-component`.
199
- - For dashboard, admin, or grouped sections with multiple child routes, prefer folder-level `layout.html` wrappers in `src/app/**` instead of repeating the same shell in each child route.
198
+ - 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.
200
199
  - 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.
201
200
  - For upload managers and similar interactive lists, prefer `pp.state(...)` plus `pp-for` over manual DOM painting so rerenders keep the list stable.
202
201
  - For route-owned WebSocket clients, use PulsePoint state, refs, and cleanup effects around the native `WebSocket`. Keep the socket object in `pp.ref(...)`, close it on component disposal, and keep socket event listeners inside the owning route template script.
@@ -232,7 +231,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
232
231
 
233
232
  - These files are the packaged Caspian documentation layer, not the runtime and not the source of current workspace state.
234
233
  - 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.
235
- - Use `node_modules/caspian-utils/dist/docs/file-conventions.md` when deciding what belongs in `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, or `error.html`.
234
+ - Use `node_modules/caspian-utils/dist/docs/file-conventions.md` when deciding what belongs in `index.py`, `layout.py`, `loading.html`, `not-found.html`, or `error.html`.
236
235
  - Use `node_modules/caspian-utils/dist/docs/websockets.md` when deciding how to document or implement app-owned FastAPI WebSockets, browser `WebSocket` clients, origin checks, auth/session checks, message contracts, and the choice between WebSockets, RPC, and SSE.
237
236
  - Verify behavior claims in this order:
238
237
  1. `caspian.config.json`, then `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, `src/app/**`