caspian-utils 0.1.9 → 0.1.11

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,18 +54,18 @@ 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 |
58
- | Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, `Object.fromEntries(new FormData(...).entries())` for simple form submits, and avoiding id-driven `querySelector` or `addEventListener` wiring |
59
- | Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and mandatory Prisma Python ORM use for database reads and writes when Prisma is enabled |
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, `Object.fromEntries(new FormData(...).entries())` for simple form submits, and avoiding id-driven `querySelector` or `addEventListener` wiring |
59
+ | Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and mandatory Prisma Python ORM use for database reads and writes when Prisma is enabled |
60
60
  | Explain why authored Caspian templates use a plain `<script>` instead of `type="text/pp"`. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [routing.md](./routing.md) | `main.py`, `.venv/Lib/site-packages/casp/scripts_type.py`, `.venv/Lib/site-packages/casp/components_compiler.py` | script rewriting, `pp-component` injection, and authored-vs-runtime boundaries |
61
61
  | Debug why `StateManager` does not persist across a full redirect. | [index.md](./index.md), [state.md](./state.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/state_manager.py` | wire vs non-wire reset behavior and `request.state.session` dependency |
62
- | 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
- | 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
- | 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) | `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
- | 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
- | Add or debug a live WebSocket channel. | [index.md](./index.md), [websockets.md](./websockets.md), [core-runtime-map.md](./core-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [auth.md](./auth.md) | `caspian.config.json`, `main.py`, `src/lib/websocket/**`, `src/app/**`, `settings/bs-config.json` | `websocket` feature gate, endpoint registration, origin checks, auth/session handling, native `WebSocket` client lifecycle, and BrowserSync URL selection |
68
- | 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 |
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
+ | 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
+ | 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) | `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
+ | 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
+ | Add or debug a live WebSocket channel. | [index.md](./index.md), [websockets.md](./websockets.md), [core-runtime-map.md](./core-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [auth.md](./auth.md) | `caspian.config.json`, `main.py`, `src/lib/websocket/**`, `src/app/**`, `settings/bs-config.json` | `websocket` feature gate, endpoint registration, origin checks, auth/session handling, native `WebSocket` client lifecycle, and BrowserSync URL selection |
68
+ | 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 |
69
69
 
70
70
  Treat the table as a prompt pack for spot checks, not as a full validation matrix.
71
71
 
@@ -74,12 +74,12 @@ Treat the table as a prompt pack for spot checks, not as a full validation matri
74
74
  - AI skips `caspian.config.json` and assumes an optional feature is enabled because a packaged doc exists.
75
75
  - AI reads only the packaged feature doc and never checks `main.py` or the installed runtime.
76
76
  - AI edits framework internals when the task only requires app-owned route or helper changes.
77
- - AI treats runtime HTML examples as authored template examples.
78
- - 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.
79
- - 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()`.
80
- - AI bypasses the generated Prisma Python ORM in a Prisma-enabled project with direct database drivers, custom fetch helpers, JSON active stores, browser-side database fetches, or a second app-owned database abstraction.
81
- - AI extracts one-route backend logic into `src/lib/**` even though the logic is not shared; route-specific first-render queries, RPC actions, validation, and response shaping belong in the owning `src/app/**/index.py`.
82
- - 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.
77
+ - AI treats runtime HTML examples as authored template examples.
78
+ - 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. This includes single-file `html("""...""")` components where the `<script>` is placed after the root's closing tag instead of nested inside it; the string handed to `html(...)` must resolve to one top-level element, script included.
79
+ - 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()`.
80
+ - AI bypasses the generated Prisma Python ORM in a Prisma-enabled project with direct database drivers, custom fetch helpers, JSON active stores, browser-side database fetches, or a second app-owned database abstraction.
81
+ - AI extracts one-route backend logic into `src/lib/**` even though the logic is not shared; route-specific first-render queries, RPC actions, validation, and response shaping belong in the owning `src/app/**/index.py`.
82
+ - 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.
83
83
  - AI decides behavior from memory without checking the owning implementation details.
84
84
 
85
85
  ## Decision Rule
@@ -37,8 +37,8 @@ As the app grows, treat `src/components/` as the default home for reusable appli
37
37
 
38
38
  When the task is about component internals rather than normal app-owned components, these runtime files are the most relevant:
39
39
 
40
- - `.venv/Lib/site-packages/casp/component_decorator.py` owns `@component`, `Component`, `render_html(...)`, the inline `html(...)` helper, and component loading.
41
- - `.venv/Lib/site-packages/casp/components_compiler.py` owns `@import` parsing, `x-*` tag resolution (including resolving a component's own tags from its Python module imports), parent-scope expansion of slot content, root validation, and `pp-component` injection.
40
+ - `.venv/Lib/site-packages/casp/component_decorator.py` owns `@component`, `Component`, `render_html(...)`, the inline `html(...)` helper (including direct-call scope capture), and component loading.
41
+ - `.venv/Lib/site-packages/casp/components_compiler.py` owns `@import` parsing, `x-*` tag 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, root validation, and `pp-component` injection.
42
42
  - `.venv/Lib/site-packages/casp/html_attrs.py` owns `get_attributes(...)` and the Python-side `merge_classes(...)` contract.
43
43
  Use [core-runtime-map.md](./core-runtime-map.md) when you need the broader Python runtime-module map. Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when a component task is specifically about browser-side PulsePoint state, refs, context, portals, events, RPC, or SPA behavior.
44
44
 
@@ -266,6 +266,54 @@ Rules for inline `html(...)`:
266
266
 
267
267
  The leading `# html` comment above the string is an optional editor hint: some editors color the HTML inside a string tagged that way (JetBrains uses `# language=HTML`). It has no runtime effect.
268
268
 
269
+ ### Single-Root Inside `html(...)`: Script Goes In The Root, Not Below It
270
+
271
+ The most common single-file mistake is returning a parent element and then a sibling `<script>` underneath it. The string handed to `html(...)` is the component's template, so it must follow the same single-root contract as a `.html` file: exactly one top-level element, with the PulsePoint `<script>` nested inside that element. A `<script>` that sits after the closing tag is a second top-level node, and the compiler raises `TemplateRootError` because there is no single root to receive `pp-component`.
272
+
273
+ Think of the returned string exactly like the one node a React component returns: everything, including the script, lives inside it.
274
+
275
+ Good — the `<script>` is inside the single root:
276
+
277
+ ```python
278
+ from casp.component_decorator import component, html
279
+
280
+ @component
281
+ def Counter(label: str = "Clicks", **props):
282
+ # html
283
+ return html("""
284
+ <div class="counter">
285
+ <h3>{{ label }}</h3>
286
+ <button onclick="setCount(count + 1)">{count}</button>
287
+
288
+ <script>
289
+ const [count, setCount] = pp.state(0);
290
+ </script>
291
+ </div>
292
+ """, label=label)
293
+ ```
294
+
295
+ Bad — the `<script>` is a sibling after the root, so the string has two top-level nodes:
296
+
297
+ ```python
298
+ from casp.component_decorator import component, html
299
+
300
+ @component
301
+ def Counter(label: str = "Clicks", **props):
302
+ # html
303
+ return html("""
304
+ <div class="counter">
305
+ <h3>{{ label }}</h3>
306
+ <button onclick="setCount(count + 1)">{count}</button>
307
+ </div>
308
+
309
+ <script>
310
+ const [count, setCount] = pp.state(0);
311
+ </script>
312
+ """, label=label)
313
+ ```
314
+
315
+ The fix is always the same: move the `<script>` above the root's closing tag so the whole return value is one element. Caspian has no fragment syntax — the single root must be a real native element — so if a component genuinely needs sibling-looking sections, wrap them all in one outer element (a `<div>` or `<section>`) and keep the script inside that wrapper. This rule is identical for the two-file `render_html(...)` form, so the same `.html` file must also end with `</script></root>` rather than `</root>` followed by a trailing `<script>`.
316
+
269
317
  ## Component Imports: Python Imports Or `@import`
270
318
 
271
319
  A component can render other components with `x-*` tags in either the two-file `.html` or the inline `html(...)` form. There are two ways to tell Caspian which component a tag refers to.
@@ -300,6 +348,33 @@ Resolution precedence for an `x-*` tag inside a component's own output, lowest t
300
348
 
301
349
  So a component's Python import wins over an inherited same-name component, and an explicit local `@import` wins over both.
302
350
 
351
+ ### Direct-Call Composition (Calling A Component As A Function)
352
+
353
+ A component can compose another component two ways: by writing its `<x-*>` tag, or by calling it as a plain Python function and interpolating the result with `{{ }}` (React-style). Both work, and both resolve nested `<x-*>` tags from the callee's own Python module imports.
354
+
355
+ ```python
356
+ from casp.component_decorator import component, html
357
+ from src.components.Panel import Panel # Panel's markup contains <x-tag>
358
+
359
+ @component
360
+ def Page(**props):
361
+ panel_markup = Panel() # direct call, not <x-panel>
362
+ # html
363
+ return html("""
364
+ <div>
365
+ {{ panel_markup }}
366
+ </div>
367
+ """, panel_markup=panel_markup)
368
+ ```
369
+
370
+ Even though `Panel()` returns a plain string here, its nested `<x-tag>` still resolves from `Panel`'s own module imports, exactly as if `Panel` had been reached through an `<x-panel>` tag. The calling component does not need to import `Tag` or add an `@import` for it — a component's tags always resolve against the module that authored them.
371
+
372
+ Notes:
373
+
374
+ - Resolution stays import-driven and per-module, so the same-name/no-collision guarantee holds: the `<x-tag>` inside `Panel`'s output resolves to whatever `Tag` `Panel`'s module imported, regardless of where `Panel()` was called from.
375
+ - Prefer the `<x-*>` tag form for readability; reach for the direct call when you need the rendered markup as a value (e.g. to pass into another context key or compose conditionally in Python).
376
+ - This is handled entirely by the runtime; there is nothing extra to author. The owning functions live in `component_decorator.py` and `components_compiler.py` (see the Framework Internals Note above).
377
+
303
378
  ### Slot Content Resolves In The Parent Scope
304
379
 
305
380
  Component tags written as slot content (children passed between a component's opening and closing tags) resolve in the scope where they were authored, not in the child component's scope. This matches slot semantics in other component systems: a `<x-tag>` written by the parent stays bound to the parent's `Tag` even when it is slotted into a child that has its own `Tag`. The component that authors a tag in markup must import that component, the same way it would for any tag it renders directly.
@@ -98,8 +98,8 @@ Interactive CRUD page:
98
98
  | [.venv/Lib/site-packages/casp/state_manager.py](../../../../.venv/Lib/site-packages/casp/state_manager.py) | request-scoped state, session bucket persistence, listener lifecycle, and wire-request reset behavior | [state.md](./state.md) |
99
99
  | [.venv/Lib/site-packages/casp/cache_handler.py](../../../../.venv/Lib/site-packages/casp/cache_handler.py) | `Cache`, cache manifest handling, filename generation, disk-backed HTML storage, and invalidation | [cache.md](./cache.md) |
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
- | [.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, component loading, and prop normalization before component calls | [components.md](./components.md) |
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), 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) |
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
+ | [.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
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
104
  | [.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
105
  | [.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) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {