SSR docs declare the embedded-V8 architecture; surface re-chartered per appeal

docs/SSR_ARCHITECTURE.md now specifies the decided shape: Django template
backend rendering through the PyO3-bound SsrEngine (cores/mizan-rust-ssr,
deno_core + deno_web), bundles built by mizan-generate, props crossing as
parsed values, no external JS runtime serving requests, no_rsc guard.
docs/AFI_ARCHITECTURE.md follows (mizan-rust-ssr in the cores layout, Bun
worker delisted). OWED_SURFACE.md regenerated via appeal-surface: the
Bun→PyO3 cutover, the PyO3 binding surface, and the mizan-generate SSR
bundling step are now chartered owed mechanisms instead of an orphaned
engine crate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 19:43:16 -04:00
parent 81ea0cea9f
commit e4091dfbe8
3 changed files with 93 additions and 65 deletions

View File

@@ -115,16 +115,18 @@ Cross-function graph checks (fail at IR-build time, before any client is emitted
## Unit: mizan-rust-ssr (`cores/mizan-rust-ssr`) ## Unit: mizan-rust-ssr (`cores/mizan-rust-ssr`)
**Charter.** The embedded-V8 SSR engine and the anti-RSC guard. It owns rendering a build-time JS bundle to HTML in-process via `deno_core`, and the structural guarantee that the SSR surface never imports an RSC/Flight runtime. It does not own the Django template backend (that is `mizan-django/ssr`). **Charter.** The embedded-V8 SSR engine, its PyO3 binding, and the anti-RSC guard. It owns rendering a build-time JS bundle to HTML in-process via `deno_core`, exposing that engine to the Python side across an in-process FFI boundary, and the structural guarantee that the SSR surface never imports an RSC/Flight runtime. It does not own the Django template backend (that is `mizan-django/ssr`) or the bundling that produces the JS bundle (that is `mizan-generate`).
**Claims substantiated here.** **Claims substantiated here.**
- SSR is hand-rolled; no frontend adapter imports an SSR runtime or meta-framework (Next/Nuxt/SvelteKit/RSC/Flight) — the CVE-2025-55182 pre-auth-RCE deserialization class (HOLOMORPHICS/Mizan project note; MEMORY: mizan-ssr-no-framework-runtimes; enforced by `cores/mizan-rust-ssr/tests/no_rsc.rs`). - SSR is hand-rolled; no frontend adapter imports an SSR runtime or meta-framework (Next/Nuxt/SvelteKit/RSC/Flight) — the CVE-2025-55182 pre-auth-RCE deserialization class (HOLOMORPHICS/Mizan project note; MEMORY: mizan-ssr-no-framework-runtimes; enforced by `cores/mizan-rust-ssr/tests/no_rsc.rs`).
- SSR renders synchronously from props, injected as validated data (the AFI provides the typed one-way version). - SSR renders synchronously from props, injected as validated data (the AFI provides the typed one-way version).
- The Python side binds the engine through PyO3 — in-process FFI, no subprocess, no JSON-RPC framing; behavior lives in the binary, languages bind to it (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary; ROADMAP.md § Core Consolidation — SSR in the binary).
**Owed behavioral mechanisms.** **Owed behavioral mechanisms.**
- the engine composes a real `deno_web` web-platform layer (TextEncoder/Decoder, MessagePort, timers) rather than a partial shim, evals the trusted bundle once, and renders per request — observable: the fixture bundle renders `Hello, World!`; a missing global would fail loudly at render, not silently pass (the doc's "partial polyfill is silent-failure-shaped" concern is discharged by using deno_web's real impls). - the engine composes a real `deno_web` web-platform layer (TextEncoder/Decoder, MessagePort, timers) rather than a partial shim, evals the trusted bundle once, and renders per request — observable: the fixture bundle renders `Hello, World!`; a missing global would fail loudly at render, not silently pass (the doc's "partial polyfill is silent-failure-shaped" concern is discharged by using deno_web's real impls).
- props cross as a `v8::json::parse`d value passed as a function argument, never spliced into evaluated source — observable: the injection test feeds a prop string crafted to break out of a string-built call; it renders as inert text and does not set a global, so code injection is structurally absent. - props cross as a `v8::json::parse`d value passed as a function argument, never spliced into evaluated source — observable: the injection test feeds a prop string crafted to break out of a string-built call; it renders as inert text and does not set a global, so code injection is structurally absent.
- the no-RSC guard scans authored SSR source and dependencies for the forbidden token set (`react-server-dom`, `renderToReadableStream`, `renderToPipeableStream`, `createFromReadableStream`/`Fetch`, `use server`, `next/`, `nuxt`, `@sveltejs/kit`) and fails on presence — observable: adding any RSC/Flight/meta-framework import to the scanned fixtures turns `no_rsc.rs` red; absence alone is not the guarantee — re-entry is loud. - the no-RSC guard scans authored SSR source and dependencies for the forbidden token set (`react-server-dom`, `renderToReadableStream`, `renderToPipeableStream`, `createFromReadableStream`/`Fetch`, `use server`, `next/`, `nuxt`, `@sveltejs/kit`) and fails on presence — observable: adding any RSC/Flight/meta-framework import to the scanned fixtures turns `no_rsc.rs` red; absence alone is not the guarantee — re-entry is loud.
- OWED (unbuilt): a PyO3 binding exposes `SsrEngine` to the Python side as an in-process extension — construct-from-bundle and `render(props_json) -> HTML` cross the FFI boundary with no process spawn, honoring V8's one-isolate-per-engine / non-`Send` constraint so the caller holds one engine per (worker thread, bundle) — observable when built: the Django backend imports the engine and calls `render(props)` in-process, and a prop still crosses as a parsed value (the injection guarantee survives the FFI hop). Today `cores/mizan-rust-ssr/src/lib.rs` exposes `SsrEngine` only as a Rust API with Rust-native `#[tokio::test]` coverage and carries no `#[pyclass]`/`#[pymodule]`, so the PyO3 surface the AFI-boundary table names is unsubstantiated.
--- ---
@@ -238,18 +240,22 @@ Return-type branching + origin cache:
## Unit: mizan-django SSR (`backends/mizan-django/src/mizan/ssr`) ## Unit: mizan-django SSR (`backends/mizan-django/src/mizan/ssr`)
**Charter.** The SSR product's Django half: a template backend that renders `.tsx`/`.jsx` component files through a persistent Bun subprocess, wrapping output with a hydration payload. It owns the Django-template-engine integration and the Bun subprocess lifecycle; it does not own the JS render (that is the Bun worker). **Charter.** The SSR product's Django half: a Django template backend that renders `.tsx`/`.jsx` component files by resolving each to its built bundle and driving the PyO3-bound `SsrEngine` in-process, wrapping output with a hydration payload. It owns the Django-template-engine integration, component-to-bundle resolution, prop gathering, and the per-(worker-thread, bundle) engine lifecycle; it does not own the V8 render (that is `cores/mizan-rust-ssr`'s `SsrEngine`) or the bundling that produces the render bundle (that is `mizan-generate`).
**Claims substantiated here.** **Claims substantiated here.**
- SSR is a Django template backend replacing the rendering engine; the template name IS a `.tsx`/`.jsx` file path; context dict becomes props; output wrapped in `<div id="mizan-root">` + `window.__MIZAN_SSR_DATA__` hydration (docs/SSR_ARCHITECTURE.md). - SSR is a Django template backend replacing the rendering engine; the template name IS a `.tsx`/`.jsx` file path; context dict becomes props; output wrapped in `<div id="mizan-root">` + `window.__MIZAN_SSR_DATA__` hydration (docs/SSR_ARCHITECTURE.md).
- SSR bridge: Django template backend → persistent Bun subprocess via JSON-RPC; worker resolves by file path (`import(file)` + `renderToString`); auto-restarts on crash, thread-safe, correlates by message id (ROADMAP.md § Done; docs/SSR_ARCHITECTURE.md § Implementation surface). - The render engine is embedded-V8 inside the Mizan Rust binary, bound via PyO3 — in-process FFI, no external JS runtime serving requests, no subprocess, no JSON-RPC framing (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary — Backend adapter row).
- The component path resolves against `DIRS` to its built bundle in `OPTIONS['bundles']` (produced by mizan-generate's SSR bundling step); the backend gathers props and wraps output for hydration (docs/SSR_ARCHITECTURE.md § AFI boundary — Backend adapter row; the `TEMPLATES` config block).
- One engine per (worker thread, bundle) — V8's Locker constraint makes an engine non-`Send`, so the Django side never shares one across threads (docs/SSR_ARCHITECTURE.md § The engine).
- SSR is orthogonal to RPC and composable; first paint carries data (INVARIANTS.md § SSR). - SSR is orthogonal to RPC and composable; first paint carries data (INVARIANTS.md § SSR).
**Owed behavioral mechanisms.** **Owed behavioral mechanisms.**
- `MizanTemplates` implements Django's template-backend interface: `get_template(name)` resolves `name` as a file path under `DIRS` and returns a `MizanTemplate` wrapping the absolute path; `render` strips `request`/`csrf_token` and passes the remaining context as props — observable: `render(request, 'components/Hello.tsx', ctx)` renders that file's component with `ctx` as props; `from_string` raises (it renders files, not strings); a missing file raises `TemplateDoesNotExist`. - `MizanTemplates` implements Django's template-backend interface: `get_template(name)` resolves `name` as a `.tsx`/`.jsx` file path under `DIRS` and returns a `MizanTemplate` for the resolved component; `render` strips `request`/`csrf_token` and passes the remaining context as props — observable: `render(request, 'components/Hello.tsx', ctx)` renders that component with `ctx` as props; `from_string` raises (it renders files, not strings); a missing file raises `TemplateDoesNotExist`.
- component-to-bundle resolution: the backend resolves each component file to its self-contained render bundle in `OPTIONS['bundles']` (the `mizan-generate`-produced bundle assigning `globalThis.renderApp`) and hands the bundle to the engine, rather than reading component source directly — observable: `render` of `components/Hello.tsx` loads that component's built bundle from the configured `bundles` directory; a component with no built bundle raises rather than rendering stale or empty HTML.
- rendered output is wrapped for client hydration — observable: output contains `<div id="mizan-root">…</div>` plus `<script>window.__MIZAN_SSR_DATA__={sorted-json}</script>`, so first paint carries the props the client hydrates from. - rendered output is wrapped for client hydration — observable: output contains `<div id="mizan-root">…</div>` plus `<script>window.__MIZAN_SSR_DATA__={sorted-json}</script>`, so first paint carries the props the client hydrates from.
- `SSRBridge` holds one persistent `bun run <worker>` subprocess, correlates requests by message id over newline-delimited JSON-RPC, serializes stdin writes, is thread-safe under concurrent renders, waits for a ready signal on start, and auto-restarts on crash — observable: five concurrent renders return five correct results with no interleaving; killing the subprocess mid-life and rendering again transparently restarts it; a render exceeding the timeout raises `TimeoutError` rather than hanging. - engine lifecycle: the backend constructs one `SsrEngine` per (worker thread, bundle) pair and reuses it across requests, never sharing a single engine across threads (the engine is non-`Send`) — observable: concurrent renders on different worker threads each use their own thread-local engine and return correct results with no interleaving; the engine is built once per bundle, not per render.
- OWED (partial, docs/PSR_VS_EDGE.md § Current state): the render-on-mutation orchestration (mutation → trigger local render → store HTML), driven by the manifest's `render_strategy`, wiring the bridge to the PSR path — observable when built: a public-context mutation triggers a local re-render and stores HTML; today the bridge renders on request and the manifest records the strategy, but the mutation→render→store wiring is absent, so PSR-on-mutation is unsubstantiated. - OWED (unbuilt — the PyO3 cutover): the Django backend binds the engine through PyO3 and renders in-process — no `bun run` subprocess, no newline-delimited JSON-RPC, no `SSRBridge`, no ready-signal / auto-restart machinery, and `OPTIONS` carries `bundles` (a directory of built bundles) rather than `worker` (a JS entry file) — observable when built: a render spawns no external JS runtime process and calls the PyO3-bound `SsrEngine.render(props)` directly; `ssr/bridge.py` and the `workers/mizan-ssr` worker are retired. Today this is entirely unsubstantiated: `ssr/bridge.py` spawns `bun run <worker>` and correlates requests over JSON-RPC, `ssr/backend.py` wires `OPTIONS['worker']` to that bridge and renders via `SSRBridge.render`, and the docstrings still describe a "persistent Bun subprocess" — the subprocess architecture the docs (§ The engine, § AFI boundary) no longer describe.
- OWED (partial, docs/PSR_VS_EDGE.md § Current state): the render-on-mutation orchestration (mutation → trigger local render → store HTML), driven by the manifest's `render_strategy`, wiring the engine to the PSR path — observable when built: a public-context mutation triggers a local re-render and stores HTML; today the engine renders on request and the manifest records the strategy, but the mutation→render→store wiring is absent, so PSR-on-mutation is unsubstantiated.
--- ---
@@ -300,7 +306,7 @@ Return-type branching + origin cache:
- the suite exercises the real HTTP stack (Django test client / LiveServer) — not just RequestFactory — for dispatch, three-tier invalidation, merge, view-path branching, and cache HIT/MISS/scoped-purge — observable: `HTTPIntegrationTests` and `CacheIntegrationTests` assert the JSON body and `X-Mizan-Invalidate` header agree, that a scoped mutation preserves other users' cached entries, and that a second identical fetch is a HIT. - the suite exercises the real HTTP stack (Django test client / LiveServer) — not just RequestFactory — for dispatch, three-tier invalidation, merge, view-path branching, and cache HIT/MISS/scoped-purge — observable: `HTTPIntegrationTests` and `CacheIntegrationTests` assert the JSON body and `X-Mizan-Invalidate` header agree, that a scoped mutation preserves other users' cached entries, and that a second identical fetch is a HIT.
- the auth suite covers every axis (JWT valid/invalid/expired, MWT, session, staff/superuser/callable/PermissionError) and asserts the body never runs on failure — observable: an invalid token returns 401 without session fall-through; an anonymous call to an auth-required function returns before the body; a callable's `PermissionError` message surfaces verbatim. - the auth suite covers every axis (JWT valid/invalid/expired, MWT, session, staff/superuser/callable/PermissionError) and asserts the body never runs on failure — observable: an invalid token returns 401 without session fall-through; an anonymous call to an auth-required function returns before the body; a callable's `PermissionError` message surfaces verbatim.
- the Edge-compatibility suite asserts the properties a CDN cares about (deterministic byte-identical bodies, sorted JSON keys, URL-encoded delimiter-safe headers, `no-store` on errors/mutations, header↔body invalidation agreement, auth-differentiated responses for the same URL) — observable: these tests go red if any of those properties regress, so "Edge caching is possible" is checkable without a CDN. - the Edge-compatibility suite asserts the properties a CDN cares about (deterministic byte-identical bodies, sorted JSON keys, URL-encoded delimiter-safe headers, `no-store` on errors/mutations, header↔body invalidation agreement, auth-differentiated responses for the same URL) — observable: these tests go red if any of those properties regress, so "Edge caching is possible" is checkable without a CDN.
- the SSR suite verifies the bridge and template backend end-to-end when Bun is present (ping, render, missing-component error, crash recovery, concurrent renders, hydration wrapper) and skips gracefully otherwise — observable: a killed worker transparently restarts on the next render; five concurrent renders return five correct results. - the SSR suite verifies the engine-based render path — template-backend resolution, bundle-driven in-process render, the hydration wrapper, and concurrent renders — and asserts that no external JS runtime process is spawned during the suite (the PyO3-bound `SsrEngine` renders in-process) — observable: a render produces the `<div id="mizan-root">` + `__MIZAN_SSR_DATA__` wrapper from the resolved bundle, concurrent renders across worker threads each use their own engine and return correct results, and the suite spawns no `bun`/`node` subprocess. The Bun ping / crash-recovery / auto-restart assertions are retired with the subprocess bridge they exercised; today `test_ssr.py` still drives `SSRBridge` (Bun subprocess, JSON-RPC, killed-worker restart), so this engine-path verification is owed alongside the mizan-django SSR PyO3 cutover.
- the benchmark suite measures HTTP-vs-executor overhead and throughput with correctness assertions on each path — observable: every benchmark also asserts the function's numeric output, so a green benchmark run is also a correctness run. - the benchmark suite measures HTTP-vs-executor overhead and throughput with correctness assertions on each path — observable: every benchmark also asserts the function's numeric output, so a green benchmark run is also a correctness run.
--- ---
@@ -401,12 +407,14 @@ Return-type branching + origin cache:
**Claims substantiated here.** **Claims substantiated here.**
- Every frontend adapter is a thin idiomatic wrapper over one shared kernel; the kernel owns `ContextState<T> = {data,status,error}`, `registerContext`, `mizanCall`/`mizanFetch`, server-driven merge/invalidate, `initSession`, and a pluggable `MizanTransport` (HTTP default, Tauri/webview swap via `configure`) (INVARIANTS.md § Client Kernel; docs/AFI_ARCHITECTURE.md § Kernel model). - Every frontend adapter is a thin idiomatic wrapper over one shared kernel; the kernel owns `ContextState<T> = {data,status,error}`, `registerContext`, `mizanCall`/`mizanFetch`, server-driven merge/invalidate, `initSession`, and a pluggable `MizanTransport` (HTTP default, Tauri/webview swap via `configure`) (INVARIANTS.md § Client Kernel; docs/AFI_ARCHITECTURE.md § Kernel model).
- Shared parameters elevate to required provider props; non-shared params elevate to optional props with per-function override (INVARIANTS.md § Named Contexts; MIZAN.md §2 param elevation / `specify` resolution order).
- Codegen targets the adapter surface, never the raw kernel; React devs get hooks, Vue composables, Svelte stores, same kernel underneath (docs/AFI_ARCHITECTURE.md § Kernel model). - Codegen targets the adapter surface, never the raw kernel; React devs get hooks, Vue composables, Svelte stores, same kernel underneath (docs/AFI_ARCHITECTURE.md § Kernel model).
- Vue and Svelte ship as v1 alongside React (docs/AFI_ARCHITECTURE.md § Launch surface). - Vue and Svelte ship as v1 alongside React (docs/AFI_ARCHITECTURE.md § Launch surface).
- Cross-language HMAC pin: `deriveCacheKey` in `mizan-ts` matches the Python key byte-for-byte (docs/CACHE_KEYING.md; README.md § Adapters — TypeScript is the protocol-reference adapter). - Cross-language HMAC pin: `deriveCacheKey` in `mizan-ts` matches the Python key byte-for-byte (docs/CACHE_KEYING.md; README.md § Adapters — TypeScript is the protocol-reference adapter).
**Owed behavioral mechanisms.** **Owed behavioral mechanisms.**
- `@mizan/base` owns the single reconciled view: `ContextState`, the context registry, `mizanCall`/`mizanFetch`, server-driven `merge`/`invalidate`, `initSession`, over a `MizanTransport` interface — observable: the same behaviors the `mizan-rust` port pins (stable-key cache identity, merge-splice, scoped-vs-broad refetch, retry, dual-envelope error parse) hold in TS; the Rust port exists precisely to mirror this file. - `@mizan/base` owns the single reconciled view: `ContextState`, the context registry, `mizanCall`/`mizanFetch`, server-driven `merge`/`invalidate`, `initSession`, over a `MizanTransport` interface — observable: the same behaviors the `mizan-rust` port pins (stable-key cache identity, merge-splice, scoped-vs-broad refetch, retry, dual-envelope error parse) hold in TS; the Rust port exists precisely to mirror this file.
- the generated provider elevates a context's shared params to required props and its non-shared params to optional props, and resolves each member's effective params by overlaying per-function overrides onto the provider props at fetch time (INVARIANTS.md § Named Contexts; MIZAN.md §2 resolution order) — observable: a two-function `user` context where both take `user_id` and only one takes `page` generates a provider with required `user_id` and optional `page`; a per-function override supplies a different `page` for that one member while the shared `user_id` still covers both, and a member still missing a required param at fetch time is a runtime error, not a silent undefined.
- `deriveCacheKey` (mizan-ts) reproduces the Python HMAC key byte-for-byte — observable: the pinned vectors in `cores/mizan-python/tests/test_keys.py::test_cross_language_pin` (`ctx:user:605a1ca5…`, `ctx:user:30fc08eb…`) are asserted against the TS output; any normalization drift (bool/None stringification, key ordering) breaks the pin, which the doc marks a security vulnerability. - `deriveCacheKey` (mizan-ts) reproduces the Python HMAC key byte-for-byte — observable: the pinned vectors in `cores/mizan-python/tests/test_keys.py::test_cross_language_pin` (`ctx:user:605a1ca5…`, `ctx:user:30fc08eb…`) are asserted against the TS output; any normalization drift (bool/None stringification, key ordering) breaks the pin, which the doc marks a security vulnerability.
- adapters subscribe to the kernel and render in their own idiom without keeping a parallel copy of the truth — observable: a React hook and a Vue composable over the same context read the same kernel entry; mutating in one path updates both because the truth lives once in the kernel. - adapters subscribe to the kernel and render in their own idiom without keeping a parallel copy of the truth — observable: a React hook and a Vue composable over the same context read the same kernel entry; mutating in one path updates both because the truth lives once in the kernel.
- OWED (unbuilt, ISSUES.md § Open / ROADMAP.md § Next): `frontends/mizan-vue` and `frontends/mizan-svelte` are runtime kernel-adapter packages — the codegen emits their clients (byte-parity-tested) but no runtime package or live-backend example exists — observable when built: a Vue composable / Svelte store subscribes to `@mizan/base` and refreshes on invalidation against a live backend; today only React has full integration verification, so the "Vue and Svelte ship as v1" claim is unsubstantiated at the runtime layer. - OWED (unbuilt, ISSUES.md § Open / ROADMAP.md § Next): `frontends/mizan-vue` and `frontends/mizan-svelte` are runtime kernel-adapter packages — the codegen emits their clients (byte-parity-tested) but no runtime package or live-backend example exists — observable when built: a Vue composable / Svelte store subscribes to `@mizan/base` and refreshes on invalidation against a live backend; today only React has full integration verification, so the "Vue and Svelte ship as v1" claim is unsubstantiated at the runtime layer.
@@ -417,23 +425,23 @@ Return-type branching + origin cache:
## Unit: mizan-codegen (`protocol/mizan-codegen`) ## Unit: mizan-codegen (`protocol/mizan-codegen`)
**Charter.** The single Rust codegen binary that reads KDL IR and emits typed clients for every target (stage1, react, vue, svelte, channels, python, rust), plus the source-fetching that spawns each backend's IR-export command and the Pydantic-pre-step. It owns the IR→client transform; it does not emit IR (backends do). **Charter.** The single Rust codegen binary that reads KDL IR and emits typed clients for every target (stage1, react, vue, svelte, channels, python, rust), plus the source-fetching that spawns each backend's IR-export command and the Pydantic-pre-step, plus the SSR bundling step that compiles each SSR entry component into a self-contained render bundle. It owns the IR→client transform and the SSR bundle production; it does not emit IR (backends do) or run the render engine (that is `cores/mizan-rust-ssr`).
**Claims substantiated here.** **Claims substantiated here.**
- Codegen reads KDL directly — no OpenAPI envelope, no `openapi-typescript`, no per-backend converter; the former JS two-stage codegen is deleted (docs/AFI_ARCHITECTURE.md § Forward-direction primitives). - Codegen reads KDL directly — no OpenAPI envelope, no `openapi-typescript`, no per-backend converter; the former JS two-stage codegen is deleted (docs/AFI_ARCHITECTURE.md § Forward-direction primitives).
- Every frontend client is generated from the IR; each target is byte-parity-tested (INVARIANTS.md § Canonical IR & Codegen; ROADMAP.md § Rust codegen). - Every frontend client is generated from the IR; each target is byte-parity-tested (INVARIANTS.md § Canonical IR & Codegen; ROADMAP.md § Rust codegen).
- The codegen drives the backend's IR-export command as a subprocess and parses the KDL it writes (docs/AFI_ARCHITECTURE.md; backends/*/README.md § Generate the frontend). - The codegen drives the backend's IR-export command as a subprocess and parses the KDL it writes (docs/AFI_ARCHITECTURE.md; backends/*/README.md § Generate the frontend).
- Stage 1 (typed `callXxx`/`fetchXxx`) + Stage 2 (`<MizanContext>` provider, per-context providers, `use{Hook}()`) emission (backends/*/README.md § Generate the frontend).
- Pydantic + Rust DX: a decoru pre-step authors Rust types from Pydantic before the cargo IR bin runs; a generic `[source.script]` source spawns any command emitting KDL (backends/mizan-tauri/README.md § Pydantic; config.rs). - Pydantic + Rust DX: a decoru pre-step authors Rust types from Pydantic before the cargo IR bin runs; a generic `[source.script]` source spawns any command emitting KDL (backends/mizan-tauri/README.md § Pydantic; config.rs).
- mizan-generate's SSR bundling step compiles each SSR entry component together with `react-dom/server.browser` into a self-contained bundle assigning `globalThis.renderApp`, written to the `bundles` directory — the only place node/bun run in the SSR path (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary — Build step row).
**Owed behavioral mechanisms.** **Owed behavioral mechanisms.**
- `fetch.rs` spawns the configured source's export command (FastAPI `-m mizan_fastapi.ir`, Django `manage.py export_mizan_ir`, Rust `cargo run --bin`, or a generic script) and parses stdout as KDL — no OpenAPI/converter anywhere in the path — observable: a codegen run against a live FastAPI backend consumes only the KDL the CLI writes; the Rust source runs the cargo bin and the optional decoru pre-step first. - `fetch.rs` spawns the configured source's export command (FastAPI `-m mizan_fastapi.ir`, Django `manage.py export_mizan_ir`, Rust `cargo run --bin`, or a generic script) and parses stdout as KDL — no OpenAPI/converter anywhere in the path — observable: a codegen run against a live FastAPI backend consumes only the KDL the CLI writes; the Rust source runs the cargo bin and the optional decoru pre-step first.
- the KDL parser reconstructs the full typed IR (types with struct/list/enum/alias shapes, functions with input/output/nullable/context/affects/merge/form, contexts with param elevation, channels) — observable: `ir_deserialization.rs` reads the AFI fixture back into typed structs and asserts the function set, per-function fields, param elevation, and named-type presence. - the KDL parser reconstructs the full typed IR (types with struct/list/enum/alias shapes, functions with input/output/nullable/context/affects/merge/form, contexts with param elevation, channels) — observable: `ir_deserialization.rs` reads the AFI fixture back into typed structs and asserts the function set, per-function fields, param elevation, and named-type presence.
- each target emits deterministically and is byte-parity-tested against a committed baseline — observable: `stage1_parity.rs`, `react_parity.rs`, `rust_parity.rs`, `python_parity.rs`, `vue_svelte_parity.rs`, and `channels_smoke.rs` diff emitter output against baselines and fail on any byte drift; two different runs produce identical output. - each target emits deterministically and is byte-parity-tested against a committed baseline — observable: `stage1_parity.rs`, `react_parity.rs`, `rust_parity.rs`, `python_parity.rs`, `vue_svelte_parity.rs`, and `channels_smoke.rs` diff emitter output against baselines and fail on any byte drift; two different runs produce identical output.
- the emitters produce genuinely different, correct artifacts per target — not one shape behind distinct labels — observable: the react target emits `<MizanContext>` + per-context providers + `use{Hook}()` reading React context; vue emits composables; svelte emits stores; python emits a Pydantic-typed facade over the PyO3 kernel; rust emits a full crate depending on `mizan-rust` — each byte-checked against its own baseline, and stage1 is auto-included whenever a framework target is requested. - the emitters produce genuinely different, correct artifacts per target — not one shape behind distinct labels — observable: the react target emits `<MizanContext>` + per-context providers + `use{Hook}()` reading React context (Stage 2), Stage 1 emits the framework-agnostic typed `callXxx`/`fetchXxx`; vue emits composables; svelte emits stores; python emits a Pydantic-typed facade over the PyO3 kernel; rust emits a full crate depending on `mizan-rust` — each byte-checked against its own baseline, and stage1 is auto-included whenever a framework target is requested.
- the codegen tree-shakes and canonicalizes types to match the backend emitters, and hoists inline enums into named Rust/TS types — observable: an unreferenced type is not emitted; an inline `field { enum … }` becomes a top-level Rust enum the struct field references; the channels target emits zero files when the IR carries no channels. - the codegen tree-shakes and canonicalizes types to match the backend emitters, and hoists inline enums into named Rust/TS types — observable: an unreferenced type is not emitted; an inline `field { enum … }` becomes a top-level Rust enum the struct field references; the channels target emits zero files when the IR carries no channels.
- OWED (unbuilt): an SSR bundling step compiles each SSR entry component together with `react-dom/server.browser` into a self-contained bundle that assigns `globalThis.renderApp`, emitted into the configured `bundles` directory — this is the only place node/bun run in the SSR path — observable when built: a `mizan-generate` run produces one render bundle per SSR entry component, each evaluable standalone by the embedded-V8 engine (assigning `renderApp` at eval time and rendering from a JSON-parsed props argument). Today the codegen binary emits typed clients (stage1/react/vue/svelte/channels/python/rust) but carries no SSR bundling target in `src/emit/` and no bundling path in `fetch.rs`/`config.rs`, so the bundling step the docs place with `mizan-generate` is unsubstantiated.
---
## Unit: AFI conformance (`tests/afi`) ## Unit: AFI conformance (`tests/afi`)
**Charter.** The cross-adapter conformance gate: one fixture registered identically in Django, FastAPI, and a Rust app, asserting all three emit byte-identical KDL. It is the executable form of "the IR is the only contract"; it authors no production mechanism. **Charter.** The cross-adapter conformance gate: one fixture registered identically in Django, FastAPI, and a Rust app, asserting all three emit byte-identical KDL. It is the executable form of "the IR is the only contract"; it authors no production mechanism.

View File

@@ -28,20 +28,21 @@ cores/ shared language-level primitives
mizan-python/ @client decorator, registry, MWT, HMAC cache keys mizan-python/ @client decorator, registry, MWT, HMAC cache keys
mizan-rust/ Rust core — IR build (build_ir()), registry mizan-rust/ Rust core — IR build (build_ir()), registry
mizan-rust-macros/ #[derive(Mizan)] / #[mizan::client] proc-macros mizan-rust-macros/ #[derive(Mizan)] / #[mizan::client] proc-macros
mizan-rust-ssr/ embedded-V8 SSR engine (deno_core + deno_web); evals the
build-time bundle, renders per request; no_rsc guard
protocol/ protocol-level tooling protocol/ protocol-level tooling
mizan-codegen/ codegen — Rust binary (crate `mizan-codegen`); reads KDL IR, mizan-codegen/ codegen — Rust binary (crate `mizan-codegen`); reads KDL IR,
emits typed clients. Targets: stage1, react, vue, svelte, emits typed clients. Targets: stage1, react, vue, svelte,
channels, python, rust. Askama templates under templates/. channels, python, rust. Askama templates under templates/.
mizan-generate/ thin npm-package launcher (bin/launcher.mjs) dispatching to mizan-generate/ thin npm-package launcher (bin/launcher.mjs) dispatching to
the compiled mizan-codegen binary per platform the compiled mizan-codegen binary per platform
workers/ runtime workers / bridges
mizan-ssr/ Bun subprocess used by the Django template backend
``` ```
## Two orthogonal products ## Two orthogonal products
- **RPC** — typed client generation via codegen - **RPC** — typed client generation via codegen
- **SSR** — server rendering via the Bun bridge - **SSR** — server rendering via the embedded-V8 engine in the Rust
binary (see `docs/SSR_ARCHITECTURE.md`)
Independent and composable. Either ships standalone; together they Independent and composable. Either ships standalone; together they
compose. compose.
@@ -68,9 +69,9 @@ contexts, types, and invalidation graph. Every codegen target consumes
KDL. KDL is the contract; everything else (REST envelopes, OpenAPI KDL. KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it. documents, framework idioms) is sediment around it.
The IR must be validated against multiple adapters before it is The IR is validated against multiple adapters — single-adapter
considered stable. Single-adapter validation hides assumptions — validation hides assumptions, and divergence between adapters is what
divergence between adapters is what the IR exists to prevent. the IR exists to prevent.
Forward-direction primitives: Forward-direction primitives:
@@ -85,17 +86,16 @@ Forward-direction primitives:
- `protocol/mizan-codegen/src/fetch.rs` spawns the configured source - `protocol/mizan-codegen/src/fetch.rs` spawns the configured source
command and parses the KDL it writes. command and parses the KDL it writes.
- Codegen reads KDL directly — no OpenAPI envelope, no - Codegen reads KDL directly — no OpenAPI envelope, no
`openapi-typescript`, no per-backend converter divergence. The `openapi-typescript`, no per-backend converter divergence; codegen
former JavaScript/Node two-stage codegen (`openapi-typescript` plus is a single Rust binary.
`.mjs` adapters) has been deleted; codegen is now the single Rust
binary.
- Edge manifest, MWT claims, and other protocol artifacts derive from - Edge manifest, MWT claims, and other protocol artifacts derive from
the same registry/IR. the same registry/IR.
## Launch surface ## Authoring surface
Python (Django) + React. Vue and Svelte ship as v1 alongside React. Python (Django) + React is the reference stack. Vue and Svelte are
TypeScript backend (`mizan-ts`) proves the protocol is portable. co-equal codegen targets over the same kernel. The TypeScript backend
(`mizan-ts`) proves the protocol is portable.
## Why the AFI shape ## Why the AFI shape

View File

@@ -1,10 +1,9 @@
# SSR Architecture # SSR Architecture
*Decided 2026-04-07.* Mizan's SSR adapter is a **Django template backend** rendering through an
**embedded V8 engine inside the Mizan Rust binary** (`cores/mizan-rust-ssr`).
Mizan's SSR adapter is a **Django template backend**. It plugs into No external JS runtime serves requests — node and bun are build-time tools
Django's existing `TEMPLATES` setting, replacing the template only, and no frontend adapter imports an SSR runtime or meta-framework.
rendering engine.
```python ```python
TEMPLATES = [ TEMPLATES = [
@@ -12,53 +11,74 @@ TEMPLATES = [
'BACKEND': 'mizan.ssr.MizanTemplates', 'BACKEND': 'mizan.ssr.MizanTemplates',
'DIRS': [BASE_DIR / 'frontend'], 'DIRS': [BASE_DIR / 'frontend'],
'OPTIONS': { 'OPTIONS': {
'worker': 'path/to/mizan-ssr/src/worker.tsx', 'bundles': BASE_DIR / 'frontend' / '.mizan' / 'ssr',
'timeout': 5, 'timeout': 5,
}, },
} }
] ]
``` ```
Then `render(request, 'components/Hello.tsx', context)` calls the Bun `render(request, 'components/Hello.tsx', context)` renders a React component
subprocess bridge instead of rendering a Django/Jinja2 template. instead of a Django/Jinja2 template. **The template name IS a `.tsx`/`.jsx`
**The template name IS a `.tsx`/`.jsx` file path**, resolved against file path**, resolved against `DIRS`; `get_template` returns a
`DIRS`; `get_template` returns a `MizanTemplate` wrapping the absolute `MizanTemplate` wrapping the resolved component. The context dict becomes
file path. The context dict becomes the component's props (`request` the component's props (`request` and `csrf_token` stripped). Rendered output
and `csrf_token` stripped). Rendered output is wrapped in is wrapped in `<div id="mizan-root">…</div>` plus a
`<div id="mizan-root">…</div>` plus a `<script>window.__MIZAN_SSR_DATA__={sorted-json}</script>` hydration
`<script>window.__MIZAN_SSR_DATA__=…</script>` hydration payload. payload the Mizan kernel hydrates from — server-validated data crossing
one way, as props.
## The engine
`SsrEngine` (`cores/mizan-rust-ssr`) embeds a `deno_core` V8 runtime
composed with `deno_web`, so the web-platform globals react-dom touches
(`TextEncoder`/`TextDecoder`, `MessagePort`, timers) are real
implementations, not shims — a partial polyfill is silent-failure-shaped;
it passes until a render path hits the gap.
- **The bundle is built, the engine evals it once.** `mizan-generate`'s SSR
bundling step compiles each SSR entry component together with
`react-dom/server.browser` into a self-contained bundle that assigns
`globalThis.renderApp`. The engine evals the trusted bundle at
construction and calls `renderApp(props)` per request.
- **Props never enter evaluated source.** Per-render data crosses as a
`v8::json::parse`d value passed as a function argument, so a prop string
has no source to break out of — code injection is structurally absent,
not filtered.
- **One isolate per engine, one engine per worker thread.** V8's Locker
constraint means an engine is not `Send`; the Django side holds one
engine per (worker thread, bundle) pair.
## AFI boundary ## AFI boundary
| Side | Responsibility | | Side | Responsibility |
|---|---| |---|---|
| Backend adapter (`SSRBridge`) | Manages the Bun subprocess lifecycle; gathers props | | Backend adapter (`mizan/ssr`) | Django template-backend interface; resolves the component path to its built bundle; gathers props; wraps output for hydration |
| Bun worker (`worker.tsx`) | `import()`s the file path, `renderToString(createElement(Component, props))` | | Rust binary (`SsrEngine`, via PyO3) | Holds the evaled bundle; parses props to a V8 value; `renderApp(props)` → HTML string |
| stdin/stdout JSON-RPC | Newline-delimited; `{id, method:"render", params:{file, props}}``{id, html}` / `{id, error}`; `ping``{id, pong:true}` | | Build step (`mizan-generate`) | Bundles component + `react-dom/server.browser` into the `bundles` directory; the only place node/bun run |
The Python side binds the engine through PyO3 — in-process FFI, no
subprocess, no JSON-RPC framing. This is the Core-Consolidation shape:
behavior lives in the binary; languages bind to it.
## No framework runtimes
No frontend adapter imports an SSR runtime or meta-framework — Next, Nuxt,
SvelteKit, React Server Components / Flight. Those add the
client-payload-deserialization step behind the pre-auth RCE class
(CVE-2025-55182); the AFI already provides the typed, Pydantic-validated,
one-way version they would otherwise import unsafely.
The guarantee is enforced, not assumed: `cores/mizan-rust-ssr/tests/no_rsc.rs`
scans the authored SSR source and its dependencies for the forbidden token
set (`react-server-dom`, `renderToReadableStream`, `renderToPipeableStream`,
`createFromReadableStream`/`Fetch`, `use server`, `next/`, `nuxt`,
`@sveltejs/kit`) and fails on presence — re-entry is loud.
## Why template backend ## Why template backend
- Django's template system is swappable by design (batteries - Django's template system is swappable by design (batteries included, but
included, but replaceable). replaceable).
- Django developers already use `render(request, template, context)` - Django developers already use `render(request, template, context)` — no
— no new API to learn. new API to learn.
- URL routing, views, middleware, auth — all unchanged. - URL routing, views, middleware, auth — all unchanged.
> A `templatetags/` package exists for a future `{% mizan_render %}`
> convenience tag (base.html shell with Mizan components inside), but
> it is currently empty — no tag is implemented yet.
## Implementation surface
The SSR backend (`mizan/ssr/backend.py`) implements Django's template
backend interface:
- `MizanTemplates(BaseEngine)` — requires `OPTIONS['worker']` (path to
`worker.tsx`); `get_template(name)` resolves a file under `DIRS`
- `MizanTemplate` with `.render(context, request)` → calls the bridge
- `SSRBridge` (`bridge.py`) — spawns `bun run <worker>`, holds the
persistent subprocess, correlates requests by message id, thread-safe,
auto-restarts on crash, waits for the worker's ready signal
Everything Django expects from a template backend, but the actual
rendering routes to Bun.