create-caspian-app 1.0.10 → 1.0.12
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.
package/dist/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ Do not treat the existence of a packaged doc as proof that the feature is enable
|
|
|
15
15
|
- Keep repo-wide always-on rules in `.github/copilot-instructions.md`.
|
|
16
16
|
- Keep this file focused on decision order, task routing, workspace-specific clarifications, and packaged-doc maintenance.
|
|
17
17
|
- Keep packaged docs under `node_modules/caspian-utils/dist/docs/` framework-oriented and use `core-runtime-map.md` when those docs need to point AI back to `main.py` or the installed `casp` runtime.
|
|
18
|
-
-
|
|
18
|
+
- **Only the built runtime under `public/js/**`exists in a generated Caspian app.** Whatever this workspace uses to produce it is a local build detail, not part of the product: never document it, reference it, or route AI to it from the packaged docs. Describe the runtime by its *behavior contract* — what the shipped runtime does — and treat`public/js/pp-reactive-v2.js` as the artifact under discussion. The same rule applies to this workspace's own quality tooling and any performance measurement setup: they are development-only and never appear in packaged docs.
|
|
19
19
|
- `node_modules/` is not version-controlled, so every packaged-doc edit must also be ported into the `caspian-utils` package source or the next reinstall wipes it.
|
|
20
20
|
|
|
21
21
|
## Decision Order
|
|
@@ -82,9 +82,11 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
82
82
|
- For first-party HTML interactivity in this workspace, PulsePoint is the required default. Use PulsePoint `on*` event attributes, `pp.state`, refs, effects, directives, and `pp.rpc()` instead of inventing id/data-attribute driven JavaScript with `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or parallel client state. For simple forms, bind `onsubmit` in the HTML, convert named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`, and validate/normalize that payload in Python; do not add `pp-ref` to each input, create a form ref, and attach an effect-managed submit listener just to collect submitted values.
|
|
83
83
|
- For PulsePoint performance work, read `node_modules/caspian-utils/dist/docs/pulsepoint.md` "High-performance authoring" and `fetch-data.md` "Search, filters, and request races" before changing the runtime. Diagnose ownership first: `pp.state` is for values whose change must produce a render (markup, bound props/context, or render-dependent effects); `pp.ref` is for timers, request generations, pagination cursors, and transient query text that should persist without rendering. A debounce delays work but does not make a state update cheap. Keep high-frequency input state in the smallest owning component, do not toggle loading state when that toggle changes no visible UI, discard stale RPC responses, and remember that `pp.transition()` reports pending work but does not provide concurrent rendering. Treat a large component that explicitly requests unnecessary renders as an authoring issue; treat byte-identical output that still traverses a large stable subtree as a possible runtime issue. Never "optimize" either case by replacing PulsePoint with manual DOM wiring.
|
|
84
84
|
- **Keyed `pp-for` rows are reconciled per row, not per list.** The runtime remembers the markup each keyed row produced; a row that re-renders byte-identically is stood in for by a `<tag pp-keep key="…">` placeholder that the morph pass resolves by repositioning the existing live node, skipping re-parse, attribute sync and event rebinding for that subtree. Three or more _consecutive_ reused rows collapse further into a single `<tag pp-keep-run="k1,k2,…">` marker, so the parse cost of a mostly-unchanged list stops scaling with its length — but the morph still resolves every key in that list individually and in order, so liveness and ordering are verified exactly as they are for a single placeholder. Never "optimise" the run marker by trusting its length instead of its keys; that trade turns a stale cache into silent DOM corruption rather than a caught mismatch. Four invariants hold this together and must not be relaxed casually: a placeholder **reuses the row's own tag name**, because an unknown tag inside `<tbody>`/`<select>` is foster-parented out of its container and silently tears the row out of its table; a run marker also carries a `key` so the morph still takes its keyed path; rows carrying a nested component boundary, owned slot content, or a context provider are excluded, because the morph pass is what refreshes those bindings; and the render cycle compares rows _emitted_ as placeholders against rows _resolved_, permanently disabling reuse for that component and re-rendering it from full markup if the two ever disagree. Keys that cannot be packed unambiguously into the marker (anything outside `[A-Za-z0-9_.:-]`) fall back to one placeholder per row. Recognising an unchanged row is deliberately **positional first**: a row whose markup matches what the same index rendered last time reuses that row's stored entry outright, costing one string comparison and skipping the key regex, the allocation and the map insert entirely; only a row that misses that check is parsed, and then looked up by key so a row that _moved_ rather than changed is still recognised. Entries are immutable once built and shared with the next render, and the key index is rebuilt only when the key sequence itself changed — so keep any new per-row work off the fast path, or a mostly-unchanged list starts paying per row again. Eligibility is decided at compile time — nested loops (whose cache slot would be shared across every iteration of the outer loop) and loop bodies that render more than one root element per row opt out entirely. The author-facing half of this contract is in `pulsepoint.md` "Keyed rows are reconciled per row, not per list".
|
|
85
|
-
- **
|
|
85
|
+
- **A mounted nested boundary's body is elided from the parent's render, not re-emitted.** The compiler already masks a script-bearing child boundary out of the parent template (`maskComplexComponents`) and restores its markup verbatim on every parent render — that markup is a compile-time _constant_, and the morph pass stops at the boundary and never descends into it, so parsing it was pure waste. `BoundaryContentCache` therefore emits the boundary's opening tag with an empty body (marked `pp-keep-content`) once the child is mounted, keeping attributes — which carry the props — emitted and reconciled exactly as before. Measured on a 100-child shell, this removed the large majority of the parent's render time. The invariants that make it safe must not be relaxed: a stub is emitted **only when the previous committed render emitted that same boundary key** (a first or reappearing boundary renders in full, because the child bootstraps from the markup the parent emitted); boundaries whose content the parent is responsible for reconciling are excluded at compile time (`pp-owner`, `<pp-context-provider`, `pp-ref`/`data-pp-ref` inside the body); the marker is stripped from the source before attribute syncing so it never reaches the DOM or `pp.props`; and, exactly like loop rows, the render cycle compares stubs _emitted_ against stubs _resolved against a live, non-empty boundary_, disabling reuse for that component and re-rendering from full markup (with `shouldForceChildRefresh` so children repaint) if the two disagree. **Owned/slot content is compiled in the owner's scope but rendered into a different component's DOM**, so `createOwnedRenderScope` deletes `__pp_boundary_html` — restoring it would let a component stub a boundary its own morph can never reach. The author-facing half is in `pulsepoint.md` "A mounted child boundary is reconciled by its attributes, not by its markup".
|
|
86
|
+
- **The bootstrap pass merges each parent scope once, not once per child.** `NestedBoundaryManager` caches a `ScopeBundle` (merged ancestor scope + compilable keys + key signature + argument values) per boundary parent for the duration of one `bootstrap()` call, and threads it into both `applyAttributeInterpolations` and the child's `computePropsFromAttributes`. Re-deriving it per child re-ran an `Object.assign` merge of the whole ancestor chain and missed the scope-descriptor WeakMap every time, because the merge produced a fresh object. Reusing one object is what keeps that cache warm. The one case that must keep resolving per child is a child whose base id equals its parent's boundary id, where `resolveComponentId` relative to the child lands on a different component.
|
|
87
|
+
- **A nested boundary's bound root attributes are evaluated from its captured bindings, and written only when the value changes.** `__ppRawBindings` is the source of truth for a boundary's expressions; the live attribute holds the _evaluated_ value continuously. `applyBoundaryBindings` evaluates from the capture and commits only on difference, and `syncNestedBoundaryAttributes` passes those attribute names to `syncAttributes` as `skipAttrs` whenever the expressions themselves are unchanged. Do not "simplify" this back into reading the expression off the element: that requires writing `{expr}` onto the live element before every evaluation, which is what made each bound attribute cost three DOM writes per render (morph writes source text, bootstrap rewrites it, bootstrap writes the result) — over 24,000 attribute mutations became 240 for 60 updates of a 100-child shell. Objects, functions and unparseable expressions must keep the raw binding text on the element, because `computePropsFromAttributes` re-evaluates them from it. Also note where the `pp-keep-content` marker is filtered: strip it from the throwaway source in `DomMorpher` rather than skipping it inside the `syncAttributes` per-attribute loop, because every list row pays for that loop.
|
|
88
|
+
- **A child whose props did not change is not re-walked.** `refreshPropsFromParent` only re-runs `bootstrapNestedComponents()` when the child produced nested runtime structure in its own last render (`hadNestedRuntimeStructures`, the same condition `render()` uses). For a leaf component — every card in a shell — the pass traversed nothing, rebuilt an empty provider set and collected an always-empty descendant list, once per child per parent render.
|
|
86
89
|
- **Every per-render capture store mints ids from a sequence that restarts at zero each render** (the `ppref_`, `ppinput_`, `ppselect_`, `ppchecked_`, `ppcontext*_`, `ppdefault*_` and `ppv_` families), so unchanged markup re-renders to an identical string. Do not give any of them a globally increasing counter: the loop capture store used to, which made every row carrying a per-row handler byte-different on every render and defeated both the byte-identical render skip and per-row reuse. These ids are also lifted out of event-handler source so all rows of one loop share a single compiled handler function — if an id format changes, the matching extraction must change with it, or every row compiles and caches its own handler.
|
|
87
|
-
- Reconciliation work is measured with a jsdom benchmark harness that mounts real components and reports a median per scenario. It belongs to this runtime-development workspace and is not part of a generated Caspian app, so never route app-facing guidance or packaged docs to it. When using it: run one scenario per process and take a best-of-N, because running the whole suite in a single process lets earlier scenarios distort later ones through GC and JIT state — that has already produced false ±50% readings on scenarios that were in fact unchanged. jsdom exaggerates HTML parsing and `insertBefore` relative to a real browser, so treat it as a relative A/B harness, never as absolute timings, and compare against a checkout of the previous runtime rather than against remembered numbers.
|
|
88
90
|
- When `caspian.config.json` has `websocket: true`, WebSocket behavior is app-owned in `main.py`. Do not assume fixed route names or endpoint paths across Caspian projects; define project-specific socket paths in the app, keep shared socket helpers in `src/lib/websocket/**` when needed, and let each route that needs a socket client pass its own `websocket_path` or `websocket_url` into its template.
|
|
89
91
|
- WebSocket pages may be public, authenticated, role-gated, or mixed depending on the app. Keep route-specific browser UI in that route's `src/app/**/index.html`, keep first-render socket URL/path values in the matching `index.py`, and keep reusable session/auth/connection/broadcast helpers out of route files only when they are shared.
|
|
90
92
|
- For WebSocket clients, use PulsePoint for component state and lifecycle but native `new WebSocket(...)` for the transport. Keep the socket in `pp.ref(...)`, close it in a cleanup effect, and keep direct socket event listeners inside the owning route template script.
|