caspian-utils 0.2.5 → 0.2.7

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.
@@ -54,13 +54,13 @@ Use prompts like these to check whether AI lands on the correct docs and files.
54
54
  | --- | --- | --- | --- |
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
- | 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 |
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`, `public/js/pp-reactive-v2.js` | single-root template shape, plain-script-inside-root authoring, and safe template materialization |
58
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
  | A debounced server search pauses the first key typed after the timer fires. Determine whether the component or runtime owns the problem. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [fetch-data.md](./fetch-data.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | Owning `src/app/**` or `src/components/**`; `public/js/pp-reactive-v2.js` only when profiling points to runtime reconciliation | State versus ref ownership, debounce frequency versus render cost, stale-response generations, the smallest useful render owner, and `pp.enablePerf()` evidence before a runtime edit |
60
60
  | 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 |
61
61
  | Add `public/icons/app.png` and make it browser-accessible without editing routes. | [index.md](./index.md), [project-structure.md](./project-structure.md), [core-runtime-map.md](./core-runtime-map.md), [auth.md](./auth.md) | `public/**`, `main.py`, `.venv/Lib/site-packages/casp/runtime_security.py` | root-relative URL mapping, no per-directory route or prefix registry, traversal and symlink containment, `GET`/`HEAD` only, and middleware order |
62
62
  | Store untrusted uploads in a custom public directory. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [project-structure.md](./project-structure.md), [core-runtime-map.md](./core-runtime-map.md) | `src/app/**/index.py`, `src/lib/**`, `main.py`, `.venv/Lib/site-packages/casp/runtime_security.py` | top-level `inline_safe_subdirectories` registration, attachment mode for HTML/SVG/unknown types, `nosniff`, path containment, and BrowserSync ignore rules |
63
- | 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 |
63
+ | Explain how a plain component `<script>` stays inert during template materialization. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [routing.md](./routing.md) | `main.py`, `.venv/Lib/site-packages/casp/components_compiler.py`, `public/js/pp-reactive-v2.js` | component-root deferral, script-source capture, native-execution prevention, and component-scope evaluation |
64
64
  | 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 |
65
65
  | 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 |
66
66
  | 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 |
@@ -258,7 +258,7 @@ def UserCard(user, **props):
258
258
  """, attrs=attrs, user=user)
259
259
  ```
260
260
 
261
- The returned value is `Markup`, so the normal pipeline still injects `pp-component` on the single root and `transform_scripts(...)` rewrites the plain `<script>` to `type="text/pp"`. A single-file component renders identically to the two-file `render_html(...)` form; the choice is purely about readability.
261
+ The returned value is `Markup`, so the normal pipeline still injects `pp-component` on the single root and defers that root inside an inert template. The plain component `<script>` remains plain; PulsePoint captures its source before materialization and evaluates it in component scope. A single-file component renders identically to the two-file `render_html(...)` form; the choice is purely about readability.
262
262
 
263
263
  ### Receiving Props In A Python Component
264
264
 
@@ -608,16 +608,16 @@ For ref purposes, a component's root is the single native element that receives
608
608
 
609
609
  The default takes precedence over catch-all `**props`: `pp-ref` is reserved for root ref forwarding and is not delivered in that dictionary. A component that intentionally wants `pp-ref` as ordinary data can opt out by declaring an explicit camel-case `ppRef` parameter, for example `def Inspector(ppRef="", **props)`. That explicit declaration consumes the prop, disables automatic root forwarding for that component invocation, and makes the component responsible for how the value is used. Use this escape hatch sparingly; ordinary UI primitives should keep the standard root-ref contract.
610
610
 
611
- ## Auto-Injected `pp-component` And PulsePoint Script Type
611
+ ## Auto-Injected `pp-component` And Plain Component Scripts
612
612
 
613
- Treat `pp-component="componentName"` and `type="text/pp"` as framework output, not authored source. The canonical authored-vs-runtime explanation lives in [pulsepoint.md](./pulsepoint.md).
613
+ Treat `pp-component="componentName"` as framework output, not authored source. The canonical authored-vs-runtime explanation lives in [pulsepoint.md](./pulsepoint.md).
614
614
 
615
615
  - Do not manually add `pp-component="..."` to route templates, layout templates, or component HTML files.
616
- - Do not manually add `type="text/pp"` to authored PulsePoint scripts.
617
- - The Python render pipeline injects `pp-component` onto the single root element during render.
618
- - `main.py` runs `transform_scripts(...)`, which rewrites authored body `<script>` tags to `<script type="text/pp">` before the browser runtime mounts.
619
- - Add a plain `<script>` only when the route or component actually needs PulsePoint logic, and keep it inside that single root element.
620
- - If a runtime example already shows `pp-component` and `type="text/pp"`, treat it as rendered output rather than authored source.
616
+ - The Python render pipeline injects `pp-component` onto the single root element during render.
617
+ - Add a plain, untyped `<script>` only when the route or component actually needs PulsePoint logic, and keep it inside that single root element.
618
+ - `main.py` preserves that plain script while deferring the root inside an inert template.
619
+ - Before materialization or morph insertion, PulsePoint captures and empties the script so the browser cannot execute it natively, then evaluates the captured source in component scope.
620
+ - If a runtime example shows `pp-component`, treat that attribute as rendered output rather than authored source.
621
621
 
622
622
  ## Single-Root Rule
623
623
 
@@ -754,5 +754,5 @@ Keep synchronous components as the default. Switch to `async def` only when the
754
754
 
755
755
  - Read [project-structure.md](./project-structure.md) for where reusable components should live.
756
756
  - Read [routing.md](./routing.md) when importing components into route templates.
757
- - Read [pulsepoint.md](./pulsepoint.md) for the runtime contract of `pp-component`, `script[type="text/pp"]`, props, and browser-side reactivity.
757
+ - Read [pulsepoint.md](./pulsepoint.md) for the runtime contract of `pp-component`, plain owned scripts, props, and browser-side reactivity.
758
758
  - Read [fetch-data.md](./fetch-data.md) when a component needs browser-triggered `pp.rpc()` calls or component-owned server actions.
@@ -37,7 +37,7 @@ Important current `main.py` behaviors AI should keep in mind:
37
37
 
38
38
  - In development, `_scoped_cookie_name(...)` appends the active BrowserSync or dev port to both the session cookie name and the CSRF cookie name. The scope is resolved from `CASPIAN_BROWSER_SYNC_PORT`, then `settings/bs-config.json`, then `PORT`.
39
39
  - `register_single_route(...)` passes path params to `page()` as one positional dict, injects matching query params by name, and injects `request` by keyword when declared.
40
- - The render pipeline is `transform_components(...)`, then `render_with_nested_layouts(...)`, then `transform_scripts(...)`.
40
+ - The render pipeline transforms components, renders nested layouts, and finally defers outermost component roots inside inert templates.
41
41
  - Route-level generators returned from `page()` are wrapped in `SSE(...)` before the response is sent.
42
42
  - Safe public-file helpers live in `casp.runtime_security`. `PublicFilesMiddleware` maps every existing nested file under `public/**` to the same root-relative URL without per-directory routes, rejects traversal and symlink escape, handles only `GET`/`HEAD`, and falls through when no file exists so page and mounted-app routing still works. User-upload directories must retain their restricted inline-media policy.
43
43
  - Session middleware secrets may be resolved through `casp.runtime_security` so production can fail fast when `AUTH_SECRET` is missing or still on a default placeholder.
@@ -100,7 +100,6 @@ Interactive CRUD page:
100
100
  | [.venv/Lib/site-packages/casp/validate.py](../../../../.venv/Lib/site-packages/casp/validate.py) | `Validate`, `Rule`, sanitization, and file-validation internals | [validation.md](./validation.md) |
101
101
  | [.venv/Lib/site-packages/casp/component_decorator.py](../../../../.venv/Lib/site-packages/casp/component_decorator.py) | `@component`, `render_html(...)`, the inline `html(...)` single-file helper (with direct-call scope capture), component loading, and prop normalization before component calls | [components.md](./components.md) |
102
102
  | [.venv/Lib/site-packages/casp/components_compiler.py](../../../../.venv/Lib/site-packages/casp/components_compiler.py) | `@import` parsing, `x-*` component resolution (including resolving a component's own tags from its Python module imports and the scope captured by directly-called components), parent-scope expansion of slot content, single-root validation, and `pp-component` injection | [components.md](./components.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md) |
103
- | [.venv/Lib/site-packages/casp/scripts_type.py](../../../../.venv/Lib/site-packages/casp/scripts_type.py) | rewriting authored body `<script>` tags to `type="text/pp"` in rendered HTML | [pulsepoint.md](./pulsepoint.md), [routing.md](./routing.md), [components.md](./components.md) |
104
103
  | [.venv/Lib/site-packages/casp/html_attrs.py](../../../../.venv/Lib/site-packages/casp/html_attrs.py) | `get_attributes(...)`, alias normalization, and the Python-side `merge_classes(...)` contract | [components.md](./components.md) |
105
104
  | [.venv/Lib/site-packages/casp/caspian_config.py](../../../../.venv/Lib/site-packages/casp/caspian_config.py) | typed config loading, feature-flag reads, `settings/files-list.json` parsing, and route rule derivation | [project-structure.md](./project-structure.md), [commands.md](./commands.md), [routing.md](./routing.md) |
106
105
 
@@ -118,7 +117,7 @@ Use these behavior checkpoints when AI needs the fastest verification path for a
118
117
  | `casp.auth` | auth settings, signin and signout flow, provider wiring, and page protection behavior |
119
118
  | `casp.rpc` and streamed RPC responses | middleware interception, CSRF and session expectations, registry behavior, and helper-level RPC contracts |
120
119
  | `casp.layout` | layout discovery, metadata merge, root handling, and layout rendering rules |
121
- | `casp.components_compiler`, `casp.component_decorator`, `casp.scripts_type`, and template-root injection | `@import` parsing, `x-*` expansion, Python-module-import tag resolution, parent-scope slot resolution, inline `html(...)` rendering and children-safe handling, deterministic root keys, `pp-component` injection, and authored-script rewriting to `type="text/pp"` |
120
+ | `casp.components_compiler`, `casp.component_decorator`, `main.py` template-root deferral, and the browser runtime | `@import` parsing, `x-*` expansion, Python-module-import tag resolution, parent-scope slot resolution, inline `html(...)` rendering and children-safe handling, deterministic root keys, `pp-component` injection, plain component-script preservation, and safe script capture during materialization |
122
121
  | `casp.state_manager` | wire vs non-wire reset behavior, request-state persistence assumptions, and AttributeDict access |
123
122
  | `casp.cache_handler` | filename generation, manifest writes, TTL handling, and invalidation behavior |
124
123
  | `casp.caspian_config` | config parsing, files index building, and Next.js-style route inventory behavior |
@@ -44,7 +44,7 @@ For authored route, layout, loading, not-found, and error HTML files, keep exact
44
44
 
45
45
  - Keep any `<!-- @import ... -->` directives above that root. They are header directives for the file, not children of the root element.
46
46
  - Keep any owned plain `<script>` inside that root, not after it.
47
- - Do not handwrite `pp-component` or `type="text/pp"`.
47
+ - Do not handwrite `pp-component`; keep owned component logic in a plain `<script>` inside the single root.
48
48
 
49
49
  This is a runtime requirement, not a style preference.
50
50
 
@@ -109,7 +109,7 @@ Preferred lookup order:
109
109
 
110
110
  1. Read `node_modules/caspian-utils/dist/docs/index.md` to discover available local docs.
111
111
  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.
112
- 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"`.
112
+ 3. Before authoring or editing any `src/app/**` or component HTML template, apply the single-root invariant: one authored root only, any owned plain `<script>` inside that root, and no handwritten `pp-component`.
113
113
  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.
114
114
  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.
115
115
  6. If the task asks for WebSockets, live bidirectional channels, socket origin checks, or native browser `WebSocket` clients, confirm `caspian.config.json` has `websocket: true`, read `websockets.md`, then inspect `main.py`, `src/lib/**`, the owning `src/app/**` route, and `settings/bs-config.json`. If `websocket` is false and the user wants it, ask first, then enable the config flag and follow the update workflow.
@@ -117,7 +117,7 @@ When a folder represents a section with child routes, such as `dashboard`, `acco
117
117
 
118
118
  If the shared wrapper should not add a URL segment, use a parenthesized route-group folder such as `(marketing)/layout.html` instead of a normal folder name. Use a normal folder such as `dashboard/` only when that segment should be part of the public URL.
119
119
 
120
- When authoring route or layout HTML, follow the authoring contract documented in [routing.md](./routing.md) and [pulsepoint.md](./pulsepoint.md): keep one authored root, keep `<!-- @import ... -->` directives above that root, use a plain `<script>` inside the root when needed, and do not handwrite `pp-component` or `type="text/pp"`.
120
+ When authoring route or layout HTML, follow the authoring contract documented in [routing.md](./routing.md) and [pulsepoint.md](./pulsepoint.md): keep one authored root, keep `<!-- @import ... -->` directives above that root, use a plain `<script>` inside the root when needed, and do not handwrite `pp-component`.
121
121
 
122
122
  See `routing.md` for the full App Router-style rules for dynamic segments, route groups, and nested layouts.
123
123
 
@@ -133,7 +133,7 @@ The common Caspian pattern is a Python file such as `Button.py` with `@component
133
133
 
134
134
  One Python file can also export multiple related `@component` functions. When that happens, import those tags from that exact file path in HTML, for example `<!-- @import { Breadcrumb, BreadcrumbItem, BreadcrumbList } from "../components/Breadcrumb.py" -->`, and render them as `<x-breadcrumb />`, `<x-breadcrumb-item />`, and `<x-breadcrumb-list />` instead of assuming each tag has its own sibling `.py` file.
135
135
 
136
- For component HTML files, follow the component authoring rules in [components.md](./components.md): one top-level parent node, no sibling roots, plain `<script>` inside that root when needed, and no handwritten `pp-component` or `type="text/pp"`.
136
+ For component HTML files, follow the component authoring rules in [components.md](./components.md): one top-level parent node, no sibling roots, a plain `<script>` inside that root when needed, and no handwritten `pp-component`.
137
137
 
138
138
  The directories listed in `componentScanDirs` determine where component tooling scans. When that list includes `src/`, `src/components/` is a conventionally clean location, not a hard-coded runtime requirement.
139
139
 
@@ -332,7 +332,7 @@ Notable internal files include:
332
332
  - `.venv/Lib/site-packages/casp/state_manager.py` for request-scoped server state and persistence caveats
333
333
  - `.venv/Lib/site-packages/casp/component_decorator.py` for `@component`, `render_html(...)`, and component loading
334
334
  - `.venv/Lib/site-packages/casp/components_compiler.py` for `@import` parsing, `x-*` resolution, root validation, and `pp-component` injection
335
- - `.venv/Lib/site-packages/casp/scripts_type.py` for converting authored `<script>` tags to `type="text/pp"`
335
+ - `main.py` for final inert-template deferral and `public/js/pp-reactive-v2.js` for safe plain-script capture and component-scope evaluation
336
336
  - `.venv/Lib/site-packages/casp/caspian_config.py` for config loading and route inventory parsing
337
337
  - `.venv/Lib/site-packages/casp/streaming.py` for `SSE` and streamed response helpers
338
338
 
@@ -361,7 +361,7 @@ If an AI agent is deciding where to make changes, use these rules first.
361
361
  - When deciding between `src/components/` and `src/lib/`, use `src/components/` for anything rendered as reusable UI and `src/lib/` for helpers, services, validators, adapters, and shared business logic.
362
362
  - Use [mcp.md](./mcp.md) only when `caspian.config.json` enables MCP and the task involves FastMCP tool definitions, nested config discovery, or local MCP commands.
363
363
  - Keep `<!-- @import ... -->` comments above the single authored root element in route, layout, and component HTML files. Do not nest them inside `<html>`, `<body>`, `<section>`, or any other parent wrapper.
364
- - Follow the single-root authoring contract in [routing.md](./routing.md), [components.md](./components.md), and [pulsepoint.md](./pulsepoint.md): one authored root, any owned `<script>` inside that root, and no handwritten `pp-component` or `type="text/pp"` in source templates.
364
+ - Follow the single-root authoring contract in [routing.md](./routing.md), [components.md](./components.md), and [pulsepoint.md](./pulsepoint.md): one authored root, any owned plain `<script>` inside that root, and no handwritten `pp-component` in source templates.
365
365
  - Put shared helpers and reusable libraries in `src/lib/`.
366
366
  - Use `settings/bs-config.ts` when uploaded public assets should not trigger BrowserSync reloads during the local stack.
367
367
  - Put app-owned FastMCP code in `src/lib/mcp/` only when `caspian.config.json` enables MCP.
@@ -20,10 +20,9 @@ Use [pulsepoint.md](./pulsepoint.md) for the full authoring contract. Use this f
20
20
  ## Source Of Truth
21
21
 
22
22
  - `public/js/pp-reactive-v2.js` is the shipped browser runtime.
23
- - `main.py` owns the final render pipeline that calls `transform_components(...)` and `transform_scripts(...)`.
24
- - `.venv/Lib/site-packages/casp/components_compiler.py` injects `pp-component` after component expansion and validates the single-root contract.
25
- - `.venv/Lib/site-packages/casp/scripts_type.py` rewrites authored body `<script>` tags to `type="text/pp"`.
26
- - `.venv/Lib/site-packages/casp/html_attrs.py` owns Python-side class and attribute helpers such as `merge_classes(...)`.
23
+ - `main.py` owns component transformation and final inert-template deferral.
24
+ - `.venv/Lib/site-packages/casp/components_compiler.py` injects `pp-component` after component expansion and validates the single-root contract.
25
+ - `.venv/Lib/site-packages/casp/html_attrs.py` owns Python-side class and attribute helpers such as `merge_classes(...)`.
27
26
 
28
27
  If an inspected browser DOM disagrees with authored template source, remember that the runtime DOM includes framework-managed output. Do not copy runtime-only attributes back into authored templates.
29
28
 
@@ -32,7 +31,7 @@ If an inspected browser DOM disagrees with authored template source, remember th
32
31
  | PulsePoint feature | Authoring surface | Runtime owner | Verify before changing |
33
32
  | --- | --- | --- | --- |
34
33
  | Component roots | `src/app/**/index.html`, `layout.html`, component `.html` files | `components_compiler.py`, `public/js/pp-reactive-v2.js` | one authored root, final expanded root receives one `pp-component`, no sibling scripts |
35
- | Component scripts | plain `<script>` inside the authored root | `scripts_type.py`, `public/js/pp-reactive-v2.js` | authored scripts are plain, runtime scripts become `type="text/pp"`, one owned script per root |
34
+ | Component scripts | plain, untyped `<script>` inside the authored root | `main.py`, `public/js/pp-reactive-v2.js` | the server preserves the plain script inside an inert component template; before materialization or morph insertion, PulsePoint captures and empties its source to prevent native execution, then evaluates it in component scope; one owned script per root |
36
35
  | Template expressions | text and attributes with `{...}` | `public/js/pp-reactive-v2.js` | top-level script bindings are exported, nested bindings are not; every destructuring shape is supported at the top level (array, object, nested, defaults, rest, holes), so any tuple-returning hook reaches template scope |
37
36
  | State | `pp.state(initial)` | `public/js/pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
38
37
  | Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `public/js/pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
@@ -76,7 +75,7 @@ If an inspected browser DOM disagrees with authored template source, remember th
76
75
  - **Templates are plain HTML, not JSX.** PulsePoint borrows React's *hook API* and *component decomposition*, never its markup syntax. Quote every brace attribute (`class="{…}"`, never `class={…}` — the unquoted form is invalid HTML and blanks the page with no console error), use `hidden="{…}"` instead of `{cond && <div/>}`, and `<template pp-for="…">` instead of `{list.map(...)}`. See [pulsepoint.md](./pulsepoint.md) "PulsePoint Is Not JSX".
77
76
  - Author one root element or one imported `x-*` root per route, layout, or component template.
78
77
  - Keep any owned plain `<script>` inside that same root.
79
- - Do not handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
78
+ - Do not handwrite `pp-component`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
80
79
  - Use `pp.rpc(...)` for current browser-to-server calls.
81
80
  - Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
82
81
  - 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.
@@ -182,7 +181,7 @@ Grouped shell scroll reset:
182
181
  Use these prompts after docs or runtime changes to confirm AI can route correctly:
183
182
 
184
183
  - "Create an interactive filter in a Caspian route template."
185
- - "Explain why authored scripts are plain `<script>` but browser output shows `type=\"text/pp\"`."
184
+ - "Explain how a plain component `<script>` stays inert while its deferred root is materialized."
186
185
  - "Debug a dashboard sidebar losing scroll during child-route navigation."
187
186
  - "Add an upload widget with progress and a reactive file list."
188
187
  - "Use context to share theme state between parent and child components."
@@ -31,7 +31,7 @@ Do not assume React, Vue, Svelte, JSX, Alpine, HTMX, or older PulsePoint docs un
31
31
  Apply these before generating any template, even without reading the rest of this page:
32
32
 
33
33
  1. One authored top-level root per route, layout, or component template; the owned plain `<script>` lives inside that root, never as a sibling.
34
- 2. Never handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, or other runtime-managed attributes; the render pipeline injects them.
34
+ 2. Never handwrite `pp-component`, `data-pp-ref`, or other runtime-managed attributes; the render pipeline injects them. Keep owned component logic in a plain, untyped `<script>` inside the root.
35
35
  3. Bind first-party events with native `on*` attributes in the HTML; never wire normal UI with ids, `querySelector`, `addEventListener`, or manual `innerHTML`.
36
36
  4. For ordinary form submits, use `onsubmit="{handler(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())`; refs are for imperative access only.
37
37
  5. Keep reactive values in `pp.state(...)`; keep template-facing bindings at the top level of the script.
@@ -108,7 +108,7 @@ There is **no** `pp-if`, `pp-show`, `pp-else`, `pp-model`, `pp-bind`, `pp-class`
108
108
 
109
109
  The render pipeline or the browser runtime writes these. Handwriting them corrupts instance tracking:
110
110
 
111
- `pp-component`, `type="text/pp"`, `pp-owner`, `pp-event-owner`, `pp-ref-owner`, `pp-ref-forward`, `pp-context-provider` (the generated tag), `pp-context-token`, `pp-context-value`, `pp-root-layout`, `pp-fragment-root`, `data-pp-ref`, `data-pp-input-value`, `data-pp-default-value`, `data-pp-checked-value`, `data-pp-default-checked`, `data-pp-select-value`, `pp-dynamic-script`, `pp-dynamic-meta`, `pp-dynamic-link`.
111
+ `pp-component`, `pp-owner`, `pp-event-owner`, `pp-ref-owner`, `pp-ref-forward`, `pp-context-provider` (the generated tag), `pp-context-token`, `pp-context-value`, `pp-root-layout`, `pp-fragment-root`, `data-pp-ref`, `data-pp-input-value`, `data-pp-default-value`, `data-pp-checked-value`, `data-pp-default-checked`, `data-pp-select-value`, `pp-dynamic-script`, `pp-dynamic-meta`, `pp-dynamic-link`.
112
112
 
113
113
  `pp-ref-forward` appears in the author-facing table only because you will see it in rendered HTML and in composition components; the server component compiler sets it on a root whose own root is another `x-*` component. Do not write it yourself.
114
114
 
@@ -131,15 +131,15 @@ Inside an `on*` attribute the runtime provides `event`, plus the aliases `e`, `$
131
131
  When documenting or generating PulsePoint code, follow this order:
132
132
 
133
133
  - `public/js/pp-reactive-v2.js` is the shipped browser runtime contract AI should follow.
134
- - `main.py` is the render-pipeline source of truth for how Caspian injects runtime attributes and rewrites scripts before the browser sees the HTML.
134
+ - `main.py` is the render-pipeline source of truth for component transformation, runtime-attribute injection, and final inert-template deferral.
135
135
  - If you are working inside PulsePoint or Caspian runtime development code and there is an authoring source tree behind the shipped files, use it only as an implementation detail. Do not assume that source tree exists in generated apps or shipped framework output.
136
136
 
137
137
  Important current facts:
138
138
 
139
139
  - `public/js/pp-reactive-v2.js` exposes the global `pp` runtime and auto-mounts on DOM ready.
140
- - `main.py` renders the final HTML, runs `transform_components(...)`, then runs `transform_scripts(...)` before returning the response.
141
- - `.venv/Lib/site-packages/casp/components_compiler.py` injects `pp-component` on the final resolved root after component expansion.
142
- - `.venv/Lib/site-packages/casp/scripts_type.py` rewrites authored body `<script>` tags to `type="text/pp"`.
140
+ - `main.py` renders the final HTML, transforms components, and defers outermost component roots inside inert templates before returning the response.
141
+ - `.venv/Lib/site-packages/casp/components_compiler.py` injects `pp-component` on the final resolved root after component expansion.
142
+ - `public/js/pp-reactive-v2.js` captures and empties plain component scripts before materialization or morph insertion, then evaluates their source in component scope.
143
143
  - Authored route and component templates compose reusable server components as HTML-first `x-*` tags before the browser runtime mounts.
144
144
 
145
145
  If docs, generated examples, or older notes disagree with `public/js/pp-reactive-v2.js` plus `main.py`, follow the code that actually runs.
@@ -359,7 +359,7 @@ Treat this section as the canonical authored-vs-runtime contract for Caspian tem
359
359
  PulsePoint authoring is split into two layers:
360
360
 
361
361
  - The authored layer: route, layout, and component templates under `src/` with plain HTML plus a plain `<script>`.
362
- - The runtime layer: the browser sees `pp-component` roots and `script[type="text/pp"]` after Caspian transforms the HTML.
362
+ - The runtime layer: the browser sees `pp-component` roots with plain owned scripts whose source PulsePoint captures before the roots become live.
363
363
 
364
364
  For authored Caspian templates:
365
365
 
@@ -367,7 +367,6 @@ For authored Caspian templates:
367
367
  - In source, that parent may be a native HTML element or a single imported `x-*` component tag, but after component expansion it must resolve to one final HTML root.
368
368
  - Put the component logic inside a plain `<script>` inside that same root.
369
369
  - Do not handwrite `pp-component="..."`.
370
- - Do not handwrite `type="text/pp"`.
371
370
 
372
371
  Treat that single-root rule as a hard invariant for AI-generated templates. A sibling `<script>` after the root or any second top-level element will break Caspian's `pp-component` injection and fail the render.
373
372
 
@@ -398,7 +397,7 @@ Authored example:
398
397
  </section>
399
398
  ```
400
399
 
401
- When that template reaches the browser, Caspian will already have injected the component id and rewritten the owned script to `type="text/pp"`. Those runtime attributes are for the rendered output, not for authored source examples.
400
+ When that template reaches the browser, Caspian has injected the component id and deferred the root inside an inert template. PulsePoint captures the plain script source before materialization so native execution cannot occur, then evaluates that source in component scope.
402
401
 
403
402
  Invalid authored shape:
404
403
 
@@ -429,7 +428,8 @@ Important:
429
428
  ## Component roots and scripts
430
429
 
431
430
  - Each runtime component root should have at most one owned script.
432
- - At runtime, the owned script is `script[type="text/pp"]`.
431
+ - The owned script remains a plain, untyped `<script>`.
432
+ - Before a deferred root or later morph becomes live, PulsePoint captures and empties that script; the captured source is evaluated only through the component runtime.
433
433
  - The script lookup walks the current root and skips nested `pp-component` boundaries, so a parent does not consume a child component's script.
434
434
  - If multiple matching runtime scripts exist in the same root, the first matching owned script wins. Generate one script per root.
435
435
  - Authored route, layout, and component templates still need one top-level parent node so Caspian can inject the component boundary correctly after component expansion.
@@ -476,7 +476,7 @@ Example:
476
476
  </div>
477
477
  ```
478
478
 
479
- That is the authored form. In browser-inspected runtime HTML, Caspian will already have added the component id and the owned script type.
479
+ That is the authored form. In browser-inspected runtime HTML, Caspian will already have added the component id; the owned script remains plain and is removed after PulsePoint captures its source.
480
480
 
481
481
  ## Hooks and runtime API
482
482
 
@@ -909,7 +909,7 @@ Each setter that changes a value requests a render, even when the eventual UI ch
909
909
 
910
910
  A render rebuilds the whole component as one HTML string and reconciles it against the live DOM. For a list, that used to mean changing one row cost the same as rebuilding every row: the entire list was re-serialized, re-parsed and re-walked.
911
911
 
912
- The runtime now remembers the markup each keyed row produced. A row that renders byte-identically again is not re-parsed and not re-diffed — its live node is kept, along with its attributes, text and bound handlers. Reconciliation work scales with the number of rows that actually changed.
912
+ The runtime now remembers the markup each keyed row produced. A row that renders byte-identically again is not re-parsed and not re-diffed — its live node is kept, along with its attributes, text and bound handlers. Runs of consecutive unchanged rows collapse together, so a list where a handful of rows changed costs about what those rows cost, largely independent of how long the list is.
913
913
 
914
914
  This is automatic, but it only engages for rows the runtime can safely stand in for. A row participates when:
915
915
 
@@ -925,9 +925,122 @@ Practical consequences when authoring a large list:
925
925
  - **A row built entirely from a child component** (`<template pp-for="row in rows"><x-row key="{row.id}" … /></template>`) is reconciled the normal way, because the parent is responsible for refreshing that boundary's bound props. Prefer plain markup in the row when the list is large and its rows rarely change.
926
926
  - Keep values that feed a row stable. A row whose markup is regenerated identically is free; a row whose markup differs by even one character is full work.
927
927
 
928
- ### Diagnose the owner before the runtime
928
+ ### A mounted child boundary is reconciled by its attributes, not by its markup
929
929
 
930
- Classify the symptom before editing `public/js/pp-reactive-v2.js`:
930
+ The same idea applies to composition, which is the shape most pages actually have: a shell that assembles many `x-*` components rather than one component owning a wall of markup.
931
+
932
+ A child component's internals belong to the child, so the parent never reconciles into them — it only reconciles the child's root, whose attributes carry the props. Once the child is mounted, the parent therefore emits that root **with an empty body** instead of re-emitting the child's markup, which means a parent render no longer re-serializes and re-parses subtrees it was going to ignore. In a real browser, a shell of 30 small children went from 0.82 ms to about 0.30 ms per parent update, and the cost stopped scaling with how much markup the children contain.
933
+
934
+ This is automatic and needs no authoring change, but the shape still matters:
935
+
936
+ - **The first render of a boundary always emits its full markup**, because the child bootstraps from what the parent emitted. A boundary that a conditional removes and later restores starts that cycle again.
937
+ - **A boundary is excluded when the parent is responsible for its content**: owned/slot content (`pp-owner`), a context provider inside it, or a parent-owned `pp-ref` capture inside it. Those keep re-emitting in full.
938
+ - **Props stay live.** Attributes on the boundary root are emitted and reconciled on every render exactly as before, so a child whose props changed still re-renders.
939
+ - **Prefer passing data as props over projecting large slot content** into a frequently re-rendering shell — projected content is re-rendered by its owner, so it does not get this treatment.
940
+
941
+ If a reused boundary is ever found not to be mounted, the runtime logs `[PP-WARN] Nested boundary content reuse was abandoned…`, permanently disables the reuse for that component, and re-renders it from full markup. Seeing that warning means the cache and the DOM disagreed — report it rather than working around it.
942
+
943
+ Two related properties follow from the same idea, and they are worth knowing when you reason about what a parent render costs:
944
+
945
+ - **A bound prop is committed to the child's root only when its value changes.** `active="{selected === 3}"` is evaluated every render, but the attribute is written only on the render where the answer actually flips. The live attribute holds the evaluated value the whole time — it no longer flickers back to the authored `{expr}` text mid-render, so a `MutationObserver` or a CSS transition watching a component root sees only real changes.
946
+ - **A child whose props did not change is not re-walked.** A leaf child costs its parent one prop comparison, not a traversal of its subtree.
947
+
948
+ Together with the elided body, a mounted child that nothing changed about costs its parent: one prop evaluation per bound attribute, one shallow comparison, and no DOM writes.
949
+
950
+ ### Prop identity decides whether a child re-renders
951
+
952
+ A child re-renders when its props changed, and props are compared **shallowly, by identity**. Every brace expression on a boundary root is re-evaluated on each parent render, so an expression that *builds* a value returns a new identity every time and the child re-renders even when the data is identical.
953
+
954
+ This is the single most common cause of "my whole page re-renders when I type one character":
955
+
956
+ ```html
957
+ <!-- Every parent render creates a new array and a new function, so this child
958
+ re-renders on every parent render even when `products` never changed. -->
959
+ <x-product-table
960
+ rows="{products.filter((p) => p.active)}"
961
+ on-select="{(row) => setSelected(row.id)}"
962
+ />
963
+ ```
964
+
965
+ Give each of them a stable identity, and the child is skipped entirely when nothing it depends on changed:
966
+
967
+ ```html
968
+ <x-product-table
969
+ rows="{activeProducts}"
970
+ options="{tableOptions}"
971
+ on-select="{selectRow}"
972
+ />
973
+
974
+ <script>
975
+ const [products, setProducts] = pp.state([]);
976
+ const [selected, setSelected] = pp.state(null);
977
+
978
+ const activeProducts = pp.memo(
979
+ () => products.filter((p) => p.active),
980
+ [products]
981
+ );
982
+
983
+ // Constant for the life of the component.
984
+ const tableOptions = pp.memo(() => ({ dense: true }), []);
985
+
986
+ const selectRow = pp.callback((row) => setSelected(row.id), []);
987
+ </script>
988
+ ```
989
+
990
+ Rules of thumb:
991
+
992
+ - **Primitives are free.** `label="Total"`, `count="{items.length}"` and `active="{selected === row.id}"` compare by value, so they only "change" when the value really changes. Prefer passing primitives over passing an object the child immediately destructures.
993
+ - **Build object props in the script, never inline in the attribute.** An attribute value that starts with `{{` is read as a server template block, not a PulsePoint binding, so an inline object literal is not valid there — assign it to a name in `<script>` (memoized) and pass that name.
994
+ - **`pp.memo` for derived arrays/objects, `pp.callback` for handlers** passed as props. Both need a dependency array; without one they rebuild every render and nothing is gained.
995
+ - **Do not memoize primitives.** `pp.memo(() => a + b, [a, b])` costs more than the addition.
996
+ - This only matters for values crossing a component boundary. Inside one component's own markup, an inline object or arrow function is harmless.
997
+
998
+ ### Give a provider a stable context value
999
+
1000
+ Context values are compared by identity too, and a provider's consumers are refreshed whenever that identity changes. Providing a freshly built object makes **every consumer in the subtree** re-render on **every** provider render — the widest-blast-radius version of the problem above:
1001
+
1002
+ ```html
1003
+ <!-- Wrong: a new object every render, so all consumers refresh every render. -->
1004
+ <script>
1005
+ const [theme, setTheme] = pp.state("light");
1006
+ ThemeContext.Provider({ value: { theme, setTheme } });
1007
+ </script>
1008
+ ```
1009
+
1010
+ ```html
1011
+ <!-- Right: identity changes only when `theme` does. -->
1012
+ <script>
1013
+ const [theme, setTheme] = pp.state("light");
1014
+
1015
+ const themeValue = pp.memo(() => ({ theme, setTheme }), [theme]);
1016
+ ThemeContext.Provider({ value: themeValue });
1017
+ </script>
1018
+ ```
1019
+
1020
+ Split unrelated values into separate contexts when they change at different rates. A context carrying both a rarely-changing `user` and a per-keystroke `query` forces every `user` consumer to re-render on each keystroke; two contexts keep each consumer tied to what it actually reads.
1021
+
1022
+ ### Keep effects from doing work nobody asked for
1023
+
1024
+ An effect with no dependency array runs after **every** render of its component:
1025
+
1026
+ ```html
1027
+ <script>
1028
+ // Runs on every render, refetching on unrelated state changes.
1029
+ pp.effect(() => { loadRows(page); });
1030
+
1031
+ // Runs only when `page` changes.
1032
+ pp.effect(() => { loadRows(page); }, [page]);
1033
+
1034
+ // Runs once on mount.
1035
+ pp.effect(() => { subscribe(); return () => unsubscribe(); }, []);
1036
+ </script>
1037
+ ```
1038
+
1039
+ Dependencies are compared by identity as well, so an object or function rebuilt each render belongs in `pp.memo`/`pp.callback` before it is used as a dependency — otherwise the array never matches and the effect runs every render anyway.
1040
+
1041
+ ### Diagnose the owner before blaming the runtime
1042
+
1043
+ Nearly every "PulsePoint is slow" report is an authoring or ownership problem, and the fix is in the template or the component split — not in the runtime. Classify the symptom first:
931
1044
 
932
1045
  | Evidence | Likely classification | First action |
933
1046
  | --- | --- | --- |
@@ -935,9 +1048,11 @@ Classify the symptom before editing `public/js/pp-reactive-v2.js`:
935
1048
  | A debounce fires just before typing stalls | Component/request ownership issue | Debounce the RPC or expensive action, not an unnecessary broad-owner setter |
936
1049
  | Loading and result setters produce two large commits | Component authoring issue | Remove invisible loading commits or isolate the visible indicator |
937
1050
  | A high-frequency control owns a large list/provider/dialog subtree | Component-boundary issue | Move the state to a smaller focused component |
938
- | Rendered HTML is byte-identical but a large stable subtree is still reconciled | Possible runtime issue | Profile runtime phases and verify the stable-DOM case |
939
- | One small necessary binding change spends disproportionate time in DOM diffing or nested bootstrap | Possible runtime issue | Build a focused reproduction and benchmark before changing reconciliation |
940
- | Changing one row of a large list costs about as much as changing every row | Component authoring issue first | Check the row is keyed, single-rooted, and not a component boundary, so per-row reuse can engage |
1051
+ | A child re-renders on every parent render although its visible props look unchanged | Component authoring issue | A prop is a new object/array/function identity each render — see "Prop identity decides whether a child re-renders" |
1052
+ | Every context consumer re-renders whenever the provider renders | Component authoring issue | The provided `value` is a fresh object each render memoize it |
1053
+ | Changing one row of a large list costs about as much as changing every row | Component authoring issue | Check the row is keyed, single-rooted, and not a component boundary, so per-row reuse can engage |
1054
+ | Rendered HTML is byte-identical but a large stable subtree is still reconciled | Possible runtime issue | Capture `pp.getPerfStats()` for the interaction and report it with the component shape |
1055
+ | One small necessary binding change spends disproportionate time in `domDiff` or `bootstrapNested` | Possible runtime issue | Reduce it to the smallest reproducing template, then report it with the phase numbers |
941
1056
 
942
1057
  Enable measurements only while profiling:
943
1058
 
@@ -978,7 +1093,7 @@ Important:
978
1093
  - Context is component-level, not directive-based. There is no `pp-context` attribute.
979
1094
  - `pp.context(token)` resolves from ancestors. A component does not consume the value it provides in the same render.
980
1095
  - If provider and consumer live in different component script scopes, pass the token through props or store it in shared outer or global state.
981
- - 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`.
1096
+ - The preferred authoring style is a lowercase provider tag in the template. The runtime recognizes `*.provider` tags case-insensitively and rewrites them to runtime-owned `<pp-context-provider>` boundaries. The context lookup is also case-insensitive, so `<themecontext.provider>` can resolve a script binding named `ThemeContext`.
982
1097
  - 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.
983
1098
  - Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
984
1099
 
@@ -1080,7 +1195,8 @@ Example:
1080
1195
  The render pipeline wraps each top-level `pp-component` root in an inert `<template pp-component="…">`, and the runtime materializes it back into live DOM on `mount()` (and on every SPA navigation) before it scans for roots. The browser never parses, validates, or fetches the contents of a `<template>`, so raw `{...}` placeholders never reach live DOM at first paint.
1081
1196
 
1082
1197
  - Because of this, `{...}` is safe in **any** attribute or position — including slots the browser would otherwise validate eagerly: SVG geometry (`d`, `viewBox`, `points`, `transform`), URL attributes (`src`, `srcset`, `href`, `poster`), form `value` on `date`/`number`/`color` inputs, and text placed directly inside `<table>` or `<select>`.
1083
- - Do not add per-tag workarounds to dodge first-paint validation (static-path `hidden` toggles instead of binding `d`, `data-*` URL holders, `hidden`-gated `<img src>`, or an SSR-resolved initial value). The deferral removes the whole class at once.
1198
+ - Before materialization, PulsePoint captures and empties each plain component script so inserting the fragment cannot trigger native browser execution; scripts introduced by later morphs receive the same treatment.
1199
+ - Do not add per-tag workarounds to dodge first-paint validation (static-path `hidden` toggles instead of binding `d`, `data-*` URL holders, `hidden`-gated `<img src>`, or an SSR-resolved initial value). The deferral removes the whole class at once.
1084
1200
  - `pp-style` and the `<input>`/`<select>`/`checked`/`defaultvalue`/`<textarea>` value rewrites still apply — but for different reasons that deferral does not replace: authoring-source tooling (`style="{...}"` breaks HTML/CSS linters) and attribute-vs-property correctness for controlled form fields.
1085
1201
 
1086
1202
  ## Refs
@@ -1315,7 +1431,7 @@ When you inspect rendered HTML in the browser, you should expect to see runtime-
1315
1431
  Normal runtime output includes:
1316
1432
 
1317
1433
  - `pp-component="..."` on mounted roots.
1318
- - `script[type="text/pp"]` for owned component scripts.
1434
+ - plain owned component `<script>` elements during deferred bootstrap; the runtime removes them from the rendered component template after capturing their source.
1319
1435
  - internal attributes such as captured ref tokens and event-owner bookkeeping.
1320
1436
 
1321
1437
  These are runtime details.
@@ -1334,9 +1450,9 @@ Use these rules when generating or editing PulsePoint runtime code:
1334
1450
  - 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.
1335
1451
  - Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
1336
1452
  - Use `public/js/pp-reactive-v2.js` as the shipped runtime contract AI should follow.
1337
- - Keep `main.py` in view because it injects the runtime-facing attributes and rewrites authored scripts before the browser sees them.
1453
+ - Keep `main.py` in view because it injects runtime-facing component attributes and defers roots before the browser sees live component DOM.
1338
1454
  - If a development-only source tree exists behind the shipped runtime, treat it as optional implementation detail rather than something generated apps are guaranteed to contain.
1339
- - In authored Caspian templates, do not handwrite `pp-component` or `type="text/pp"`; let the render pipeline inject them.
1455
+ - In authored Caspian templates, do not handwrite `pp-component`; let the render pipeline inject it, and keep owned logic in a plain `<script>`.
1340
1456
  - For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md), keep the shared interactive shell in the parent folder's `layout.html`, and keep route-specific PulsePoint code in each child `index.html`.
1341
1457
  - 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.
1342
1458
  - Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
@@ -1347,7 +1463,7 @@ Use these rules when generating or editing PulsePoint runtime code:
1347
1463
  - Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
1348
1464
  - 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(...)`.
1349
1465
  - If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
1350
- - In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
1466
+ - In authored templates, use a plain, untyped `<script>` inside the root. PulsePoint safely captures it during deferred bootstrap and morph insertion.
1351
1467
  - Keep template-facing variables at top level.
1352
1468
  - Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
1353
1469
  - Do not invent `pp.provideContext`, `pp-context`, or other legacy context helpers.
@@ -1388,7 +1504,7 @@ Also avoid:
1388
1504
  - `pp-dynamic-script`
1389
1505
  - `pp-dynamic-meta`
1390
1506
  - `pp-dynamic-link`
1391
- - handwritten `pp-component="..."` or `type="text/pp"` in authored route, layout, or component templates
1507
+ - handwritten `pp-component="..."` in authored route, layout, or component templates
1392
1508
  - `pp.fetchFunction()` as the current raw runtime helper name
1393
1509
  - made-up hooks, directives, or globals not present in the current bundled runtime
1394
1510
 
@@ -1398,8 +1514,9 @@ These are current runtime caveats that matter for authors and AI tools:
1398
1514
 
1399
1515
  - `pp-component` is the registry key for instances, state, parent tracking, and templates. Treat it as unique per mounted root.
1400
1516
  - `public/js/pp-reactive-v2.js` is the runtime surface that ships and should be assumed to exist.
1401
- - Caspian already injects `pp-component` and rewrites owned scripts to `type="text/pp"` during render.
1402
- - Nested roots without their own `script[type="text/pp"]` block are not fully isolated during parent template compilation.
1517
+ - Caspian injects `pp-component` and preserves owned scripts as plain elements inside deferred roots.
1518
+ - PulsePoint captures and empties plain scripts before a deferred root or later morph becomes live, preventing native browser execution while preserving component-scope evaluation.
1519
+ - Nested roots without their own plain owned `<script>` block are not fully isolated during parent template compilation.
1403
1520
  - The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
1404
1521
  - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
1405
1522
  - Callback refs may return synchronous cleanup functions, which run instead of a later `callback(null)` detach call.
@@ -185,7 +185,7 @@ Place those import comments at the top of the file, above the authored root elem
185
185
 
186
186
  Keep route templates as composition, not as the place where all section markup accumulates. A route with tabs, dashboard panels, forms, tables, or repeated cards should import focused components for those responsibilities and assemble them with `x-*` tags. For example, a dashboard route can import `<x-dashboard-header />`, `<x-metric-strip />`, `<x-activity-tab />`, and `<x-settings-tab />` rather than placing every tab panel's full markup in `index.html` or one giant Python component.
187
187
 
188
- 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.
188
+ 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`. 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.
189
189
 
190
190
  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.
191
191
 
@@ -389,7 +389,7 @@ Resolved SEO fields are exposed to layouts as `{{ metadata.* }}`, while values r
389
389
 
390
390
  Use `layout.html` for the shared wrapper markup of a subtree. Keep the visible shell here, not in `layout.py`.
391
391
 
392
- Follow the same authoring contract used by route templates: one authored parent node, top-of-file `<!-- @import ... -->` directives above that root, plain `<script>` inside the root when needed, and no handwritten `pp-component` or `type="text/pp"`. See [pulsepoint.md](./pulsepoint.md) for the canonical authored-vs-runtime explanation.
392
+ Follow the same authoring contract used by route templates: one authored parent node, top-of-file `<!-- @import ... -->` directives above that root, a plain `<script>` inside the root when needed, and no handwritten `pp-component`. See [pulsepoint.md](./pulsepoint.md) for the canonical authored-vs-runtime explanation.
393
393
 
394
394
  Place child routes with a plain HTML `<slot />` tag. Caspian replaces that layout slot with the current child route or nested layout while rendering.
395
395
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {