caspian-utils 0.1.0 → 0.1.2

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.
@@ -55,14 +55,14 @@ Use prompts like these to check whether AI lands on the correct docs and files.
55
55
  | Create a protected dashboard section with child routes and a shared shell. | [index.md](./index.md), [routing.md](./routing.md), [auth.md](./auth.md), [project-structure.md](./project-structure.md) | `src/app/**`, `src/lib/auth/auth_config.py`, `main.py`, `.venv/Lib/site-packages/casp/layout.py` | section layout ownership, route privacy mode, and child-route wrapping |
56
56
  | Make a grouped shell keep sidebar scroll while resetting page content on child-route navigation. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [core-runtime-map.md](./core-runtime-map.md) | `src/app/**/layout.html`, `public/js/pp-reactive-v2.js`, `main.py` | `pp-reset-scroll` placement, push-vs-history scroll behavior, and shared-shell ownership |
57
57
  | Create a new contact page with interactive form behavior. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `src/app/**/index.html`, `main.py`, `.venv/Lib/site-packages/casp/components_compiler.py`, `.venv/Lib/site-packages/casp/scripts_type.py` | single-root template shape, script-inside-root authoring, and authored-vs-runtime boundaries |
58
- | Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, and avoiding id-driven `querySelector` or `addEventListener` wiring |
59
- | Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and Prisma-backed storage boundaries |
58
+ | Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, `Object.fromEntries(new FormData(...).entries())` for simple form submits, and avoiding id-driven `querySelector` or `addEventListener` wiring |
59
+ | Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and mandatory Prisma Python ORM use for database reads and writes when Prisma is enabled |
60
60
  | Explain why authored Caspian templates use a plain `<script>` instead of `type="text/pp"`. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [routing.md](./routing.md) | `main.py`, `.venv/Lib/site-packages/casp/scripts_type.py`, `.venv/Lib/site-packages/casp/components_compiler.py` | script rewriting, `pp-component` injection, and authored-vs-runtime boundaries |
61
61
  | Debug why `StateManager` does not persist across a full redirect. | [index.md](./index.md), [state.md](./state.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/state_manager.py` | wire vs non-wire reset behavior and `request.state.session` dependency |
62
62
  | Trace the auth cookie and CSRF cookie names used in development. | [index.md](./index.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/auth.py` | development cookie scoping and CSRF or session naming ownership |
63
63
  | Find how route params and query params reach `page()`. | [index.md](./index.md), [routing.md](./routing.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/layout.py` | path-dict delivery, query coercion, and `request` injection |
64
64
  | Decide whether a dashboard child route should use page caching. | [index.md](./index.md), [cache.md](./cache.md), [routing.md](./routing.md), [fetch-data.md](./fetch-data.md) | `main.py`, `.venv/Lib/site-packages/casp/cache_handler.py`, the route's `index.py` | route-level cache ownership, cacheable HTML scope, and invalidation points |
65
- | Add a PulsePoint context provider to a reusable component. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `public/js/pp-reactive-v2.js`, component `.html`, `.venv/Lib/site-packages/casp/components_compiler.py` | `Context.Provider` usage, logical ancestry, single-root template shape |
65
+ | Add a PulsePoint context provider to a reusable component. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `ts/TemplateCompiler.ts`, `public/js/pp-reactive-v2.js`, component `.html`, `.venv/Lib/site-packages/casp/components_compiler.py` | lowercase HTML-first `*.provider` usage, logical ancestry, single-root template shape |
66
66
  | Add upload progress to an existing file manager. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md) | `src/app/**/index.py`, `src/app/**/index.html`, `src/lib/**`, `public/js/pp-reactive-v2.js` | `pp.rpc` upload options, state replacement, persisted metadata, BrowserSync ignore rules |
67
67
  | Decide whether MCP files should be created. | [index.md](./index.md), [mcp.md](./mcp.md), [commands.md](./commands.md), [project-structure.md](./project-structure.md) | `caspian.config.json`, `settings/restart-mcp.ts`, `package.json`, `src/lib/mcp/**` when enabled | `mcp` feature gate, update workflow, nested FastMCP config ownership |
68
68
 
@@ -75,7 +75,9 @@ Treat the table as a prompt pack for spot checks, not as a full validation matri
75
75
  - AI edits framework internals when the task only requires app-owned route or helper changes.
76
76
  - AI treats runtime HTML examples as authored template examples.
77
77
  - AI generates a route or component template with a valid-looking root element but leaves a sibling top-level `<script>` or second top-level element after it.
78
- - AI starts first-party interactivity with ids, `data-*` attributes, `querySelector`, `addEventListener`, manual `fetch`, or manual `innerHTML` instead of PulsePoint `on*` attributes, state, directives, and `pp.rpc()`.
78
+ - AI starts first-party interactivity with ids, `data-*` attributes, `querySelector`, `addEventListener`, manual `fetch`, manual `innerHTML`, or per-input `pp-ref` payload collection instead of PulsePoint `on*` attributes, form submit events, state, directives, and `pp.rpc()`.
79
+ - AI bypasses the generated Prisma Python ORM in a Prisma-enabled project with direct database drivers, custom fetch helpers, JSON active stores, browser-side database fetches, or a second app-owned database abstraction.
80
+ - AI extracts one-route backend logic into `src/lib/**` even though the logic is not shared; route-specific first-render queries, RPC actions, validation, and response shaping belong in the owning `src/app/**/index.py`.
79
81
  - AI puts `pp-reset-scroll="true"` on the whole shell or `body` when only the page-content pane should reset, causing persistent sidebars or rails to lose their scroll position.
80
82
  - AI decides behavior from memory without checking the owning implementation details.
81
83
 
@@ -14,7 +14,9 @@ related:
14
14
 
15
15
  This page documents the Prisma workflow for Caspian projects where `caspian.config.json` enables Prisma.
16
16
 
17
- When a project enables Prisma, use `prisma/schema.prisma` for schema management, `prisma.config.ts` for Prisma config and seed wiring, and reuse an app-owned `src/lib/prisma/` package when the project includes one.
17
+ When a project enables Prisma, use `prisma/schema.prisma` for schema management, `prisma.config.ts` for Prisma config and seed wiring, and reuse an app-owned `src/lib/prisma/` package when the project includes one.
18
+
19
+ High-priority rule: when `caspian.config.json` has `prisma: true`, Python-side database access must use the generated Prisma Python ORM. Route `page()` functions, route-owned `@rpc()` actions, auth handlers, upload flows, and shared helpers should import the generated client from `src/lib/prisma/**` instead of inventing direct driver calls, hand-written SQL wrappers, JSON manifests as active stores, app-specific HTTP fetches, or browser-side database fetches. Use raw SQL only through Prisma as a narrow fallback when the generated ORM cannot express a query clearly.
18
20
 
19
21
  Treat `caspian.config.json` as the single source of truth for whether Prisma is enabled in a workspace. If `prisma` is false and the user wants Prisma, ask first, then update `caspian.config.json` and run `npx casp update project` before assuming Prisma-managed files exist.
20
22
 
@@ -29,7 +31,7 @@ The standard Prisma flow in Caspian is:
29
31
  5. Run `npx ppy generate` so the Python ORM classes stay aligned with the updated schema.
30
32
  6. Reuse the shared Python database layer in `src/lib/prisma/` when Python route or RPC code needs database access, and never hand-edit generated ORM classes.
31
33
 
32
- Use this workflow instead of writing raw SQL first. Drop to raw SQL only when a query cannot be expressed clearly with the generated client.
34
+ Use this workflow instead of writing raw SQL first. Drop to raw SQL only through Prisma when a query cannot be expressed clearly with the generated client.
33
35
 
34
36
  ## Environment Setup
35
37
 
@@ -157,7 +159,7 @@ If Python route or RPC code needs database access, import from `src.lib.prisma`
157
159
 
158
160
  ## Python Route Usage
159
161
 
160
- If the project ships an app-owned Python Prisma-style layer under `src/lib/prisma/`, treat it as async-first and reuse it from route and RPC code.
162
+ If the project ships an app-owned Python Prisma-style layer under `src/lib/prisma/`, treat it as async-first and reuse it from route and RPC code. In a Prisma-enabled project, this is the required data-access path for Python code.
161
163
 
162
164
  Example:
163
165
 
@@ -178,7 +180,7 @@ Prisma calls fit naturally in:
178
180
  - `async def page()` for first-render data
179
181
  - `@rpc()` actions for browser-triggered reads and writes
180
182
 
181
- Keep route-specific Prisma I/O in `page()` or `@rpc()` actions. The installed layout engine supports synchronous and async `layout()` results, but layout work should stay focused on shared subtree props or metadata.
183
+ Keep route-specific Prisma I/O in that route's `page()` or `@rpc()` actions in `src/app/**/index.py`. The installed layout engine supports synchronous and async `layout()` results, but layout work should stay focused on shared subtree props or metadata. Move Prisma helpers into `src/lib/**` only when the same behavior is reused by multiple routes, features, or integrations.
182
184
 
183
185
  See `fetch-data.md` for the recommended route-render versus RPC split.
184
186
 
@@ -325,12 +327,13 @@ users = await prisma.query_raw(
325
327
  )
326
328
  ```
327
329
 
328
- Use raw SQL sparingly. Prefer the generated Prisma API when the query can be expressed clearly there.
330
+ Use raw SQL sparingly and only through Prisma. Prefer the generated Prisma API when the query can be expressed clearly there.
329
331
 
330
332
  ## Recommended Project Rules
331
333
 
332
334
  - Keep the schema in `prisma/schema.prisma` and follow the required regeneration order after changes: `npx prisma migrate dev`, optional seed flow, then `npx ppy generate`.
333
- - Reuse `src/lib/prisma/` for Python-side database access instead of creating a second bridge.
335
+ - Reuse `src/lib/prisma/` for Python-side database access instead of creating a second bridge.
336
+ - When Prisma is enabled, do not bypass the Prisma Python ORM with direct database drivers, custom fetch functions, JSON files as active stores, or browser-side database access.
334
337
  - Never hand-edit generated Prisma or Python ORM classes.
335
338
  - Keep reusable database helpers in `src/lib/`, and keep route-specific orchestration in `src/app/`.
336
339
  - Use `await` with Prisma operations.
@@ -349,7 +352,8 @@ If an AI agent is working on a Caspian app with Prisma enabled, apply these rule
349
352
  - Run `npx ppy generate` after schema changes to refresh the Python ORM classes.
350
353
  - Never hand-edit generated Prisma or Python ORM classes.
351
354
  - Read `prisma.config.ts` and `prisma/seed.ts` when you need the current project's Prisma tooling examples.
352
- - Reuse the existing `src/lib/prisma/` package when the Python app needs database access.
355
+ - Reuse the existing `src/lib/prisma/` package when the Python app needs database access.
356
+ - Treat use of the Prisma Python ORM as mandatory for Python-side database reads and writes in Prisma-enabled projects.
353
357
  - For file managers and uploads, persist metadata in Prisma and keep blob storage separate. See [file-uploads.md](./file-uploads.md).
354
358
  - Put reusable database helpers in `src/lib/`; keep route and RPC orchestration in `src/app/`.
355
359
  - Use `async def page()` for route-specific first-render reads. Use `layout()` only for shared subtree props or metadata, and use `@rpc()` plus `pp.rpc()` for browser-triggered reads and writes.
@@ -23,7 +23,9 @@ This page explains how data fetching works in Caspian. Use route functions for i
23
23
 
24
24
  Treat RPC as the default way for browser code to talk to Python in Caspian. For CRUD operations and any browser-initiated backend reads after first render, default to `@rpc()` on the server and `pp.rpc()` in PulsePoint code. Do not reach for ad hoc fetch calls to custom JSON endpoints, alternate transport layers, or older helper names unless the task explicitly requires that shape.
25
25
 
26
- Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, or manual DOM repainting.
26
+ When `caspian.config.json` has `prisma: true`, the Python side of those route and RPC actions must use the generated Prisma Python ORM from `src/lib/prisma/**` for database reads and writes. The browser asks Python through `pp.rpc(...)`; Python asks the database through Prisma. Do not replace either side with a custom browser fetch, direct database driver, hand-written SQL helper, JSON active store, or second app-owned data-fetch abstraction.
27
+
28
+ Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. For normal form submissions, read named controls with `Object.fromEntries(new FormData(event.currentTarget).entries())` in the submit handler and pass that object directly to `pp.rpc(...)`; the route's Python `@rpc()` action should validate, normalize, and decide what to persist. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, manual DOM repainting, or per-input `pp-ref` payload collection.
27
29
 
28
30
  MCP is a separate integration surface. Do not place app-owned FastMCP tools in route `index.py` files or treat `@rpc()` actions as a replacement for MCP tools. Use `mcp.md` and `src/lib/mcp/` only when `caspian.config.json` has `mcp: true`. If `mcp` is false, do not assume those files exist.
29
31
 
@@ -49,7 +51,9 @@ When a page belongs to a grouped subtree such as a dashboard, account area, admi
49
51
  - Use `page()` for route-level data required before HTML renders, and use `layout()` only for shared subtree props or metadata.
50
52
  - When a route renders UI and also needs backend work, keep the HTML in the sibling `index.html`; `index.py` should prepare data and call `render_page(__file__, ...)`, not inline the route markup.
51
53
  - Use `@rpc()` on the server and `pp.rpc()` in PulsePoint code for all browser-triggered data work after first render, including CRUD operations and follow-up reads.
54
+ - When Prisma is enabled, route all Python database I/O inside `page()`, `layout()` when truly shared, `@rpc()` actions, and reusable helpers through the generated Prisma Python ORM.
52
55
  - Trigger those browser actions through PulsePoint event attributes in the HTML, not through a separate DOM listener layer.
56
+ - For simple form submits, prefer `onsubmit="{submitForm(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())` over `pp-ref` fields and effect-managed submit listeners.
53
57
  - Keep custom REST or other endpoint patterns as explicit exceptions, not the baseline Caspian approach.
54
58
 
55
59
  ## Initial Data In `index.py`
@@ -85,7 +89,7 @@ Notes:
85
89
  - Use a normal parent folder such as `dashboard/` when the section name should appear in the URL. Use a route-group folder such as `(reports)/` only when the shared wrapper should organize child routes without adding a URL segment.
86
90
  - Keep reusable database or API clients under `src/lib/`; keep route-specific orchestration in `src/app/`.
87
91
 
88
- If the data source is Prisma, confirm `caspian.config.json` has `prisma: true`, then see `database.md` for the project's schema, migration, and generation workflow. If the project includes an app-owned Python database layer under `src/lib/prisma/`, reuse it instead of creating another helper.
92
+ If the data source is Prisma, confirm `caspian.config.json` has `prisma: true`, then see `database.md` for the project's schema, migration, and generation workflow. If the project includes an app-owned Python database layer under `src/lib/prisma/`, reuse it instead of creating another helper. In Prisma-enabled projects, direct driver access and custom fetch helpers are exceptions to avoid, not alternate defaults.
89
93
 
90
94
  ## Interactive Data With RPC
91
95
 
@@ -107,14 +111,18 @@ from src.lib.prisma import prisma
107
111
  async def list_todos():
108
112
  return await prisma.todo.find_many()
109
113
 
110
- @rpc(require_auth=True, limits="20/minute")
111
- async def create_todo(title: str):
112
- return await prisma.todo.create(
113
- data={"title": title, "completed": False}
114
- )
114
+ @rpc(require_auth=True, limits="20/minute")
115
+ async def create_todo(title: str):
116
+ title = title.strip()
117
+ if not title:
118
+ raise ValueError("Title is required.")
119
+
120
+ return await prisma.todo.create(
121
+ data={"title": title, "completed": False}
122
+ )
115
123
  ```
116
124
 
117
- Call it from the client with `pp.rpc()`:
125
+ Call it from the client with `pp.rpc()`:
118
126
 
119
127
  ```html
120
128
  <script>
@@ -127,10 +135,39 @@ Call it from the client with `pp.rpc()`:
127
135
  const todo = await pp.rpc("create_todo", { title });
128
136
  console.log(todo);
129
137
  }
130
- </script>
131
- ```
132
-
133
- Use RPC for:
138
+ </script>
139
+ ```
140
+
141
+ Do not call `fetch("/api/todos")` from the browser for normal Caspian CRUD when the route can expose an `@rpc()` action. Do not use `fetch()` or a database driver in Python to work around Prisma when `src/lib/prisma/**` already provides the generated ORM client.
142
+
143
+ For form submissions, let the form event provide the submitted values. The inputs' `name` attributes define the RPC payload keys, and the Python action owns trimming, validation, coercion, and persistence decisions.
144
+
145
+ ```html
146
+ <form onsubmit="{createTodoFromForm(event)}" novalidate>
147
+ <input name="title" required />
148
+ <button type="submit" disabled="{isSaving}">Add</button>
149
+
150
+ <script>
151
+ const [isSaving, setIsSaving] = pp.state(false);
152
+
153
+ async function createTodoFromForm(event) {
154
+ event.preventDefault();
155
+
156
+ const data = Object.fromEntries(new FormData(event.currentTarget).entries());
157
+ if (isSaving) return;
158
+
159
+ setIsSaving(true);
160
+ try {
161
+ await pp.rpc("create_todo", data, { abortPrevious: true });
162
+ } finally {
163
+ setIsSaving(false);
164
+ }
165
+ }
166
+ </script>
167
+ </form>
168
+ ```
169
+
170
+ Use RPC for:
134
171
 
135
172
  - CRUD reads and writes after the initial render
136
173
  - Button-triggered refreshes
@@ -83,7 +83,7 @@ Use `index.html` by itself when the route is UI-only.
83
83
 
84
84
  ## `index.py`
85
85
 
86
- `index.py` is the backend companion for a route.
86
+ `index.py` is the backend companion for a route.
87
87
 
88
88
  Add it when the route needs:
89
89
 
@@ -92,9 +92,13 @@ Add it when the route needs:
92
92
  - auth checks or redirects
93
93
  - route-level `Cache(...)`
94
94
  - route-owned `@rpc()` actions
95
- - server-side context before rendering the sibling template
96
-
97
- For UI routes, `index.py` does not replace `index.html`. It prepares data and calls `render_page(__file__, ...)` so Caspian renders the sibling template.
95
+ - server-side context before rendering the sibling template
96
+
97
+ For UI routes, `index.py` does not replace `index.html`. It prepares data and calls `render_page(__file__, ...)` so Caspian renders the sibling template.
98
+
99
+ Keep logic here when it belongs only to this route. That includes route-specific first-render queries, `@rpc()` actions, auth checks, redirects, filters, validation, upload orchestration, and response shaping. Move logic to `src/lib/**` only when it is shared across routes, components, integrations, or features. Do not extract one-route logic into a library just because it is written in Python.
100
+
101
+ When `caspian.config.json` has `prisma: true`, route-specific database access in `index.py` should use the generated Prisma Python ORM from `src/lib/prisma/**`. Shared database helpers may live in `src/lib/**`, but they should still call the generated Prisma Python ORM rather than a separate fetch or driver layer.
98
102
 
99
103
  Example:
100
104
 
@@ -251,11 +255,12 @@ If rendering `error.html` fails, the runtime falls back to a minimal plain HTML
251
255
  Use this order when deciding where a concern belongs:
252
256
 
253
257
  1. Put visible route markup in `index.html`.
254
- 2. Add `index.py` only when the same route needs backend work.
255
- 3. Put shared subtree wrapper markup in `layout.html`.
256
- 4. Add `layout.py` only when that shared shell needs synchronous Python props or metadata.
257
- 5. Add `loading.html` when SPA navigation needs an immediate scoped loading state.
258
- 6. Use root `not-found.html` for unmatched URLs.
259
- 7. Use root `error.html` for unhandled exceptions.
258
+ 2. Add `index.py` only when the same route needs backend work.
259
+ 3. Keep route-specific logic in `index.py`; move logic into `src/lib/**` only when it is actually shared.
260
+ 4. Put shared subtree wrapper markup in `layout.html`.
261
+ 5. Add `layout.py` only when that shared shell needs synchronous Python props or metadata.
262
+ 6. Add `loading.html` when SPA navigation needs an immediate scoped loading state.
263
+ 7. Use root `not-found.html` for unmatched URLs.
264
+ 8. Use root `error.html` for unhandled exceptions.
260
265
 
261
266
  Use [routing.md](./routing.md) for the broader file-based routing model, [metadata.md](./metadata.md) for page and layout metadata, [cache.md](./cache.md) for route caching in `index.py`, and [pulsepoint.md](./pulsepoint.md) for the authored template contract and browser runtime behavior.
@@ -33,8 +33,9 @@ When generating or editing a Caspian app, treat these as the default choices unl
33
33
  - For first-party HTML events and reactivity, use PulsePoint `on*` attributes, state, refs, effects, directives, and `pp.rpc()` instead of ordinary DOM wiring with ids, `data-*` state, `querySelector`, `addEventListener`, or manual `innerHTML`.
34
34
  - Treat every authored route, layout, and component HTML file like a React component return value: exactly one top-level parent HTML element or one imported `x-*` root, with any owned plain `<script>` kept inside that same root.
35
35
  - When `caspian.config.json` has `tailwindcss: true`, use Python `merge_classes(...)` plus browser `twMerge(...)` as the only supported Tailwind class-merging path.
36
- - Use `@rpc()` plus `pp.rpc()` for browser-triggered reads, writes, streaming, and uploads.
37
- - Use `Validate` and `Rule` from `casp.validate` for server-side input validation and sanitization.
36
+ - Use `@rpc()` plus `pp.rpc()` for browser-triggered reads, writes, streaming, and uploads.
37
+ - When `caspian.config.json` has `prisma: true`, use the generated Prisma Python ORM from `src/lib/prisma/**` for Python-side database reads and writes. Do not invent a second fetch layer, raw driver wrapper, JSON active store, or browser-side data path that bypasses Prisma.
38
+ - Use `Validate` and `Rule` from `casp.validate` for server-side input validation and sanitization.
38
39
 
39
40
  ## AI Doc Shape
40
41
 
@@ -101,14 +102,15 @@ Preferred lookup order:
101
102
  2. Read `./caspian.config.json` before making any feature assumption. A doc existing in the package does not mean that feature is enabled in the current project.
102
103
  3. Before authoring or editing any `src/app/**` or component HTML template, apply the single-root invariant: one authored root only, any owned `<script>` inside that root, and no handwritten `pp-component` or `type="text/pp"`.
103
104
  4. Before inventing browser JavaScript, check whether the interaction is first-party UI behavior. If it is, use PulsePoint `on*` attributes, `pp.state`, refs, effects, directives, and `pp.rpc()` rather than id-driven DOM scripting.
104
- 5. Treat `caspian.config.json` as the single source of truth for optional features. Use feature-specific docs only when the matching flag is 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`.
105
- 6. If the task touches `main.py` or `.venv/Lib/site-packages/casp/**`, read `core-runtime-map.md` to jump to the controlling runtime file and the matching feature doc.
106
- 7. If the task names a PulsePoint feature or directive, read `pulsepoint-runtime-map.md` for the fastest feature-to-runtime lookup, then read `pulsepoint.md` for authoring rules.
107
- 8. After the feature is confirmed, inspect the actual project files that decide behavior, such as `package.json`, `main.py`, `src/app/**`, `src/lib/**`, `settings/**`, `prisma/**`, and the installed `casp` runtime.
108
- 9. Use `file-conventions.md` for quick decisions about `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, and `error.html`; use `commands.md` for scaffold and update workflows, `project-structure.md` for placement decisions, and the feature docs such as `mcp.md`, `database.md`, `auth.md`, `fetch-data.md`, and `file-uploads.md` for task-specific guidance.
109
- 10. Prefer packaged Caspian docs before upstream documentation when generating code, commands, or migration guidance.
110
- 11. Use `ai-validation-checklist.md` when you want to verify that the docs lead AI to the correct files and behavior checkpoints.
111
- 12. Keep `index.md` and cross-links aligned so AI can quickly discover the right doc.
105
+ 5. When Prisma is enabled, route all Python database work through the generated Prisma Python ORM. Use `database.md` before schema, migration, seed, or ORM decisions, and use `fetch-data.md` before wiring route data or RPC flows.
106
+ 6. Treat `caspian.config.json` as the single source of truth for optional features. Use feature-specific docs only when the matching flag is 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`.
107
+ 7. If the task touches `main.py` or `.venv/Lib/site-packages/casp/**`, read `core-runtime-map.md` to jump to the controlling runtime file and the matching feature doc.
108
+ 8. If the task names a PulsePoint feature or directive, read `pulsepoint-runtime-map.md` for the fastest feature-to-runtime lookup, then read `pulsepoint.md` for authoring rules.
109
+ 9. After the feature is confirmed, inspect the actual project files that decide behavior, such as `package.json`, `main.py`, `src/app/**`, `src/lib/**`, `settings/**`, `prisma/**`, and the installed `casp` runtime.
110
+ 10. Use `file-conventions.md` for quick decisions about `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, and `error.html`; use `commands.md` for scaffold and update workflows, `project-structure.md` for placement decisions, and the feature docs such as `mcp.md`, `database.md`, `auth.md`, `fetch-data.md`, and `file-uploads.md` for task-specific guidance.
111
+ 11. Prefer packaged Caspian docs before upstream documentation when generating code, commands, or migration guidance.
112
+ 12. Use `ai-validation-checklist.md` when you want to verify that the docs lead AI to the correct files and behavior checkpoints.
113
+ 13. Keep `index.md` and cross-links aligned so AI can quickly discover the right doc.
112
114
 
113
115
  ## Maintenance
114
116
 
@@ -104,9 +104,11 @@ This is the main application area. It contains route files, templates, styles, a
104
104
 
105
105
  ### `src/app/`
106
106
 
107
- This directory handles file-based routing. Route templates and route-specific backend logic live here.
108
-
107
+ This directory handles file-based routing. Route templates and route-specific backend logic live here.
108
+
109
109
  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.
110
+
111
+ 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
112
 
111
113
  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.
112
114
 
@@ -148,7 +150,7 @@ Do not add `src/lib/security/runtime_security.py` for normal app work. Runtime s
148
150
 
149
151
  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.
150
152
 
151
- 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.
153
+ 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.
152
154
 
153
155
  ### `src/lib/auth/`
154
156
 
@@ -323,7 +325,8 @@ If an AI agent is deciding where to make changes, use these rules first.
323
325
  - 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`.
324
326
  - 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.
325
327
  - 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`.
326
- - Put route templates and route-specific backend logic in `src/app/`.
328
+ - Put route templates and route-specific backend logic in `src/app/`.
329
+ - Put only genuinely shared helpers, services, adapters, and validation logic in `src/lib/`.
327
330
  - 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/`.
328
331
  - 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.
329
332
  - Read [file-uploads.md](./file-uploads.md) when the task involves upload widgets, media libraries, or file-manager flows.
@@ -37,10 +37,10 @@ If an inspected browser DOM disagrees with authored template source, remember th
37
37
  | State | `pp.state(initial)` | `pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
38
38
  | Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
39
39
  | Refs | `pp.ref(...)`, `pp-ref` | `pp-reactive-v2.js` | generated ref internals are runtime-managed; do not author `data-pp-ref` |
40
- | Context | `pp.createContext(...)`, `<Context.Provider>`, `pp.context(token)` | `pp-reactive-v2.js` | ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
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
41
  | Portals | `pp.portal(ref, target?)` | `pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
42
42
  | Lists | `<template pp-for="item in items">` | `pp-reactive-v2.js` | `pp-for` belongs on `<template>`, use 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`; avoid id-driven `querySelector`/`addEventListener` for normal UI |
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
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
45
  | Upload progress | `pp.rpc(..., { onUploadProgress })` | `pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; replace state from returned payload |
46
46
  | Streaming | `pp.rpc(..., { onStream })` | `pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
@@ -63,31 +63,32 @@ If an inspected browser DOM disagrees with authored template source, remember th
63
63
  - Author one root element or one imported `x-*` root per route, layout, or component template.
64
64
  - Keep any owned plain `<script>` inside that same root.
65
65
  - Do not handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
66
- - Use `pp.rpc(...)` for current browser-to-server calls.
67
- - Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
68
- - Use `Context.Provider` and `pp.context(...)` for context.
66
+ - Use `pp.rpc(...)` for current browser-to-server calls.
67
+ - Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
68
+ - 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.
69
+ - Use lowercase provider tags such as `<themecontext.provider>` and `pp.context(...)` for context.
69
70
  - Use `pp-for` only on `<template>` and plain `key` for keyed lists.
70
71
  - Prefer PulsePoint state and directives over manual `innerHTML` repainting.
71
72
  - Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
72
73
 
73
74
  ## Compact Examples
74
75
 
75
- Context provider:
76
-
77
- ```html
78
- <section>
79
- <script>
80
- const ThemeContext = pp.createContext("light");
76
+ Context provider:
77
+
78
+ ```html
79
+ <section>
80
+ <script>
81
+ const ThemeContext = pp.createContext("light");
81
82
  const [theme, setTheme] = pp.state("dark");
82
83
  </script>
83
84
 
84
- <ThemeContext.Provider value="{theme}">
85
- <button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
86
- Theme: {theme}
87
- </button>
88
- </ThemeContext.Provider>
89
- </section>
90
- ```
85
+ <themecontext.provider value="{theme}">
86
+ <button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
87
+ Theme: {theme}
88
+ </button>
89
+ </themecontext.provider>
90
+ </section>
91
+ ```
91
92
 
92
93
  Upload progress:
93
94
 
@@ -55,6 +55,7 @@ When a Caspian page needs reactive browser behavior, use PulsePoint.
55
55
  - Use PulsePoint component roots, scripts, directives, and runtime helpers for interactive UI.
56
56
  - Use PulsePoint state, effects, refs, and template directives as the default reactivity model in authored Caspian templates.
57
57
  - Bind first-party events in the HTML with PulsePoint-handled native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit`.
58
+ - For ordinary forms, bind the submit event in the `<form>` and collect named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`. Let input `name` attributes define the payload keys, then validate and normalize those values in Python. Do not use `pp.ref(...)` on every input or an effect-managed listener just to read submitted values.
58
59
  - When the browser needs CRUD operations or follow-up reads from the backend, call `pp.rpc()` from PulsePoint code and back it with route or backend `@rpc()` actions.
59
60
  - Keep server-rendered HTML plus PulsePoint enhancement as the baseline architecture.
60
61
  - For dashboards, admin areas, account sections, docs sections, and other grouped subtrees, keep shared shell markup and shared PulsePoint behavior in the parent folder's `layout.html`, then keep child-route PulsePoint state local to each `index.html`. Follow the same mental model as the Next.js App Router.
@@ -69,9 +70,10 @@ Default to this workflow:
69
70
  - Put the button, form, input, toggle, menu, filter, upload control, or list markup directly in the route, layout, or component HTML template.
70
71
  - Bind events with native `on*` attributes handled by PulsePoint, for example `onclick="save()"`, `oninput="setQuery(event.target.value)"`, or `onsubmit="{submitForm(event)}"`.
71
72
  - This is the PulsePoint `onClick`/native event-attribute model. Authored examples use lowercase HTML spellings such as `onclick` because browser HTML normalizes attribute names, but the important rule is to bind the event in the template instead of wiring it later with DOM selectors.
73
+ - For simple form submissions, let HTML own the form shape and let the submit event carry the values: use `const data = Object.fromEntries(new FormData(event.currentTarget).entries())` inside the handler, then pass that object directly to `pp.rpc(...)`. In a form-level submit handler, `event.target` is usually the form too, but `event.currentTarget` is the copy-safe default because it always means the element that owns the handler.
72
74
  - Keep reactive values in `pp.state(...)`.
73
75
  - Render conditional text, classes, attributes, lists, and styles with template expressions, `pp-for`, `pp-style`, `pp-spread`, and other PulsePoint-supported template features.
74
- - Use `pp.ref(...)` and `pp-ref` when a real element reference is needed.
76
+ - Use `pp.ref(...)` and `pp-ref` when a real imperative element reference is needed, such as focus, measurement, media, canvas, third-party widgets, or resetting a specific file/password input after a successful action.
75
77
  - Use `pp.effect(...)` or `pp.layoutEffect(...)` for lifecycle work that must happen after render.
76
78
  - Use `pp.rpc(...)` for browser-triggered backend reads and writes.
77
79
 
@@ -79,7 +81,9 @@ Avoid building a parallel JavaScript layer for normal UI behavior:
79
81
 
80
82
  - Do not add ids only so a script can find elements with `document.querySelector(...)` or `document.getElementById(...)`.
81
83
  - Do not use `data-*` attributes as a private client state system when PulsePoint state or props should own the data.
84
+ - Do not create click-in buttons by placing intent in `data-*` attributes and then scanning for those attributes. Put the action directly on the element with `onclick`, `oninput`, `onchange`, `onsubmit`, or another native `on*` event attribute.
82
85
  - Do not bind normal first-party clicks, input changes, submits, filters, menus, or toggles with `addEventListener(...)`.
86
+ - Do not use form refs plus input refs plus `pp.effect(...)` solely to construct an RPC payload from normal submitted fields.
83
87
  - Do not repaint first-party lists or panels with manual `innerHTML` writes when `pp.state(...)` plus `pp-for` can express the same UI.
84
88
  - Do not create a custom client-side store, event bus, or hydration routine for behavior that belongs in a PulsePoint component script.
85
89
 
@@ -134,6 +138,155 @@ Avoid this first-party pattern:
134
138
  ```
135
139
 
136
140
  That second shape recreates a separate event and rendering system inside a Caspian component. It is harder to maintain because it bypasses PulsePoint's rerender, event rebinding, refs, cleanup, and backend RPC conventions.
141
+
142
+ Preferred form submit pattern:
143
+
144
+ ```html
145
+ <section>
146
+ <form onsubmit="{saveProfile(event)}" class="grid gap-4" novalidate>
147
+ <input name="name" value="{{ user_name }}" autocomplete="name" required />
148
+ <input name="email" type="email" value="{{ user_email }}" autocomplete="email" required />
149
+ <input name="password" type="password" autocomplete="new-password" />
150
+
151
+ <button type="submit" disabled="{isSubmitting}">
152
+ {isSubmitting ? "Saving..." : "Save changes"}
153
+ </button>
154
+ </form>
155
+
156
+ <script>
157
+ const [isSubmitting, setIsSubmitting] = pp.state(false);
158
+
159
+ async function saveProfile(event) {
160
+ event.preventDefault();
161
+ if (isSubmitting) return;
162
+
163
+ const data = Object.fromEntries(new FormData(event.currentTarget).entries());
164
+
165
+ setIsSubmitting(true);
166
+ try {
167
+ await pp.rpc("save_profile", data, { abortPrevious: true });
168
+ } finally {
169
+ setIsSubmitting(false);
170
+ }
171
+ }
172
+ </script>
173
+ </section>
174
+ ```
175
+
176
+ Avoid this for a normal form:
177
+
178
+ ```html
179
+ <section>
180
+ <form pp-ref="{formRef}">
181
+ <input pp-ref="{nameRef}" name="name" />
182
+ <input pp-ref="{emailRef}" name="email" />
183
+ </form>
184
+
185
+ <script>
186
+ const formRef = pp.ref(null);
187
+ const nameRef = pp.ref(null);
188
+ const emailRef = pp.ref(null);
189
+
190
+ pp.effect(() => {
191
+ const form = formRef.current;
192
+ if (!form) return;
193
+
194
+ const submit = (event) => {
195
+ event.preventDefault();
196
+ return pp.rpc("save_profile", {
197
+ name: nameRef.current?.value ?? "",
198
+ email: emailRef.current?.value ?? "",
199
+ });
200
+ };
201
+
202
+ form.addEventListener("submit", submit);
203
+ return () => form.removeEventListener("submit", submit);
204
+ }, []);
205
+ </script>
206
+ </section>
207
+ ```
208
+
209
+ Refs are still useful when the feature actually requires imperative access. They are not the first choice for reading standard form fields that the browser already exposes through the submit event. Keep client code minimal and put reviewable data normalization in the route's Python `@rpc()` action unless the client must transform values for UX before submitting.
210
+
211
+ Basic two-way state pattern:
212
+
213
+ ```html
214
+ <section>
215
+ <label>
216
+ Name
217
+ <input name="name" value="{name}" oninput="setName(event.target.value)" />
218
+ </label>
219
+
220
+ <p>Hello, {name || "friend"}.</p>
221
+ <button onclick="setName('')">Clear</button>
222
+
223
+ <script>
224
+ const [name, setName] = pp.state("");
225
+ </script>
226
+ </section>
227
+ ```
228
+
229
+ Basic RPC-backed form pattern:
230
+
231
+ ```html
232
+ <section>
233
+ <form onsubmit="{saveContact(event)}" novalidate>
234
+ <input name="name" value="{name}" oninput="setName(event.target.value)" required />
235
+ <input name="email" type="email" required />
236
+
237
+ <button type="submit" disabled="{isSaving}">
238
+ {isSaving ? "Saving..." : "Save"}
239
+ </button>
240
+ </form>
241
+
242
+ <script>
243
+ const [name, setName] = pp.state("");
244
+ const [isSaving, setIsSaving] = pp.state(false);
245
+
246
+ async function saveContact(event) {
247
+ event.preventDefault();
248
+ if (isSaving) return;
249
+
250
+ const data = Object.fromEntries(new FormData(event.currentTarget).entries());
251
+
252
+ setIsSaving(true);
253
+ try {
254
+ await pp.rpc("save_contact", data, { abortPrevious: true });
255
+ } finally {
256
+ setIsSaving(false);
257
+ }
258
+ }
259
+ </script>
260
+ </section>
261
+ ```
262
+
263
+ Back that form with a route-owned `@rpc()` action in the sibling `index.py`. If Prisma is enabled, the action should use the generated Prisma Python ORM for persistence.
264
+
265
+ ```python
266
+ from casp.rpc import rpc
267
+ from casp.validate import Validate
268
+ from src.lib.prisma import prisma
269
+
270
+
271
+ @rpc()
272
+ async def save_contact(name: str, email: str):
273
+ name = name.strip()
274
+ email = email.strip().lower()
275
+
276
+ if not name:
277
+ raise ValueError("Name is required.")
278
+ if Validate.with_rules(email, "required|email") is not True:
279
+ raise ValueError("A valid email address is required.")
280
+
281
+ contact = await prisma.contact.create(
282
+ data={
283
+ "name": name,
284
+ "email": email,
285
+ }
286
+ )
287
+
288
+ return contact.to_dict()
289
+ ```
137
290
 
138
291
  ## Authoring Model
139
292
 
@@ -296,12 +449,12 @@ Notes:
296
449
 
297
450
  ## Context
298
451
 
299
- Context is implemented in the current runtime, but the current API follows a React-style `Context.Provider` pattern rather than a legacy `pp.provideContext(...)` helper.
452
+ Context is implemented in the current runtime with a React-style provider pattern rather than a legacy `pp.provideContext(...)` helper. Because Caspian templates are HTML-first, authored provider tags should be written in lowercase HTML form, for example `<themecontext.provider>`, even when the JavaScript token is named `ThemeContext`.
300
453
 
301
454
  How it works:
302
455
 
303
456
  - Create a token with `pp.createContext(defaultValue)`.
304
- - Provide a value with `Context.Provider`.
457
+ - Provide a value with a lowercase provider tag such as `<themecontext.provider value="{theme}">`.
305
458
  - Read it in a descendant component with `pp.context(token)`.
306
459
  - Resolution walks the logical component parent chain stored in the registry, not the live DOM.
307
460
  - Portaled descendants still resolve providers through component ancestry.
@@ -315,23 +468,24 @@ Important:
315
468
  - Context is component-level, not directive-based. There is no `pp-context` attribute.
316
469
  - `pp.context(token)` resolves from ancestors. A component does not consume the value it provides in the same render.
317
470
  - If provider and consumer live in different component script scopes, pass the token through props or store it in shared outer or global state.
318
- - The preferred authoring style is a provider tag in the template. The runtime also supports imperative `ThemeContext.Provider({ value })` calls during render, but the tag form is clearer for AI-generated authored templates.
319
- - Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
471
+ - The preferred authoring style is a lowercase provider tag in the template. The TypeScript runtime's `TemplateCompiler.transformContextProviderTags(...)` recognizes `*.provider` tags case-insensitively and rewrites them to runtime-owned `<pp-context-provider>` boundaries. The runtime context lookup is also case-insensitive, so `<themecontext.provider>` can resolve a script binding named `ThemeContext`.
472
+ - The runtime also supports imperative `ThemeContext.Provider({ value })` calls during render, but that form provides from the component boundary rather than documenting the HTML subtree shape. Use it only when component-wide provision is intended.
473
+ - Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
320
474
 
321
475
  Provider example:
322
476
 
323
477
  ```html
324
478
  <section>
325
- <script>
326
- const ThemeContext = pp.createContext("light");
327
- const [theme] = pp.state("dark");
328
- </script>
329
-
330
- <ThemeContext.Provider value="{theme}">
331
- <p>This subtree receives the provided theme.</p>
332
- </ThemeContext.Provider>
333
- </section>
334
- ```
479
+ <script>
480
+ const ThemeContext = pp.createContext("light");
481
+ const [theme] = pp.state("dark");
482
+ </script>
483
+
484
+ <themecontext.provider value="{theme}">
485
+ <p>This subtree receives the provided theme.</p>
486
+ </themecontext.provider>
487
+ </section>
488
+ ```
335
489
 
336
490
  Consumer example for a child component that receives the token through props:
337
491
 
@@ -401,9 +555,10 @@ Example:
401
555
  </div>
402
556
  ```
403
557
 
404
- ## Refs
405
-
406
- - Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
558
+ ## Refs
559
+
560
+ - Refs are for imperative element access. Do not use `pp-ref` as the default way to read normal form input values on submit; prefer the form's `onsubmit` event plus `Object.fromEntries(new FormData(event.currentTarget).entries())`.
561
+ - Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
407
562
  - Use `pp-ref="{registerRef(id)}"` when you want the compiler to capture a dynamic ref expression.
408
563
  - `pp.ref(null)` is the normal way to create a ref object.
409
564
  - Callback refs and `{ current }` refs are both supported.
@@ -469,10 +624,13 @@ Example:
469
624
  - Treat this as the HTML form of `onClick`-style PulsePoint event binding. Prefer lowercase examples in authored HTML because the browser normalizes attribute names.
470
625
  - Event values may be raw code or wrapped in `{...}`.
471
626
  - The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
627
+ - For form submit handlers, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` when collecting named fields for `pp.rpc(...)`.
628
+ - `event.target` can also work for form-level submit handlers, but examples use `event.currentTarget` so nested event origins do not change which element becomes the `FormData` source.
472
629
  - Do not use hyphenated event attrs like `on-click`.
473
630
  - Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
474
631
  - Owned template/event-owner internals are runtime-managed. Do not author them directly.
475
632
  - Do not replace normal PulsePoint event attributes with id-driven `querySelector(...)` plus `addEventListener(...)` wiring. If an imperative listener is unavoidable for an integration, attach and clean it up from `pp.effect(...)`.
633
+ - Do not replace a normal `onclick` with a `data-action`, `data-target`, or similar attribute plus a script that scans the DOM. PulsePoint event attributes are the event contract for first-party Caspian UI.
476
634
 
477
635
  ## SPA, loading, and navigation helpers
478
636
 
@@ -528,6 +686,7 @@ Use these rules when generating or editing PulsePoint runtime code:
528
686
 
529
687
  - Treat PulsePoint as the default reactive frontend for Caspian app code.
530
688
  - For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
689
+ - For simple forms, bind `onsubmit` in the authored HTML and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; do not generate per-input refs or effect-managed submit listeners just to gather values.
531
690
  - Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
532
691
  - Use `public/js/pp-reactive-v2.js` as the shipped runtime contract AI should follow.
533
692
  - Keep `main.py` in view because it injects the runtime-facing attributes and rewrites authored scripts before the browser sees them.
@@ -537,10 +696,11 @@ Use these rules when generating or editing PulsePoint runtime code:
537
696
  - For grouped shells with independent shell and content scrolling, put `pp-reset-scroll="true"` on the content pane rather than the whole shell when only the page content should reset between child-route navigations.
538
697
  - Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
539
698
  - Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
699
+ - Avoid data-attribute click wiring such as `data-action="save"` plus a delegated listener. Use `onclick="save()"` or `onsubmit="{save(event)}"` in authored HTML and keep reactive state in `pp.state(...)`.
540
700
  - If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
541
701
  - In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
542
702
  - Keep template-facing variables at top level.
543
- - Follow the React-style context pattern: `pp.createContext(...)`, `<Context.Provider value="{...}">`, and `pp.context(token)`.
703
+ - Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
544
704
  - Do not invent `pp.provideContext`, `pp-context`, or other legacy context helpers.
545
705
  - Keep `pp-for` on `<template>` and use plain `key`.
546
706
  - Use native `on*` attributes, not framework-specific event syntax.
@@ -581,7 +741,7 @@ These are current runtime caveats that matter for authors and AI tools:
581
741
  - The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
582
742
  - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
583
743
  - `pp.context()` resolves through ancestor components, not the current component's own pending providers.
584
- - `pp.provideContext` is not part of the current runtime API. Use `Context.Provider`.
744
+ - `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
585
745
  - `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
586
746
 
587
747
  ## Final reminder
@@ -29,8 +29,9 @@ Start with these rules:
29
29
  - Put application routes in `src/app/`.
30
30
  - For any route that renders a page, put the markup in `index.html`.
31
31
  - If a route is UI-only, `index.html` by itself is enough.
32
- - Add `index.py` only when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side logic.
33
- - Use a standalone `index.py` only for non-visual routes such as redirects or action-only handlers.
32
+ - Add `index.py` only when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side logic.
33
+ - Keep route-specific server logic in that route's `index.py`. Move logic into `src/lib/**` only when it is shared across routes, components, integrations, or features.
34
+ - Use a standalone `index.py` only for non-visual routes such as redirects or action-only handlers.
34
35
  - When a folder owns child routes, use `layout.html` to wrap them. This is the default pattern for dashboards, admin sections, account areas, settings trees, and route groups.
35
36
  - In grouped shells with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content pane in `layout.html` when that pane should reset on child-route navigation while the shell sidebar or rail keeps its own scroll.
36
37
  - Use `layout.py` when a layout needs shared props or metadata before rendering. The `layout()` function may be synchronous or async.
@@ -184,7 +185,7 @@ Place those import comments at the top of the file, above the authored root elem
184
185
 
185
186
  Route templates follow the same authored-vs-runtime contract documented in [pulsepoint.md](./pulsepoint.md) and the same single-root discipline documented in [components.md](./components.md): keep one authored parent node, keep any `<!-- @import ... -->` directives above that root, use a plain `<script>` inside that root when needed, and do not handwrite `pp-component` or `type="text/pp"`. That root may be a native HTML element or a single imported `x-*` component tag, but after expansion it must resolve to one final HTML root.
186
187
 
187
- When a route needs button clicks, form submits, input changes, filters, tabs, menus, uploads, polling, or reactive list updates, author those interactions as PulsePoint behavior in `index.html`. Use `onclick`, `oninput`, `onchange`, `onsubmit`, `pp.state(...)`, `pp-for`, refs, effects, and `pp.rpc(...)` instead of starting with `id` attributes plus `document.querySelector(...)` or `addEventListener(...)`.
188
+ When a route needs button clicks, form submits, input changes, filters, tabs, menus, uploads, polling, or reactive list updates, author those interactions as PulsePoint behavior in `index.html`. Use `onclick`, `oninput`, `onchange`, `onsubmit`, `pp.state(...)`, `pp-for`, refs, effects, and `pp.rpc(...)` instead of starting with `id` attributes plus `document.querySelector(...)` or `addEventListener(...)`. For simple form submits, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` over per-input `pp-ref` payload collection.
188
189
 
189
190
  For AI-generated route templates, treat `src/app/**/index.html` the same way you would a React component body: return one parent node that contains the entire route markup. This includes any owned script.
190
191
 
@@ -235,7 +236,9 @@ Use [pulsepoint.md](./pulsepoint.md) when you need the full authored-vs-rendered
235
236
 
236
237
  ### `index.py`
237
238
 
238
- Use `index.py` as the backend companion when a route needs metadata or async server-side logic. For routes that render UI, keep the markup in the sibling `index.html` and let `page()` call `render_page(__file__, ...)`. Do not inline route HTML inside `index.py`.
239
+ Use `index.py` as the backend companion when a route needs metadata or async server-side logic. For routes that render UI, keep the markup in the sibling `index.html` and let `page()` call `render_page(__file__, ...)`. Do not inline route HTML inside `index.py`.
240
+
241
+ If the logic belongs only to this route, keep it here. Examples include this page's first-render query, route-owned RPC actions, redirect decisions, route-specific validation, route-specific filters, and route-specific response shaping. Move code into `src/lib/**` when the same logic is reused or intentionally shared; do not extract one-route orchestration into a library just because it is Python code.
239
242
 
240
243
  Use `index.py` by itself only for non-visual routes such as redirects or action-only handlers. Because Caspian runs on FastAPI, the page entry should be async when it performs async work.
241
244
 
@@ -288,7 +291,7 @@ The key name is arbitrary, but it must match exactly between the dict returned f
288
291
 
289
292
  Use distinct names for those layout props. In the current router, the second dict is merged into the full layout context after path params and `request`, so a key such as `slug` or `request` can shadow an existing value.
290
293
 
291
- When a route owns a file manager or upload UI, keep the owning upload and delete `@rpc()` actions in that same `index.py` and move reusable filesystem or Prisma helpers into `src/lib/`. Do not move ordinary upload behavior into `main.py`. See [file-uploads.md](./file-uploads.md).
294
+ When a route owns a file manager or upload UI, keep the owning upload and delete `@rpc()` actions in that same `index.py` and move reusable filesystem or Prisma helpers into `src/lib/`. Do not move ordinary upload behavior into `main.py`. See [file-uploads.md](./file-uploads.md). When Prisma is enabled, database reads and writes in this route logic or its shared helpers should use the generated Prisma Python ORM.
292
295
 
293
296
  For static and dynamic metadata rules, inheritance order, and social card fields, see [metadata.md](./metadata.md).
294
297
 
@@ -505,7 +508,8 @@ If an AI agent is choosing where to add or update route code, apply these rules
505
508
  - Treat `src/app/` as the routing source of truth.
506
509
  - Use folder names to model URL segments.
507
510
  - If a route renders UI, create or update `index.html` for the markup.
508
- - Add `index.py` only when the same route needs metadata or server behavior; do not place route HTML in `index.py`.
511
+ - Add `index.py` only when the same route needs metadata or server behavior; do not place route HTML in `index.py`.
512
+ - Keep route-specific backend logic in the route's own `index.py`; extract to `src/lib/**` only for genuinely shared logic.
509
513
  - Keep visible page markup in `index.html` and shared subtree shells in `layout.html`; do not place route HTML in `index.py` or layout HTML in `layout.py`.
510
514
  - Use PulsePoint as the first-party interaction model for route and layout HTML. Avoid custom DOM wiring for normal events and reactivity.
511
515
  - When the user asks for a dashboard, admin area, account section, or any grouped subtree of child routes, create a parent folder with `layout.html` and place the child routes beneath it. Follow the same mental model as the Next.js App Router.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {