caspian-utils 0.1.0 → 0.1.1

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 |
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
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 |
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,7 @@ 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
79
  - 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
80
  - AI decides behavior from memory without checking the owning implementation details.
81
81
 
@@ -23,7 +23,7 @@ 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
+ 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
27
 
28
28
  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
29
 
@@ -50,6 +50,7 @@ When a page belongs to a grouped subtree such as a dashboard, account area, admi
50
50
  - 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
51
  - 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.
52
52
  - Trigger those browser actions through PulsePoint event attributes in the HTML, not through a separate DOM listener layer.
53
+ - 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
54
  - Keep custom REST or other endpoint patterns as explicit exceptions, not the baseline Caspian approach.
54
55
 
55
56
  ## Initial Data In `index.py`
@@ -107,14 +108,18 @@ from src.lib.prisma import prisma
107
108
  async def list_todos():
108
109
  return await prisma.todo.find_many()
109
110
 
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
- )
111
+ @rpc(require_auth=True, limits="20/minute")
112
+ async def create_todo(title: str):
113
+ title = title.strip()
114
+ if not title:
115
+ raise ValueError("Title is required.")
116
+
117
+ return await prisma.todo.create(
118
+ data={"title": title, "completed": False}
119
+ )
115
120
  ```
116
121
 
117
- Call it from the client with `pp.rpc()`:
122
+ Call it from the client with `pp.rpc()`:
118
123
 
119
124
  ```html
120
125
  <script>
@@ -127,10 +132,37 @@ Call it from the client with `pp.rpc()`:
127
132
  const todo = await pp.rpc("create_todo", { title });
128
133
  console.log(todo);
129
134
  }
130
- </script>
131
- ```
132
-
133
- Use RPC for:
135
+ </script>
136
+ ```
137
+
138
+ 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.
139
+
140
+ ```html
141
+ <form onsubmit="{createTodoFromForm(event)}" novalidate>
142
+ <input name="title" required />
143
+ <button type="submit" disabled="{isSaving}">Add</button>
144
+
145
+ <script>
146
+ const [isSaving, setIsSaving] = pp.state(false);
147
+
148
+ async function createTodoFromForm(event) {
149
+ event.preventDefault();
150
+
151
+ const data = Object.fromEntries(new FormData(event.currentTarget).entries());
152
+ if (isSaving) return;
153
+
154
+ setIsSaving(true);
155
+ try {
156
+ await pp.rpc("create_todo", data, { abortPrevious: true });
157
+ } finally {
158
+ setIsSaving(false);
159
+ }
160
+ }
161
+ </script>
162
+ </form>
163
+ ```
164
+
165
+ Use RPC for:
134
166
 
135
167
  - CRUD reads and writes after the initial render
136
168
  - Button-triggered refreshes
@@ -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
 
@@ -80,6 +82,7 @@ Avoid building a parallel JavaScript layer for normal UI behavior:
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.
82
84
  - Do not bind normal first-party clicks, input changes, submits, filters, menus, or toggles with `addEventListener(...)`.
85
+ - Do not use form refs plus input refs plus `pp.effect(...)` solely to construct an RPC payload from normal submitted fields.
83
86
  - Do not repaint first-party lists or panels with manual `innerHTML` writes when `pp.state(...)` plus `pp-for` can express the same UI.
84
87
  - Do not create a custom client-side store, event bus, or hydration routine for behavior that belongs in a PulsePoint component script.
85
88
 
@@ -134,6 +137,75 @@ Avoid this first-party pattern:
134
137
  ```
135
138
 
136
139
  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.
140
+
141
+ Preferred form submit pattern:
142
+
143
+ ```html
144
+ <section>
145
+ <form onsubmit="{saveProfile(event)}" class="grid gap-4" novalidate>
146
+ <input name="name" value="{{ user_name }}" autocomplete="name" required />
147
+ <input name="email" type="email" value="{{ user_email }}" autocomplete="email" required />
148
+ <input name="password" type="password" autocomplete="new-password" />
149
+
150
+ <button type="submit" disabled="{isSubmitting}">
151
+ {isSubmitting ? "Saving..." : "Save changes"}
152
+ </button>
153
+ </form>
154
+
155
+ <script>
156
+ const [isSubmitting, setIsSubmitting] = pp.state(false);
157
+
158
+ async function saveProfile(event) {
159
+ event.preventDefault();
160
+ if (isSubmitting) return;
161
+
162
+ const data = Object.fromEntries(new FormData(event.currentTarget).entries());
163
+
164
+ setIsSubmitting(true);
165
+ try {
166
+ await pp.rpc("save_profile", data, { abortPrevious: true });
167
+ } finally {
168
+ setIsSubmitting(false);
169
+ }
170
+ }
171
+ </script>
172
+ </section>
173
+ ```
174
+
175
+ Avoid this for a normal form:
176
+
177
+ ```html
178
+ <section>
179
+ <form pp-ref="{formRef}">
180
+ <input pp-ref="{nameRef}" name="name" />
181
+ <input pp-ref="{emailRef}" name="email" />
182
+ </form>
183
+
184
+ <script>
185
+ const formRef = pp.ref(null);
186
+ const nameRef = pp.ref(null);
187
+ const emailRef = pp.ref(null);
188
+
189
+ pp.effect(() => {
190
+ const form = formRef.current;
191
+ if (!form) return;
192
+
193
+ const submit = (event) => {
194
+ event.preventDefault();
195
+ return pp.rpc("save_profile", {
196
+ name: nameRef.current?.value ?? "",
197
+ email: emailRef.current?.value ?? "",
198
+ });
199
+ };
200
+
201
+ form.addEventListener("submit", submit);
202
+ return () => form.removeEventListener("submit", submit);
203
+ }, []);
204
+ </script>
205
+ </section>
206
+ ```
207
+
208
+ 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.
137
209
 
138
210
  ## Authoring Model
139
211
 
@@ -296,12 +368,12 @@ Notes:
296
368
 
297
369
  ## Context
298
370
 
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.
371
+ 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
372
 
301
373
  How it works:
302
374
 
303
375
  - Create a token with `pp.createContext(defaultValue)`.
304
- - Provide a value with `Context.Provider`.
376
+ - Provide a value with a lowercase provider tag such as `<themecontext.provider value="{theme}">`.
305
377
  - Read it in a descendant component with `pp.context(token)`.
306
378
  - Resolution walks the logical component parent chain stored in the registry, not the live DOM.
307
379
  - Portaled descendants still resolve providers through component ancestry.
@@ -315,23 +387,24 @@ Important:
315
387
  - Context is component-level, not directive-based. There is no `pp-context` attribute.
316
388
  - `pp.context(token)` resolves from ancestors. A component does not consume the value it provides in the same render.
317
389
  - 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.
390
+ - 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`.
391
+ - 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.
392
+ - Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
320
393
 
321
394
  Provider example:
322
395
 
323
396
  ```html
324
397
  <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
- ```
398
+ <script>
399
+ const ThemeContext = pp.createContext("light");
400
+ const [theme] = pp.state("dark");
401
+ </script>
402
+
403
+ <themecontext.provider value="{theme}">
404
+ <p>This subtree receives the provided theme.</p>
405
+ </themecontext.provider>
406
+ </section>
407
+ ```
335
408
 
336
409
  Consumer example for a child component that receives the token through props:
337
410
 
@@ -401,9 +474,10 @@ Example:
401
474
  </div>
402
475
  ```
403
476
 
404
- ## Refs
405
-
406
- - Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
477
+ ## Refs
478
+
479
+ - 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())`.
480
+ - Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
407
481
  - Use `pp-ref="{registerRef(id)}"` when you want the compiler to capture a dynamic ref expression.
408
482
  - `pp.ref(null)` is the normal way to create a ref object.
409
483
  - Callback refs and `{ current }` refs are both supported.
@@ -469,6 +543,8 @@ Example:
469
543
  - 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
544
  - Event values may be raw code or wrapped in `{...}`.
471
545
  - The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
546
+ - For form submit handlers, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` when collecting named fields for `pp.rpc(...)`.
547
+ - `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
548
  - Do not use hyphenated event attrs like `on-click`.
473
549
  - Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
474
550
  - Owned template/event-owner internals are runtime-managed. Do not author them directly.
@@ -528,6 +604,7 @@ Use these rules when generating or editing PulsePoint runtime code:
528
604
 
529
605
  - Treat PulsePoint as the default reactive frontend for Caspian app code.
530
606
  - For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
607
+ - 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
608
  - Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
532
609
  - Use `public/js/pp-reactive-v2.js` as the shipped runtime contract AI should follow.
533
610
  - Keep `main.py` in view because it injects the runtime-facing attributes and rewrites authored scripts before the browser sees them.
@@ -540,7 +617,7 @@ Use these rules when generating or editing PulsePoint runtime code:
540
617
  - If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
541
618
  - In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
542
619
  - Keep template-facing variables at top level.
543
- - Follow the React-style context pattern: `pp.createContext(...)`, `<Context.Provider value="{...}">`, and `pp.context(token)`.
620
+ - Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
544
621
  - Do not invent `pp.provideContext`, `pp-context`, or other legacy context helpers.
545
622
  - Keep `pp-for` on `<template>` and use plain `key`.
546
623
  - Use native `on*` attributes, not framework-specific event syntax.
@@ -581,7 +658,7 @@ These are current runtime caveats that matter for authors and AI tools:
581
658
  - The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
582
659
  - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
583
660
  - `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`.
661
+ - `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
585
662
  - `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
586
663
 
587
664
  ## Final reminder
@@ -184,7 +184,7 @@ Place those import comments at the top of the file, above the authored root elem
184
184
 
185
185
  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
186
 
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(...)`.
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(...)`. For simple form submits, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` over per-input `pp-ref` payload collection.
188
188
 
189
189
  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
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {