caspian-utils 0.1.26 → 0.2.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.
@@ -30,7 +30,7 @@ Use it when you already know a behavior is controlled by `main.py` or `.venv/Lib
30
30
 
31
31
  | Concern | Core file | Read first | Why it matters |
32
32
  | --- | --- | --- | --- |
33
- | App bootstrap and request flow | [main.py](../../../../main.py) plus imported runtime helpers such as `casp.runtime_security` | [project-structure.md](./project-structure.md), [routing.md](./routing.md), [auth.md](./auth.md) | FastAPI app creation, middleware wiring, route registration, cache check and save, exception handlers, and final HTML transforms live here. Package-owned helpers imported by `main.py` may own safe public-file serving, baseline non-CSP response headers, production-safe error messages, or production session-secret enforcement. |
33
+ | App bootstrap and request flow | [main.py](../../../../main.py) plus imported runtime helpers such as `casp.runtime_security` | [project-structure.md](./project-structure.md), [routing.md](./routing.md), [auth.md](./auth.md) | FastAPI app creation, middleware wiring, route registration, cache check and save, exception handlers, and final HTML transforms live here. Package-owned helpers imported by `main.py` may own safe public-file serving, baseline response headers including the Content-Security-Policy, production-safe error messages, fail-closed environment resolution, or production session-secret enforcement. |
34
34
  | Browser runtime, SPA navigation, and scroll restoration | [public/js/pp-reactive-v2.js](../../../../public/js/pp-reactive-v2.js) | [pulsepoint.md](./pulsepoint.md), [fetch-data.md](./fetch-data.md) | The shipped `pp` runtime, same-origin SPA interception, per-history-entry scroll state, `pp-reset-scroll` behavior, and browser RPC helpers live here. |
35
35
 
36
36
  Important current `main.py` behaviors AI should keep in mind:
@@ -41,8 +41,8 @@ Important current `main.py` behaviors AI should keep in mind:
41
41
  - Route-level generators returned from `page()` are wrapped in `SSE(...)` before the response is sent.
42
42
  - Safe public-file helpers live in `casp.runtime_security` and should reject `..` traversal before serving from `public/**`.
43
43
  - Session middleware secrets may be resolved through `casp.runtime_security` so production can fail fast when `AUTH_SECRET` is missing or still on a default placeholder.
44
- - Baseline non-CSP response headers may be built by `casp.runtime_security` and attached through `SecurityHeadersMiddleware`. Do not assume that helper is a third-party browser resource or domain registry; check the current helper before adding CSP behavior.
45
- - Middleware is added in source order as `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`, `SecurityHeadersMiddleware`, so the effective request order is reversed at runtime.
44
+ - Baseline response headers are built by `casp.runtime_security` and attached through `SecurityHeadersMiddleware`. That set now includes a Content-Security-Policy from `build_content_security_policy()`, which the `CONTENT_SECURITY_POLICY` environment variable replaces wholesale. The policy must keep `'unsafe-eval'` and `'unsafe-inline'` in `script-src`, because the PulsePoint runtime compiles templates with `new Function`; read the current helper before tightening it. Outside production the helper also adds loopback origins to `connect-src` so the BrowserSync live-reload client, which polls a different port than the proxied page, is not blocked; an environment override replaces that too.
45
+ - Middleware is added in source order as `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`, `SecurityHeadersMiddleware`, plus `RequestDiagnosticsMiddleware` outside production. The effective request order is reversed at runtime, so security headers and the rate limit run first and `RPCMiddleware` runs last. Verify the current list in `main.py` rather than trusting this order.
46
46
  - `public/js/pp-reactive-v2.js` saves scroll positions per history entry, resets window scroll on push navigation, and uses `pp-reset-scroll="true"` to opt specific containers or the whole body into reset behavior.
47
47
 
48
48
  ## Caspian Core Feature Map
@@ -92,7 +92,7 @@ Interactive CRUD page:
92
92
  | --- | --- | --- |
93
93
  | [.venv/Lib/site-packages/casp/layout.py](../../../../.venv/Lib/site-packages/casp/layout.py) | `render_page(...)`, `render_layout(...)`, nested layout discovery, metadata merge, sync or async `layout()` results, and parser-based `<slot />` replacement | [routing.md](./routing.md), [metadata.md](./metadata.md) |
94
94
  | [.venv/Lib/site-packages/casp/auth.py](../../../../.venv/Lib/site-packages/casp/auth.py) | `AuthSettings`, route privacy checks, session payloads, OAuth providers, CSRF helper behavior, and redirect logic | [auth.md](./auth.md) |
95
- | [.venv/Lib/site-packages/casp/runtime_security.py](../../../../.venv/Lib/site-packages/casp/runtime_security.py) | safe public-file serving, baseline non-CSP response headers, production-safe error messages, and production session-secret enforcement used by `main.py` | [project-structure.md](./project-structure.md), [auth.md](./auth.md) |
95
+ | [.venv/Lib/site-packages/casp/runtime_security.py](../../../../.venv/Lib/site-packages/casp/runtime_security.py) | safe public-file serving (with optional attachment mode for user uploads), baseline response headers including the CSP, production-safe error messages, fail-closed `APP_ENV` resolution via `is_production_environment()`, and production session-secret enforcement used by `main.py` | [project-structure.md](./project-structure.md), [auth.md](./auth.md) |
96
96
  | [.venv/Lib/site-packages/casp/rpc.py](../../../../.venv/Lib/site-packages/casp/rpc.py) | `@rpc()` registration, rate limits, request handling, auth-aware action checks, and streamed RPC responses | [fetch-data.md](./fetch-data.md) |
97
97
  | [.venv/Lib/site-packages/casp/streaming.py](../../../../.venv/Lib/site-packages/casp/streaming.py) | `SSE`, `ServerSentEvent`, and generator-to-event-stream wrapping | [fetch-data.md](./fetch-data.md) |
98
98
  | [.venv/Lib/site-packages/casp/state_manager.py](../../../../.venv/Lib/site-packages/casp/state_manager.py) | request-scoped state, session bucket persistence, listener lifecycle, and wire-request reset behavior | [state.md](./state.md) |
@@ -356,7 +356,25 @@ Use [auth.md](./auth.md) when the action should also participate in centralized
356
356
 
357
357
  According to the upstream Caspian RPC docs, actions are private by default until decorated with `@rpc()`, and the framework includes CSRF protection plus origin validation for exposed actions.
358
358
 
359
- For reverse-proxy deployments, RPC origin validation can also resolve the public app origin from `APP_BASE_URL`, `PUBLIC_BASE_URL`, or `SITE_URL`, and otherwise falls back to forwarded host and protocol headers when they are present.
359
+ For reverse-proxy deployments, RPC origin validation resolves the public app origin from `APP_BASE_URL` and `CORS_ALLOWED_ORIGINS`. Forwarded host and protocol headers are consulted **only** when `TRUST_FORWARDED_HEADERS` is enabled, because a direct client can set those headers itself; the same flag decides whether the forwarded address is used as the rate-limit bucket key.
360
+
361
+ ### Only Declared Parameters Are Accepted
362
+
363
+ The RPC payload is attacker-controlled, so the runtime filters it against the function signature before the call. A key that does not name a declared parameter is dropped rather than passed through.
364
+
365
+ This makes a parameter client-settable only when you declare it. Anything the server should decide — the acting user, an ownership check, a privilege flag — must be derived inside the function rather than accepted as an argument with a default:
366
+
367
+ ```python
368
+ @rpc(require_auth=True)
369
+ def update_post(post_id: int, title: str = ""):
370
+ # Correct: identity comes from the session, not the payload.
371
+ author = auth.get_payload()
372
+
373
+ # If this were `def update_post(..., author_id: str = "")`, the browser
374
+ # could set author_id and edit another user's post.
375
+ ```
376
+
377
+ Declaring `**kwargs` opts the function into the entire payload, so use it deliberately. A sent-but-undeclared key is silently ignored, which also means a client-side typo shows up as a missing value rather than a 500.
360
378
 
361
379
  ## Serialization Rules
362
380
 
@@ -31,7 +31,7 @@ In that layout, the default stack is Python components for reusable UI, PulsePoi
31
31
 
32
32
  For public pages that can safely reuse rendered HTML, Caspian also supports route-level page caching through `casp.cache_handler`.
33
33
 
34
- Before an AI agent decides which Caspian features are available in a workspace, it should read `./caspian.config.json` almost immediately. That file is the feature gate for project capabilities such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, `websocket`, and `componentScanDirs`.
34
+ Before an AI agent decides which Caspian features are available in a workspace, it should read `./caspian.config.json` almost immediately. That file is the feature gate for project capabilities such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, `websocket`, and `componentScanDirs`.
35
35
 
36
36
  Treat `caspian.config.json` as the single source of truth for optional feature enablement. Use feature-specific files and docs only after the matching flag is confirmed as enabled. If a feature is disabled and the user wants it, ask first, then update `caspian.config.json` and follow the update workflow in `commands.md`.
37
37
 
@@ -40,10 +40,10 @@ Treat `caspian.config.json` as the single source of truth for optional feature e
40
40
  - `src/` contains routes, page templates, styles, reusable components, and shared libraries.
41
41
  - `src/components/` contains reusable application UI components and optional same-name HTML templates.
42
42
  - `src/lib/` contains reusable non-UI code such as helpers, services, validators, adapters, and shared support modules.
43
- - `src/lib/auth/auth_config.py` contains auth-specific configuration for the app.
44
- - `src/lib/mcp/` contains the app-owned FastMCP server and nested FastMCP config when MCP is enabled.
45
- - `src/lib/websocket/` contains reusable socket helpers when WebSockets are enabled and the project includes shared session, auth, connection, or broadcast utilities.
46
- - `prisma/` contains the Prisma schema and seed scripts.
43
+ - `src/lib/auth/auth_config.py` contains auth-specific configuration for the app.
44
+ - `src/lib/mcp/` contains the app-owned FastMCP server and nested FastMCP config when MCP is enabled.
45
+ - `src/lib/websocket/` contains reusable socket helpers when WebSockets are enabled and the project includes shared session, auth, connection, or broadcast utilities.
46
+ - `prisma/` contains the Prisma schema and seed scripts.
47
47
  - `public/` contains static assets served directly.
48
48
  - `settings/` contains BrowserSync, build, restart, and generated-project helper files.
49
49
  - `main.py` is the application entry point.
@@ -78,15 +78,15 @@ my-app/
78
78
  lib/
79
79
  auth/
80
80
  auth_config.py
81
- mcp/
82
- fastmcp.json
83
- mcp_server.py
84
- websocket/
85
- websocket_security.py
86
- prisma/
87
- __init__.py
88
- db.py
89
- models.py
81
+ mcp/
82
+ fastmcp.json
83
+ mcp_server.py
84
+ websocket/
85
+ websocket_security.py
86
+ prisma/
87
+ __init__.py
88
+ db.py
89
+ models.py
90
90
  .venv/
91
91
  Lib/
92
92
  site-packages/
@@ -97,7 +97,7 @@ my-app/
97
97
  docs/
98
98
  ```
99
99
 
100
- Optional directories such as `src/lib/mcp/` and `src/lib/websocket/` appear only when the relevant feature flag is enabled in `caspian.config.json` and the project needs that app-owned surface.
100
+ Optional directories such as `src/lib/mcp/` and `src/lib/websocket/` appear only when the relevant feature flag is enabled in `caspian.config.json` and the project needs that app-owned surface.
101
101
 
102
102
  ## Directory Breakdown
103
103
 
@@ -107,11 +107,11 @@ This is the main application area. It contains route files, templates, styles, a
107
107
 
108
108
  ### `src/app/`
109
109
 
110
- This directory handles file-based routing. Route templates and route-specific backend logic live here.
111
-
112
- For any route that renders UI, keep that markup in `src/app/**/index.html`. If the route is UI-only, `index.html` alone is enough. 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 wrappers in `layout.html` and use `layout.py` only for shared props or metadata. Use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
113
-
114
- Keep backend logic in the owning route when it is route-specific. Move Python code into `src/lib/**` only when it is shared across routes, components, integrations, or features.
110
+ This directory handles file-based routing. Route templates and route-specific backend logic live here.
111
+
112
+ For any route that renders UI, keep that markup in `src/app/**/index.html`. If the route is UI-only, `index.html` alone is enough. 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 wrappers in `layout.html` and use `layout.py` only for shared props or metadata. Use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
113
+
114
+ Keep backend logic in the owning route when it is route-specific. Move Python code into `src/lib/**` only when it is shared across routes, components, integrations, or features.
115
115
 
116
116
  When a folder represents a section with child routes, such as `dashboard`, `account`, `settings`, or `docs`, create `layout.html` in that folder and let the child routes live beneath it. See [routing.md](./routing.md) for the canonical section layout pattern.
117
117
 
@@ -125,11 +125,11 @@ See `routing.md` for the full App Router-style rules for dynamic segments, route
125
125
 
126
126
  Use this folder for reusable UI components that should be imported into route templates or other component templates.
127
127
 
128
- As the app grows, default to `src/components/` for application-level UI that will be shared across routes or features. Keep page-only markup close to the route in `src/app/`, but move shared cards, forms, shells, navigation, and other reusable visual building blocks into `src/components/`.
129
-
130
- For page-specific chunks that are still substantial, a route-local component folder is also acceptable. Use this when a component is owned by one route but deserves its own focused file, such as one tab panel, one settings form, one analytics section, or one table toolbar. The route should still read as a short assembly of named `x-*` tags.
131
-
132
- The common Caspian pattern is a Python file such as `Button.py` with `@component`, optionally paired with a same-name HTML file such as `Button.html` when the component has richer markup or PulsePoint behavior. In authored HTML, that component is consumed as `<x-button />`.
128
+ As the app grows, default to `src/components/` for application-level UI that will be shared across routes or features. Keep page-only markup close to the route in `src/app/`, but move shared cards, forms, shells, navigation, and other reusable visual building blocks into `src/components/`.
129
+
130
+ For page-specific chunks that are still substantial, a route-local component folder is also acceptable. Use this when a component is owned by one route but deserves its own focused file, such as one tab panel, one settings form, one analytics section, or one table toolbar. The route should still read as a short assembly of named `x-*` tags.
131
+
132
+ The common Caspian pattern is a Python file such as `Button.py` with `@component`, optionally paired with a same-name HTML file such as `Button.html` when the component has richer markup or PulsePoint behavior. In authored HTML, that component is consumed as `<x-button />`.
133
133
 
134
134
  One Python file can also export multiple related `@component` functions. When that happens, import those tags from that exact file path in HTML, for example `<!-- @import { Breadcrumb, BreadcrumbItem, BreadcrumbList } from "../components/Breadcrumb.py" -->`, and render them as `<x-breadcrumb />`, `<x-breadcrumb-item />`, and `<x-breadcrumb-list />` instead of assuming each tag has its own sibling `.py` file.
135
135
 
@@ -137,7 +137,7 @@ For component HTML files, follow the component authoring rules in [components.md
137
137
 
138
138
  The directories listed in `componentScanDirs` determine where component tooling scans. When that list includes `src/`, `src/components/` is a conventionally clean location, not a hard-coded runtime requirement.
139
139
 
140
- ### `src/lib/`
140
+ ### `src/lib/`
141
141
 
142
142
  Use this folder for shared helpers, reusable validators, RPC-facing service wrappers, data-access helpers, formatting utilities, and other app-level support code that is not itself a reusable rendered component.
143
143
 
@@ -147,15 +147,15 @@ For file upload and manager flows, keep route-owned `@rpc()` actions in `src/app
147
147
 
148
148
  When a project includes an app-owned Python database layer under `src/lib/prisma/`, reuse that package for Python-side data access and keep any additional shared database helpers in `src/lib/`.
149
149
 
150
- When MCP is enabled for the project, this folder also contains the app-owned FastMCP server under `src/lib/mcp/`.
151
-
152
- Do not add `src/lib/security/runtime_security.py` for normal app work. Runtime security helpers for safe public-file serving, production session-secret enforcement, production-safe error messages, and baseline non-CSP response headers are package-owned by `casp.runtime_security`.
150
+ When MCP is enabled for the project, this folder also contains the app-owned FastMCP server under `src/lib/mcp/`.
151
+
152
+ Do not add `src/lib/security/runtime_security.py` for normal app work. Runtime security helpers for safe public-file serving, production session-secret enforcement, production-safe error messages, fail-closed environment resolution, and baseline response headers including the Content-Security-Policy are package-owned by `casp.runtime_security`.
153
153
 
154
154
  ### Shared Database Helpers
155
155
 
156
156
  If your Python routes or RPC actions need reusable database access code, keep that helper layer under `src/lib/` and extend the existing `src/lib/prisma/` package.
157
157
 
158
- In a Prisma-enabled Caspian project, schema and seed files typically live under `prisma/`, while the Python-side adapter lives in application-owned code under `src/lib/prisma/` when that layer exists. When `caspian.config.json` has `prisma: true`, Python-side database reads and writes should use that generated Prisma Python ORM instead of a custom fetch, raw driver, JSON active store, or second app-owned database abstraction.
158
+ In a Prisma-enabled Caspian project, schema and seed files typically live under `prisma/`, while the Python-side adapter lives in application-owned code under `src/lib/prisma/` when that layer exists. When `caspian.config.json` has `prisma: true`, Python-side database reads and writes should use that generated Prisma Python ORM instead of a custom fetch, raw driver, JSON active store, or second app-owned database abstraction.
159
159
 
160
160
  ### `src/lib/auth/`
161
161
 
@@ -194,31 +194,31 @@ If the local BrowserSync stack is running, keep that upload directory in `settin
194
194
 
195
195
  The application entry point for the project.
196
196
 
197
- In the current Caspian app shape, this file is where startup wiring happens:
198
-
199
- - load environment variables
200
- - call `configure_auth(build_auth_settings())`
201
- - register OAuth providers with `Auth.set_providers(...)`
202
- - create the FastAPI app
203
- - register routes and RPC handlers
204
- - add response headers middleware, `SessionMiddleware`, CSRF middleware, auth middleware, and RPC middleware
205
-
206
- Use `main.py` for auth bootstrap and middleware-order changes. Use `src/lib/auth/auth_config.py` for auth policy values such as public routes, redirects, and RBAC maps.
207
-
208
- ### `.venv/Lib/site-packages/casp/runtime_security.py`
209
-
210
- The installed Caspian package owns runtime security helpers that `main.py` can import as `casp.runtime_security`. Keep this file in the package instead of copying it into `src/lib/`, because normal app users should not need to edit it.
211
-
212
- This helper is not a registry for third-party browser resources. Do not add Google, YouTube, CDN, image-host, API, or iframe origins here just because the browser or server uses them. If an app later chooses to enforce a Content Security Policy, document and implement that as an explicit project policy rather than assuming `runtime_security.py` owns a domain allowlist.
213
-
214
- Use this package module when the task is about:
215
-
216
- - safe serving of files from `public/**`
217
- - production session-secret enforcement
218
- - user-facing vs production-safe exception messaging helpers
219
- - baseline response headers such as permissions, referrer, MIME sniffing, framing, or HSTS behavior
220
-
221
- Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
197
+ In the current Caspian app shape, this file is where startup wiring happens:
198
+
199
+ - load environment variables
200
+ - call `configure_auth(build_auth_settings())`
201
+ - register OAuth providers with `Auth.set_providers(...)`
202
+ - create the FastAPI app
203
+ - register routes and RPC handlers
204
+ - add response headers middleware, `SessionMiddleware`, CSRF middleware, auth middleware, and RPC middleware
205
+
206
+ Use `main.py` for auth bootstrap and middleware-order changes. Use `src/lib/auth/auth_config.py` for auth policy values such as public routes, redirects, and RBAC maps.
207
+
208
+ ### `.venv/Lib/site-packages/casp/runtime_security.py`
209
+
210
+ The installed Caspian package owns runtime security helpers that `main.py` can import as `casp.runtime_security`. Keep this file in the package instead of copying it into `src/lib/`, because normal app users should not need to edit it.
211
+
212
+ This helper is not a registry for third-party browser resources. Do not add Google, YouTube, CDN, image-host, API, or iframe origins here just because the browser or server uses them. If an app later chooses to enforce a Content Security Policy, document and implement that as an explicit project policy rather than assuming `runtime_security.py` owns a domain allowlist.
213
+
214
+ Use this package module when the task is about:
215
+
216
+ - safe serving of files from `public/**`
217
+ - production session-secret enforcement
218
+ - user-facing vs production-safe exception messaging helpers
219
+ - baseline response headers such as permissions, referrer, MIME sniffing, framing, or HSTS behavior
220
+
221
+ Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
222
222
 
223
223
  ### `caspian.config.json`
224
224
 
@@ -268,7 +268,7 @@ Keep visible wrapper markup in `layout.html`, not in `layout.py`.
268
268
 
269
269
  ### `src/app/layout.py`
270
270
 
271
- The backend companion for a layout. Use this file for shared props, metadata defaults, and other server-side preparation for the sibling `layout.html`.
271
+ The backend companion for a layout. Use this file for shared props, metadata defaults, and other server-side preparation for the sibling `layout.html`.
272
272
 
273
273
  Do not store layout HTML in `layout.py`. Keep the authored wrapper in `layout.html` and let `layout.py` return props or metadata.
274
274
 
@@ -330,17 +330,17 @@ If an AI agent is deciding where to make changes, use these rules first.
330
330
  - Treat `package.json` scripts as opt-in operations. Do not run `npm run dev` or `npm run build` unless the user explicitly asks, the task genuinely requires that exact script, or deployment prep needs `npm run build`.
331
331
  - Treat `__pycache__/` directories, `.pyc` files, `public/css/styles.css`, `settings/component-map.json`, and `settings/files-list.json` as generated artifacts when the local stack is intentionally running. They are not authored source files.
332
332
  - Inspect `settings/component-map.json` and `settings/files-list.json` when you need the generated component or route inventory, but do not hand-edit them. The workspace regenerates them from `settings/component-map.ts` and `settings/files-list.ts`.
333
- - Put route templates and route-specific backend logic in `src/app/`.
334
- - Put only genuinely shared helpers, services, adapters, and validation logic in `src/lib/`.
333
+ - Put route templates and route-specific backend logic in `src/app/`.
334
+ - Put only genuinely shared helpers, services, adapters, and validation logic in `src/lib/`.
335
335
  - As the app grows, keep route-owned code in `src/app/`, reusable rendered UI in `src/components/`, and reusable non-UI support code in `src/lib/`.
336
336
  - When the user asks for a dashboard, admin area, account area, or any grouped set of child routes, create a parent folder in `src/app/` with `layout.html` and place the child routes beneath it. Use `(group)/layout.html` only when that parent should not appear in the URL.
337
337
  - Read [file-uploads.md](./file-uploads.md) when the task involves upload widgets, media libraries, or file-manager flows.
338
338
  - Check [routing.md](./routing.md) when you need URL segment rules, layout nesting behavior, or dynamic route conventions.
339
- - Put reusable component files in `src/components/` and check [components.md](./components.md) for `@component`, `render_html(__file__)`, `x-*` component tags, import comments, and single-root template rules.
340
- - Split UI by component responsibility before files become large. A tab panel, settings form, table section, toolbar, or repeated card/list item can be its own component even when it is only used by one route.
341
- - When deciding between `src/components/` and `src/lib/`, use `src/components/` for anything rendered as reusable UI and `src/lib/` for helpers, services, validators, adapters, and shared business logic.
339
+ - Put reusable component files in `src/components/` and check [components.md](./components.md) for `@component`, `render_html(__file__)`, `x-*` component tags, import comments, and single-root template rules.
340
+ - Split UI by component responsibility before files become large. A tab panel, settings form, table section, toolbar, or repeated card/list item can be its own component even when it is only used by one route.
341
+ - When deciding between `src/components/` and `src/lib/`, use `src/components/` for anything rendered as reusable UI and `src/lib/` for helpers, services, validators, adapters, and shared business logic.
342
342
  - Use [mcp.md](./mcp.md) only when `caspian.config.json` enables MCP and the task involves FastMCP tool definitions, nested config discovery, or local MCP commands.
343
- - Keep `<!-- @import ... -->` comments above the single authored root element in route, layout, and component HTML files. Do not nest them inside `<html>`, `<body>`, `<section>`, or any other parent wrapper.
343
+ - Keep `<!-- @import ... -->` comments above the single authored root element in route, layout, and component HTML files. Do not nest them inside `<html>`, `<body>`, `<section>`, or any other parent wrapper.
344
344
  - Follow the single-root authoring contract in [routing.md](./routing.md), [components.md](./components.md), and [pulsepoint.md](./pulsepoint.md): one authored root, any owned `<script>` inside that root, and no handwritten `pp-component` or `type="text/pp"` in source templates.
345
345
  - Put shared helpers and reusable libraries in `src/lib/`.
346
346
  - Use `settings/bs-config.ts` when uploaded public assets should not trigger BrowserSync reloads during the local stack.
@@ -31,23 +31,31 @@ If an inspected browser DOM disagrees with authored template source, remember th
31
31
 
32
32
  | PulsePoint feature | Authoring surface | Runtime owner | Verify before changing |
33
33
  | --- | --- | --- | --- |
34
- | Component roots | `src/app/**/index.html`, `layout.html`, component `.html` files | `components_compiler.py`, `pp-reactive-v2.js` | one authored root, final expanded root receives one `pp-component`, no sibling scripts |
35
- | Component scripts | plain `<script>` inside the authored root | `scripts_type.py`, `pp-reactive-v2.js` | authored scripts are plain, runtime scripts become `type="text/pp"`, one owned script per root |
36
- | Template expressions | text and attributes with `{...}` | `pp-reactive-v2.js` | top-level script bindings are exported, nested bindings are not assumed |
37
- | State | `pp.state(initial)` | `pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
38
- | Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
39
- | Refs | `pp.ref(...)`, `pp-ref` on native or `x-*` tags | `components_compiler.py`, `TemplateCompiler.ts`, `Component.ts`, `RefBindingManager.ts`, shipped in `pp-reactive-v2.js` | component-tag refs are reserved by the Python compiler, stamped with runtime-owned `pp-ref-owner`, captured in the parent's scope, and bound to the child's concrete root; do not author `data-pp-ref`, `pp-ref-owner`, or `pp-ref-forward`; callback refs may return synchronous cleanup functions that run on replacement or detach |
40
- | Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `TemplateCompiler.ts`, `NestedBoundaryManager.ts`, `pp-reactive-v2.js` | authored provider tags are HTML-first and lowercase; `TemplateCompiler.transformContextProviderTags(...)` rewrites `*.provider` to runtime-owned `pp-context-provider`; ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
41
- | Portals | `pp.portal(ref, target?)` | `pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
42
- | Lists | `<template pp-for="item in items">` | `pp-reactive-v2.js` | `pp-for` belongs on `<template>`, accepts arrays and synchronous iterables, captures rendered row values for events/props, and uses plain `key`, not `pp-key` |
43
- | Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `pp-reactive-v2.js` | first-party events belong in `on*` attributes; event scope exposes `event`, `e`, `$event`, `target`, `currentTarget`, and `el`; normal form submits should use `Object.fromEntries(new FormData(event.currentTarget).entries())` instead of per-input refs; avoid id-driven `querySelector`/`addEventListener` for normal UI |
44
- | RPC | `pp.rpc(...)` in scripts, `@rpc()` in Python | `pp-reactive-v2.js`, `casp/rpc.py` | use `pp.rpc`, not legacy `pp.fetchFunction`; protected actions use `@rpc(require_auth=True)` |
45
- | Upload progress | `pp.rpc(..., { onUploadProgress })` | `pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; the callback receives `{ loaded, total, percent }` and `percent` can be `null` when length is not computable; replace state from returned payload |
46
- | Streaming | `pp.rpc(..., { onStream })` | `pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
47
- | SPA navigation | `body[pp-spa="true"]`, links | `pp-reactive-v2.js`, `main.py` | same-origin eligible links intercept; root-layout mismatches hard reload |
48
- | Scroll restoration | `pp-reset-scroll="true"` | `pp-reactive-v2.js` | push navigation resets window; mark only content panes that should reset |
49
- | Tailwind merge | `{twMerge(...)}`, Python `merge_classes(...)` | `html_attrs.py`, `pp-reactive-v2.js` | Python emits frontend-ready expressions when Tailwind is enabled |
50
- | Markup deferral | `<template pp-component>` / `<template pp-owner>` wrappers | render pipeline (`main.py`), `pp-reactive-v2.js` (`materializeTemplateComponentBoundaries`) | top-level roots ship inside an inert `<template>`; runtime materializes them on mount and SPA navigation before scanning. Browser never validates `<template>` contents, so `{...}` is safe in any attribute/position (SVG geometry, `src`/`href`, form `value`/date, table/select text) — no per-tag workarounds. See [pulsepoint.md](./pulsepoint.md) "Component markup is deferred inside an inert `<template>`" |
34
+ | Component roots | `src/app/**/index.html`, `layout.html`, component `.html` files | `components_compiler.py`, `public/js/pp-reactive-v2.js` | one authored root, final expanded root receives one `pp-component`, no sibling scripts |
35
+ | Component scripts | plain `<script>` inside the authored root | `scripts_type.py`, `public/js/pp-reactive-v2.js` | authored scripts are plain, runtime scripts become `type="text/pp"`, one owned script per root |
36
+ | Template expressions | text and attributes with `{...}` | `public/js/pp-reactive-v2.js` | top-level script bindings are exported, nested bindings are not; every destructuring shape is supported at the top level (array, object, nested, defaults, rest, holes), so any tuple-returning hook reaches template scope |
37
+ | State | `pp.state(initial)` | `public/js/pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
38
+ | Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `public/js/pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
39
+ | Refs | `pp.ref(...)`, `pp-ref` on native or `x-*` tags | `components_compiler.py`, `TemplateCompiler.ts`, `Component.ts`, `RefBindingManager.ts`, shipped in `public/js/pp-reactive-v2.js` | component-tag refs are reserved by the Python compiler, stamped with runtime-owned `pp-ref-owner`, captured in the parent's scope, and bound to the child's concrete root; do not author `data-pp-ref`, `pp-ref-owner`, or `pp-ref-forward`; callback refs may return synchronous cleanup functions that run on replacement or detach |
40
+ | Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `TemplateCompiler.ts`, `NestedBoundaryManager.ts`, `public/js/pp-reactive-v2.js` | authored provider tags are HTML-first and lowercase; `TemplateCompiler.transformContextProviderTags(...)` rewrites `*.provider` to runtime-owned `pp-context-provider`; ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
41
+ | Portals | `pp.portal(ref, target?)` | `public/js/pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
42
+ | Props into a component | `x-*` attributes, Python `get_attributes({...}, props)` + `{{ attributes }}` | `html_attrs.py`, `components_compiler.py`, `public/js/pp-reactive-v2.js` | `pp.props` is read from the **rendered root's attributes**, so a prop accepted in Python but not re-emitted by `get_attributes` is silently `undefined` — no server error, no console warning. Brace expressions (`volume="{vol}"`) keep their real type; literals arrive as strings (`volume="0"` makes `volume === 0` false); valueless attributes arrive as `true`; JS reserved words such as `class` are dropped. See [components.md](./components.md#every-prop-a-template-reads-must-be-forwarded-to-the-root) |
43
+ | Stable ids | `pp.id()` | `public/js/pp-reactive-v2.js` | derived from component id plus hook slot; stable across rerenders, unique per instance; use for `id`/`for` pairing and `aria-*` instead of counters |
44
+ | External stores | `pp.syncExternalStore(subscribe, getSnapshot)` | `public/js/pp-reactive-v2.js` | `subscribe` must be stable wrap in `pp.callback(..., [])`; runtime re-reads the snapshot right after subscribing to close the render-to-subscribe gap; cleanup runs on dispose |
45
+ | Imperative handles | `pp.imperativeHandle(ref, createHandle, deps?)` | `public/js/pp-reactive-v2.js` | parent passes its own `pp.ref(...)` down as a normal prop (object props pass by reference); do not author `pp-ref-forward`; handle publishes in the layout-effect phase and clears on unmount |
46
+ | Transitions | `pp.transition()` | `public/js/pp-reactive-v2.js` | returns `[isPending, startTransition]`; not concurrent rendering is still synchronous; handles sync and promise scopes; overlapping scopes stay pending until the last settles |
47
+ | Deferred values | `pp.deferredValue(value, initialValue?)` | `public/js/pp-reactive-v2.js` | lags one commit behind the source |
48
+ | Optimistic updates | `pp.optimistic(passthrough, reducer?)` | `public/js/pp-reactive-v2.js` | pending actions are dropped when `passthrough` changes, so a confirmed server value never double-counts the guess; without a reducer each action replaces the value |
49
+ | Error boundaries | `pp.errorBoundary()` | `public/js/pp-reactive-v2.js` | returns `[error, reset]`; render **and** effect/cleanup throws walk logical component ancestry to the nearest boundary; a boundary also catches its own throws; latches until `reset()`; caps at 5 captures per arming; event-handler errors are not routed use `try`/`catch` |
50
+ | Lists | `<template pp-for="item in items">` | `public/js/pp-reactive-v2.js` | `pp-for` belongs on `<template>`, accepts arrays and synchronous iterables, captures rendered row values for events/props, and uses plain `key`, not `pp-key` |
51
+ | Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `public/js/pp-reactive-v2.js` | first-party events belong in `on*` attributes; event scope exposes `event`, `e`, `$event`, `target`, `currentTarget`, and `el`; normal form submits should use `Object.fromEntries(new FormData(event.currentTarget).entries())` instead of per-input refs; avoid id-driven `querySelector`/`addEventListener` for normal UI |
52
+ | RPC | `pp.rpc(...)` in scripts, `@rpc()` in Python | `public/js/pp-reactive-v2.js`, `casp/rpc.py` | use `pp.rpc`, not legacy `pp.fetchFunction`; protected actions use `@rpc(require_auth=True)` |
53
+ | Upload progress | `pp.rpc(..., { onUploadProgress })` | `public/js/pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; the callback receives `{ loaded, total, percent }` and `percent` can be `null` when length is not computable; replace state from returned payload |
54
+ | Streaming | `pp.rpc(..., { onStream })` | `public/js/pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
55
+ | SPA navigation | `body[pp-spa="true"]`, links | `public/js/pp-reactive-v2.js`, `main.py` | same-origin eligible links intercept; root-layout mismatches hard reload |
56
+ | Scroll restoration | `pp-reset-scroll="true"` | `public/js/pp-reactive-v2.js` | push navigation resets window; mark only content panes that should reset |
57
+ | Tailwind merge | `{twMerge(...)}`, Python `merge_classes(...)` | `html_attrs.py`, `public/js/pp-reactive-v2.js` | Python emits frontend-ready expressions when Tailwind is enabled |
58
+ | Markup deferral | `<template pp-component>` / `<template pp-owner>` wrappers | render pipeline (`main.py`), `public/js/pp-reactive-v2.js` (`materializeTemplateComponentBoundaries`) | top-level roots ship inside an inert `<template>`; runtime materializes them on mount and SPA navigation before scanning. Browser never validates `<template>` contents, so `{...}` is safe in any attribute/position (SVG geometry, `src`/`href`, form `value`/date, table/select text) — no per-tag workarounds. See [pulsepoint.md](./pulsepoint.md) "Component markup is deferred inside an inert `<template>`" |
51
59
 
52
60
  ## AI Decision Rules
53
61
 
@@ -69,6 +77,10 @@ If an inspected browser DOM disagrees with authored template source, remember th
69
77
  - For ordinary form submits, bind `onsubmit` on the form and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; reserve `pp-ref` for imperative access rather than routine payload collection.
70
78
  - Use lowercase provider tags such as `<themecontext.provider>` and `pp.context(...)` for context.
71
79
  - Use `pp-for` only on `<template>` and plain `key` for keyed lists.
80
+ - Use `pp.id()` for any generated `id`, `for`, `aria-labelledby`, or `aria-describedby` value instead of a loop index or module-level counter.
81
+ - Use `pp.syncExternalStore(...)` for browser or global sources the component does not own (`matchMedia`, `localStorage`, `navigator.onLine`, a shared store) instead of `pp.state` plus `pp.effect`.
82
+ - Wrap a component subtree that can fail in a parent with `pp.errorBoundary()` rather than leaving a render throw to the console.
83
+ - In a Python component, list every prop the template's `{...}` expressions reference in `get_attributes({...}, props)`. If a binding "does nothing" — an icon toggle that never switches, a `hidden` that never applies — check that prop is on the rendered root before debugging the expression.
72
84
  - Prefer PulsePoint state and directives over manual `innerHTML` repainting.
73
85
  - Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
74
86
 
@@ -112,6 +124,40 @@ Upload progress:
112
124
  </section>
113
125
  ```
114
126
 
127
+ Error boundary with fallback and retry:
128
+
129
+ ```html
130
+ <section>
131
+ <script>
132
+ const [error, reset] = pp.errorBoundary();
133
+ </script>
134
+
135
+ <div hidden="{!error}">
136
+ <p>Could not load this panel: {error?.message}</p>
137
+ <button onclick="{reset()}">Retry</button>
138
+ </div>
139
+
140
+ <div hidden="{!!error}">
141
+ <x-revenue-chart />
142
+ </div>
143
+ </section>
144
+ ```
145
+
146
+ Accessible field pairing with stable ids:
147
+
148
+ ```html
149
+ <div>
150
+ <label for="{emailId}">Email</label>
151
+ <input id="{emailId}" type="email" aria-describedby="{hintId}" />
152
+ <p id="{hintId}">We never share this.</p>
153
+
154
+ <script>
155
+ const emailId = pp.id();
156
+ const hintId = pp.id();
157
+ </script>
158
+ </div>
159
+ ```
160
+
115
161
  Grouped shell scroll reset:
116
162
 
117
163
  ```html