Cleaned dead code and updated documents

This commit is contained in:
2026-06-04 02:42:13 -04:00
parent 578e124d67
commit ffdf9aa24d
31 changed files with 374 additions and 498 deletions

View File

@@ -17,15 +17,24 @@ backends/ server protocol adapters
mizan-django/ Django adapter
mizan-fastapi/ FastAPI adapter (RPC + context + invalidation; AFI-common scope)
mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic)
frontends/ client kernel + per-framework adapters
mizan-rust-axum/ Rust/Axum adapter (server-side substrate; three-way parity)
mizan-tauri/ Tauri-as-Mizan-backend substrate
frontends/ client kernel + per-framework adapters + transports
mizan-base/ framework-agnostic kernel; owns data, status, error; adapters subscribe
mizan-react/ React contexts + hooks over the kernel
mizan-vue/ Vue composables over the kernel
mizan-svelte/ Svelte stores/runes over the kernel
mizan-vue/ Vue composables over the kernel (codegen target; runtime package unimplemented)
mizan-svelte/ Svelte stores over the kernel (codegen target; runtime package unimplemented)
mizan-rust/ Rust kernel (PyO3 bridge; consumed by the Rust codegen's python target)
mizan-tauri-transport/ Tauri IPC transport for the kernel
mizan-webview-transport/ VSCode-webview transport for the kernel
mizan-webview-channels/ webview channel transport
cores/ shared language-level primitives
mizan-python/ @client decorator, registry, MWT, HMAC cache keys; consumed by both Python backends
mizan-rust/ shared Rust primitives (IR, KDL, registry, graph-check)
mizan-rust-macros/ proc-macros for the Rust backend/kernel
protocol/ protocol-level tooling
mizan-generate/ codegen — fetches schema from any backend, emits typed React/Vue/Svelte client
mizan-codegen/ the codegen — a Rust binary; reads KDL IR, emits typed clients per target
mizan-generate/ thin npm launcher around the compiled mizan-codegen binary
workers/ runtime workers / bridges
mizan-ssr/ Bun subprocess used by the Django template backend
```
@@ -446,22 +455,18 @@ urlpatterns = [
## Codegen — Current State
The codegen is `protocol/mizan-generate/` — framework-agnostic, two-stage. Stage 1 emits the protocol layer (`callXxx` for mutations, `fetchXxx` for context bundles, types). Stage 2 emits per-framework hooks/composables/stores that subscribe to the `mizan-base` kernel.
The codegen is a **Rust binary**, `protocol/mizan-codegen/` (crate `mizan-codegen`). `protocol/mizan-generate/` is a thin npm launcher (`bin/launcher.mjs`) that shells out to the compiled binary. The IR is **KDL** — each backend emits KDL describing its functions/contexts; the binary reads it (`src/fetch.rs`, `src/ir.rs`) and emits per-target output from Askama templates (`templates/`, dispatched in `src/emit/`).
**What's in place:**
Two layers, same as before: a framework-agnostic protocol layer (`callXxx` for mutations, `fetchXxx` for context bundles, types) and a per-framework adapter layer that subscribes to the `mizan-base` kernel.
- Function hooks (`useEcho`, `useUserProfile`, etc.) in the React adapter, subscribing to kernel state via `useSyncExternalStore`
- Context hooks for named contexts and `global`
- Channel hooks for WebSocket transport
- Vue and Svelte equivalents (Stage 2 templates compile, but no live-backend example exercises them — see `ISSUES.md` A4)
**Targets** (`src/emit/`, each byte-checked by a `*_parity.rs` test):
**What's not yet emitted (the wrapper layer):**
- `react` — function/context hooks over `useSyncExternalStore`, plus the full wrapper layer: the `MizanContext` root provider (calls `configure()`, mounts the global context), `useMizan()` imperative escape hatch, and `useMutation`-backed hooks exposing `{ mutate, isPending, error }`.
- `vue`, `svelte` — composables / `readable` stores. Byte-parity-tested, but no runtime adapter package or live-backend example exercises them yet (the `mizan-vue`/`mizan-svelte` packages are unimplemented stubs).
- `channels` — WebSocket transport hooks.
- `stage1` — the framework-agnostic protocol files.
- `python`, `rust` — typed clients for the Python (PyO3) and Rust frontends.
- `<MizanContext>` provider component for React (calls `configure()` and mounts the kernel into the component tree)
- `useMizan()` hook for accessing the kernel from React
- Framework-named error class (e.g. `DjangoError`) wrapping `MizanError` from the kernel
- Vue and Svelte equivalents
The pre-kernel `MizanProvider` in `mizan-react/src/context.tsx` (~750 lines) still ships and is imported by the desktop example; it coexists with the generated `MizanContext`. Forms (`mizan-react/src/forms.ts`) are hand-written and consume the pre-kernel provider — a form codegen target wired to `mizanCall` is still owed. See `ISSUES.md`.
The legacy `MizanProvider` in `mizan-react/src/context.tsx` (~750 lines) and the `forms.ts` consumer (~1163 lines) still depend on the pre-kernel API. They're tracked as `ISSUES.md` A1 and A3. Removing them is gated on the wrapper layer above being emitted.
The SSR pipeline is independent of the codegen — it renders whatever components are registered in the Bun worker.
The SSR pipeline is independent of the codegen the Bun worker resolves a component by **file path** (`import(file)` + `renderToString`), not via a registry.

189
ISSUES.md
View File

@@ -1,178 +1,23 @@
# Mizan — Known Issues
Identified by domain expert review (Cloudflare, Serverless, Vercel, React Query, Django, Laravel, Vue/Svelte).
Status board against the current codebase: Rust codegen (`protocol/mizan-codegen`),
KDL IR, kernel-owned frontend state (`@mizan/base`). Issues that the earlier
expert-review board filed against the deleted JavaScript codegen and the
pre-kernel `mizan-react` provider have been removed — they audited files that
no longer exist.
## Fixed
## Open
- ~~C1~~ Scoped cache purge now passes user_id
- ~~C2~~ initSession retries 3x, resets on failure
- ~~C3~~ SSR backend injects `__MIZAN_SSR_DATA__` script tag
- ~~C4~~ SSR bridge uses _write_lock for stdin
- ~~C5~~ SSR bridge registers atexit handler
- ~~C7~~ View-path mutations now purge origin cache
- ~~H1~~ pendingScoped is Array, not Map (no overwrite)
- ~~H2~~ stableKey() sorts JSON keys (order-independent)
- ~~H3~~ mizanFetch retries 2x on 5xx/network errors
- ~~H4~~ Named contexts skip refetch if SSR data exists
- ~~H6~~ refreshContext uses GET /ctx/ not POST /call/
- ~~H10~~ _meta always fresh dict
- ~~H11~~ Python normalizes True→"true" for cross-language HMAC
- ~~H13~~ isValid checks all required fields are touched
- ~~H14~~ `@client(merge=...)` primitive — kernel splices return value into cached context, no refetch
- ~~H15~~ Schema export handles `BaseModel | None` return types
- ~~H16~~ Generated `react.tsx` imports per-function `*Output` types
- ~~M11~~ execute_function return type includes HttpResponseBase
- ~~M18~~ registerContext cleanup uses ?. (no crash)
- ~~M19~~ `list[BaseModel]` returns reach the wire as bare arrays (RootModel-based rename, no `{result: ...}` wrap)
- ~~M20~~ `initSession()` gated on `configure({session: bool})` (Django default-on, FastAPI opt-out)
- [ ] **Vue / Svelte frontend packages are unimplemented stubs.** `frontends/mizan-vue` and `frontends/mizan-svelte` contain only a `package.json` — no `src/`. The Rust codegen emits Vue composables and Svelte stores (`src/emit/vue.rs`, `src/emit/svelte.rs`, byte-checked by `vue_svelte_parity.rs`), but there is no runtime kernel-adapter package for either and no example app exercises them against a live backend. React is the only frontend with full integration verification.
- [ ] **Svelte adapter emits Svelte 4 stores.** `src/emit/svelte.rs` generates `readable` stores from `svelte/store`. Svelte 5 `$state`/`$derived` runes are the current idiom.
- [ ] **Forms have no codegen target.** `mizan-react/src/forms.ts` (form core hooks) is hand-written and consumed via the pre-kernel `MizanProvider`; the e2e harness has its form fixtures removed. A form codegen target wired to `mizanCall` is owed.
- [ ] **Pre-kernel MizanProvider still shipped.** `mizan-react/src/context.tsx` (~750 lines) is the pre-kernel provider, still imported by the desktop example. It coexists with the codegen-emitted `MizanContext` (which subscribes to `@mizan/base`). Migrating the desktop example onto the generated provider retires it.
- [ ] **Cache module open issues.** See `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`: purge atomicity, cross-language stringification, per-param sub-index cleanup, thundering-herd protection, `cache_get`/`cache_put` argument inconsistency, RedisCache test coverage.
- [ ] **Packages missing a README.** `frontends/mizan-base` (the kernel everything imports), `protocol/mizan-codegen` (the codegen binary), `frontends/mizan-vue`, `frontends/mizan-svelte`, `frontends/mizan-rust`, `backends/mizan-ts`, `backends/mizan-rust-axum`, `cores/mizan-python`.
## Remaining Critical
## Resolved this pass
### C6. No loading/error/stale states in runtime
**File:** `mizan-base/src/index.ts`
The kernel stores only `{params, refetch}`. No `data`, `status`, `error`. Every adapter reinvents loading tracking. Blocks stale-while-revalidate.
## Remaining High
### H5. Mutation hooks expose no loading/error state
**File:** `protocol/mizan-generate/generator/lib/adapters/react.mjs`
Returns bare `useCallback`. No `isPending`, `error`, `isSuccess`.
### H7. Redis SCAN blocks request path at scale
**File:** `mizan-django/src/mizan/cache/backend.py`
Synchronous SCAN at 1M keys: multi-second blocking.
### H8. Svelte codegen uses Svelte 4 stores
**File:** `protocol/mizan-generate/generator/lib/adapters/svelte.mjs`
Should use Svelte 5 `$state`/`$derived` runes.
### H9. Svelte destroy() not auto-called
**File:** `protocol/mizan-generate/generator/lib/adapters/svelte.mjs`
Memory leak if user forgets `onDestroy`.
### H12. Forms triggerValidation captures stale data
**File:** `mizan-react/src/forms.ts`
Debounced validation uses stale closure data.
## Remaining Medium
### M1. SSR bridge not fork-safe
gunicorn prefork shares file descriptors and Redis connections.
### M2. cache_purge_user() not implemented
No way to purge all cache entries for one user.
### M3. No garbage collection for context entries
Runtime `contexts` Map grows monotonically.
### M4. No cross-tab invalidation
No BroadcastChannel. Logout in tab 1 doesn't affect tab 2.
### M5. React 18 Strict Mode double-fetch
useEffect runs twice in dev mode.
### M6. No request deduplication
Two components mounting same context fire parallel fetches.
### M7. SSR worker module cache never invalidates
Dynamic imports cached forever.
### M8. Vue injection key not exported
Can't inject directly without generated composables.
### M9. Vue onMounted won't pre-fetch in Vue SSR
Needs `onServerPrefetch` for Nuxt.
### M10. Svelte should use setContext/getContext
Module-level stores don't scope to component tree.
### M12. render_strategy heuristic uses hardcoded param names
Misses `member_id`, `customer_id`, non-English names.
### M13. initSession called for token-auth requests
Wastes GET /session/ round-trip for JWT/MWT apps.
### M14. Vue watch imported but unused
Params not watched — reactive param changes don't trigger refetch.
### M15. Vue mutation composables misleading `use` prefix
`export const useXxx = callXxx` — not a real composable.
### M16. Svelte mutation imports bypass Stage 1 index
Should import from `'../index'` consistently.
### M17. Side effects in React state updater
Context listeners called inside `setContextStore()` updater.
## Architectural / Cleanup Debt
### A1. Legacy MizanProvider not yet removed
**File:** `mizan-react/src/context.tsx` (~750 lines)
Superseded by the kernel (`mizan-base`) + generated React adapter (`useSyncExternalStore`). Still exported as `MizanProvider`, `useMizan`, `useMizanContext`, etc. Must be deleted or replaced with thin shims that call `configure()` + delegate to the new generated hooks.
### A2. Allauth pending extraction
**File:** `legacy/allauth/` (44 files)
Sitting in `legacy/` since the cleanup pass. Should become its own `mizan-django-allauth` package consuming Mizan's public API. Unblocks v1 mizan-react publishing.
### A3. Forms codegen not adapted to kernel
**File:** `mizan-react/src/forms.ts` (~1163 lines)
Still uses `useMizan().call()` from the legacy MizanProvider. Needs rewrite to use `mizanCall` from the kernel. Currently the only consumer of MizanProvider — blocks A1.
### A4. Codegen for Vue/Svelte not validated end-to-end
The Stage 2 templates produce code that compiles, but no example app exercises Vue or Svelte rendering against a live backend. React is the only adapter with full integration verification.
### A5. ROADMAP.md is stale
**File:** `ROADMAP.md`
Lists SSR Bridge, Edge Manifest, Codegen Rewrite, etc. as "Next" — all are done. Doesn't reflect:
- Two-stage codegen with Vue/Svelte adapters
- C6 kernel-owned state (`ContextState<T>`)
- mizan-ts cross-language adapter
- Cleanup of djarea/Django-specific naming
### A6. CLAUDE.md may also be stale
**File:** `CLAUDE.md`
Written before the kernel rewrite. References to MizanProvider responsibilities and the old codegen pattern are likely outdated. Needs audit.
## Test Coverage Gaps
### T1. No tests for C6 kernel state machine
**File:** `mizan-base/` has no `tests/` directory at all
The state-owning kernel has zero unit tests. No coverage of:
- `registerContext` returning `getState/subscribe/refetch/unregister`
- Status transitions: idle → loading → success/error
- Subscriber notifications on state change
- Refetch reusing the same entry on Strict Mode re-mount
- `unregister` clearing listeners
### T2. No tests for generated Vue adapter output
The `vue.mjs` template produces code, but no test verifies it generates valid Vue 3 composables, that `onServerPrefetch` is wired correctly, or that the kernel subscription bridges to Vue reactivity.
### T3. No tests for generated Svelte adapter output
Same as T2. Readable store factory pattern is unverified against actual Svelte components.
### T4. No tests for view-path cache purge (C7 fix unverified)
The fix added `_purge_cache_for_invalidation()` to the view-path branch, but no test asserts that an `HttpResponse`-returning mutation actually purges the origin cache.
### T5. No tests for SSR thread safety (C4 fix unverified)
The `_write_lock` was added but no concurrent-render test exists to prove it prevents JSON interleaving.
### T6. No tests for SSR atexit cleanup (C5 fix unverified)
`atexit.register(self.shutdown)` was added but not exercised — no test that asserts the Bun process is reaped on Python exit.
### T7. No tests for SSR hydration injection (C3 fix unverified)
The `<script>window.__MIZAN_SSR_DATA__=...</script>` was added to template output but no test asserts it appears in rendered HTML or that the JSON is valid/safe.
### T8. No cross-language HMAC pin test for booleans/None (H11 fix unverified)
Python now normalizes True→"true", but there's no test comparing Python's `derive_cache_key(secret, ctx, {flag: True})` against TypeScript's equivalent to prove they produce identical hex output.
### T9. No tests for retry logic (H3)
`fetchWithRetry` retries 5xx/network errors with backoff. No test for: 5xx triggers retry, 4xx does not, mutation calls bypass retry, max retries respected.
### T10. No end-to-end integration test
Nothing exercises the full pipeline: Django function defined → schema exported → codegen runs → generated React mounts → mutation fires → server response includes invalidate → kernel refetches → DOM updates. Each layer is tested in isolation.
### T11. No tests for `isValid` requiring all required fields touched (H13 fix unverified)
The forms fix checks `field.required && !touched` but no test exercises a form with untouched required fields to confirm `isValid === false`.
### T12. No tests for `_meta` fresh-dict isolation (H10 fix unverified)
The shared-dict fix replaced `{**FunctionWrapper._meta, **meta}` with `{**meta}`. No test confirms that mutating one function's `_meta` doesn't leak into others.
- [x] **Codegen test suite compile break** — every `mizan-codegen` test constructed `SourceConfig` without the `rust`/`script` fields added alongside the Rust-backend work. Suite now compiles and is green.
- [x] **React parity baseline** — the emitter correctly drops the dead `initSession`/`MizanError` top-level imports (they are only re-exported, never used in the module body); baseline regenerated. Fixed the template whitespace artifact that indented the `} from '@mizan/base'` closing brace.
- [x] **Edge manifest non-determinism**`generate_edge_manifest` iterated registration order; now sorts context and mutation keys, so the manifest is deterministic regardless of registration order.
- [x] **Dead code removed**`workers/mizan-ssr/src/test-worker.tsx` (a relic of the rejected `registerComponent` registry), unused TS helpers `isResponseReturn` and `sortedStringify` (mizan-ts), the unused `IndexMap` import (`emit/python.rs`), the dead `debug_expose_names` Django setting, and the dead `package.json` exports + vite aliases (`./client/nextjs`, `./allauth`, `./allauth/nextjs`) pointing at source that does not exist.

View File

@@ -1,14 +1,19 @@
# MIZAN — Named Contexts & Mutation Architecture
> **Historical design spec.** The original named-contexts / mutation design
> document from the January 2025 design conversation. Kept as a record of design
> intent, not as a description of the current build — names and surfaces here
> predate the implementation (the codegen is the Rust binary
> `protocol/mizan-codegen`, never shipped under the working name "Maison"). For
> current architecture, read `CLAUDE.md` (wire protocol, package layout, codegen
> state) and `docs/` (`AFI_ARCHITECTURE.md`, `SSR_ARCHITECTURE.md`,
> `CACHE_KEYING.md`, `MWT_SPEC.md`).
## For Claude Code
This plan was written by Ryth's Claude.ai session after an extended design conversation
reviewing the full codebase, the original @compose discussion from January 2025, and
several rounds of architectural refinement. Treat this as the spec.
The framework formerly called mizan is now called **MIZAN**. Package names, imports,
and references should be updated accordingly. The internal codegen engine is called
**Maison** — it lives inside Mizan and does not need its own public surface.
several rounds of architectural refinement.
---

View File

@@ -4,47 +4,38 @@
### Done
- **`@client` decorator** — `context=`, `affects=`, `auth=`, `websocket=`, `private=`, `route=`, `methods=`, `rev=`, `cache=`
- **`ReactContext` class** — type-safe context/affects references with linting
- **Named contexts** — functions sharing a context name grouped into one provider and one fetch
- **Context bundling endpoint** — `GET /api/mizan/ctx/<name>/` returns all functions in one response
- **Server-driven invalidation (JSON body)** — mutation responses carry `{"result": ..., "invalidate": [...]}`
- **`X-Mizan-Invalidate` header** — second invalidation transport for view-path responses (redirects, HTML)
- **Return-type branching** — data return → RPC path; `HttpResponse` return → view path
- **Scoped invalidation** — `affects_params` lambda; runtime supports `{context, params}` form
- **Auth guards** — `auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable`
- **JWT + session auth** — auto-detected, CSRF handled
- **MWT** — Mizan Web Token for Edge cache keying (separate secret from JWT/cache)
- **Shapes** — Pydantic + django-readers for typed query projections
- **WebSocket channels** — real-time bidirectional communication
- **HMAC cache keying** — origin-side cache with cross-language HMAC conformance (Python + TypeScript pin)
- **Edge manifest** — `python manage.py export_edge_manifest`; both RPC and view-path functions
- **SSR bridge** — Django template backend → persistent Bun subprocess via JSON-RPC
- **`mizan-base` kernel** — framework-agnostic imperative client primitives (data/status/error owned by kernel)
- **Two-stage codegen** — Stage 1 emits framework-agnostic protocol layer; Stage 2 emits per-framework hooks (React, Vue, Svelte)
- **`mizan-ts`** — TypeScript backend adapter; proves the protocol is language-agnostic
- [x] **`@client` decorator** — `context=`, `affects=`, `auth=`, `websocket=`, `private=`, `route=`, `methods=`, `rev=`, `cache=`
- [x] **`ReactContext` class** — type-safe context/affects references with linting
- [x] **Named contexts** — functions sharing a context name grouped into one provider and one fetch
- [x] **Context bundling endpoint**`GET /api/mizan/ctx/<name>/` returns all functions in one response
- [x] **Server-driven invalidation (JSON body)** — mutation responses carry `{"result": ..., "invalidate": [...]}`
- [x] **`X-Mizan-Invalidate` header** — second invalidation transport for view-path responses (redirects, HTML)
- [x] **Return-type branching** — data return → RPC path; `HttpResponse` return → view path
- [x] **Scoped invalidation**`affects_params` lambda; runtime supports `{context, params}` form
- [x] **Auth guards**`auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable`
- [x] **JWT + session auth** — auto-detected, CSRF handled
- [x] **MWT** — Mizan Web Token for Edge cache keying (separate secret from JWT/cache)
- [x] **Shapes** — Pydantic + django-readers for typed query projections
- [x] **WebSocket channels** — real-time bidirectional communication
- [x] **HMAC cache keying** — origin-side cache with cross-language HMAC conformance (Python + TypeScript pin)
- [x] **Edge manifest**`python manage.py export_edge_manifest`; both RPC and view-path functions; deterministic (sorted) output
- [x] **SSR bridge** — Django template backend → persistent Bun subprocess via JSON-RPC; the worker resolves components by file path (`import(file)` + `renderToString`)
- [x] **`mizan-base` kernel** — framework-agnostic imperative client primitives (data/status/error owned by kernel)
- [x] **Rust codegen**`protocol/mizan-codegen`, a Rust binary reading KDL IR and emitting per-target clients (react, vue, svelte, channels, stage1, python, rust), each byte-parity-tested. `mizan-generate` is the thin npm launcher.
- [x] **React wrapper layer**codegen emits the `MizanContext` root provider, `useMizan` escape hatch, and `useMutation`-backed hooks exposing `{ mutate, isPending, error }`
- [x] **Additional backend adapters**`mizan-ts` (TypeScript), `mizan-rust-axum` (Rust/Axum with three-way parity), `mizan-tauri`
- [x] **Frontend transports**`mizan-tauri-transport`, `mizan-webview-transport`, `mizan-webview-channels`
---
### Next (in progress)
### Next
- **React adapter wrapper layer** — codegen emits `MizanContext` provider, `useMizan` hook, `DjangoError` class on top of the `mizan-base` kernel. Equivalent wrapper layers for Vue and Svelte adapters. The harness in `examples/django-react-site` is blocked on this.
- **Legacy `MizanProvider` removal (A1)** — `mizan-react/src/context.tsx` (~750 lines) replaced by codegen-emitted wrappers. Blocks v1 `mizan-react` publishing.
- **Forms migration to kernel (A3)** — `mizan-react/src/forms.ts` (~1163 lines) currently consumes legacy `MizanProvider`. Rewrite to use `mizanCall` from the kernel. Blocks A1.
- **Allauth extraction (A2)** — `legacy/allauth/` becomes `mizan-django-allauth` package consuming Mizan's public API.
- **Vue/Svelte e2e validation (A4)** — example apps exercising a live backend end-to-end, like `examples/django-react-site` does for React.
- **Test coverage gaps** — T1T12 in `ISSUES.md` (kernel state machine, view-path purge, SSR thread safety, retry logic, cross-language HMAC pin, etc.)
---
### Quality
- **H5** — Mutation hooks expose no loading/error state
- **H7** — Redis SCAN blocks request path at scale
- **H8** — Svelte codegen uses Svelte 4 stores; should use Svelte 5 runes
- **H9** — Svelte `destroy()` not auto-called (memory leak)
- **H12** — Forms `triggerValidation` captures stale data
- Medium issues (M1M18) per developer judgment
- [ ] **Vue / Svelte runtime packages**`frontends/mizan-vue` and `frontends/mizan-svelte` are unimplemented stubs. The codegen emits their clients (byte-parity-tested), but a kernel-adapter runtime package and a live-backend example are owed for each.
- [ ] **Svelte 5 runes** — the Svelte target emits Svelte 4 `readable` stores; migrate to `$state`/`$derived`.
- [ ] **Forms codegen target** — emit form clients wired to `mizanCall` from the kernel; retire the hand-written `mizan-react/src/forms.ts` and its dependence on the pre-kernel provider.
- [ ] **Desktop example onto the generated provider**migrate `examples/django-react-desktop-app` off the pre-kernel `MizanProvider` (`mizan-react/src/context.tsx`) so it can be retired.
- [ ] **Cache hardening**purge atomicity, per-param sub-index cleanup, thundering-herd protection, RedisCache coverage (see `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`).
- [ ] **Package READMEs**`mizan-base`, `mizan-codegen`, and the other packages missing one (see `ISSUES.md`).
---

View File

@@ -144,40 +144,33 @@ Frontend gets `useChatChannel({ room })`.
## Generate the frontend
The codegen is `mizan-generate` (in `protocol/mizan-generate/`). From your
frontend project, point a config at the Django backend and run the CLI:
The codegen is the `mizan-generate` Rust binary (source at
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is a thin npm
launcher that dispatches to the platform binary). From your frontend
project, point a `mizan.toml` at the Django backend and run the CLI:
```js
// frontend/django.config.mjs
import path from "path"
import { fileURLToPath } from "url"
```toml
# frontend/mizan.toml
output = "src/api"
targets = ["react"]
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, "..")
[source.django]
manage_path = "../backend/manage.py"
command = ["uv", "run", "python"] # optional — defaults to ["python"]
export default {
source: {
django: {
managePath: path.join(root, "backend/manage.py"),
command: ["uv", "run", "python"],
env: {
PYTHONPATH: path.join(root, "backend"),
DJANGO_SETTINGS_MODULE: "myproject.settings",
},
},
},
output: "src/api",
}
[source.django.env]
PYTHONPATH = "../backend"
DJANGO_SETTINGS_MODULE = "myproject.settings"
```
```bash
npx mizan-generate --config django.config.mjs
mizan-generate --config mizan.toml
```
The codegen drives Django's management command (`export_mizan_schema`) under
the hood, then emits Stage 1 (typed `callXxx`/`fetchXxx` over the runtime
kernel) + Stage 2 (`<MizanContext>` provider, per-context providers,
`use{Hook}()` hooks) into `src/api/`.
The codegen drives Django's management command (`export_mizan_ir`) under
the hood, parses the emitted KDL IR, then emits Stage 1 (typed
`callXxx`/`fetchXxx` over the runtime kernel) + Stage 2 (`<MizanContext>`
provider, per-context providers, `use{Hook}()` hooks) into `src/api/`.
```tsx
// app.tsx

View File

@@ -1,65 +1,40 @@
# Cache Module — Known Issues
Issues identified by 8-domain-expert review. Status tracked here.
Open issues against the current cache implementation. Resolved items are
removed once their fix lands.
## Critical (Security / Data Corruption)
## Correctness
### 1. ~~User-scoped content cached without user_id~~ FIXED
`context_fetch_view` now extracts `user_id` from `request.user.pk` and
passes it to `cache_get`/`cache_put`.
### Purge race condition (non-atomic index operations)
`cache_purge` reads the index and deletes as separate operations. A
concurrent `cache_put` between the two steps can orphan entries. Mitigated
by AND-intersection purge semantics, but full atomicity (Lua script or
`WATCH`/`MULTI` on the Redis backend) is still owed.
### 2. Purge race condition (non-atomic index operations)
`cache_purge` does index reads and deletes as separate operations.
Concurrent `cache_put` between steps can orphan entries.
**Status:** Partially mitigated by AND semantics fix. Full atomicity
(Lua script or WATCH/MULTI) still needed for Redis backend.
### Cross-language stringification divergence
Python `str(True)``"True"` vs JS `String(true)``"true"`. `_normalize`
canonicalizes `True`/`False`/`None` today, but the rules for the remaining
value types are not yet pinned in the protocol spec — so Python and
TypeScript HMAC keys can still diverge on an un-normalized type.
### 3. ~~No Redis error handling~~ FIXED
All cache operations in `executor.py` wrapped in try/except with
`logger.warning`. Redis failure falls through to uncached execution.
## Performance / Operability
### 4. ~~Scoped purge uses OR semantics~~ FIXED
Changed to AND (intersection). `{user_id: 5, org_id: 3}` now only
deletes entries matching BOTH params.
### Broad purge leaves per-param sub-indexes
A broad `cache_purge(context)` deletes the entries but not the per-param
sub-indexes — a slow Redis memory leak.
## High (Correctness / Operability)
### No thundering-herd protection
Concurrent cold misses on the same key all execute and write. No
single-flight / request-coalescing.
### 5. ~~No TTL on Redis entries~~ FIXED
`RedisCache.put` now sets `ex=86400` (24h safety-net TTL) by default.
## API shape
### 6. Cross-language str() vs String() divergence
Python `str(True)` -> `"True"`, JS `String(true)` -> `"true"`.
**Status:** Open. Needs canonical stringification rules in protocol spec.
### cache_get / cache_put argument inconsistency
`cache_get`/`cache_put` take explicit args while the executor resolves some
inputs from module globals — two access patterns for one concern.
### 7. Broad purge doesn't clean per-param sub-indexes
**Status:** Open. Slow memory leak in Redis.
## Coverage
### 8. ~~build_index_keys doesn't stringify values~~ FIXED
Now calls `str(v)` on all values, matching `derive_cache_key`.
### 9. ~~Silent exception swallowing in get_cache()~~ FIXED
Now logs warnings for partial config and connection failures.
### 10. ~~_initialized flag not thread-safe~~ FIXED
Now uses `threading.Lock` for thread-safe initialization.
## Medium (Design / Performance)
### 11. No thundering-herd protection
**Status:** Open. Concurrent cold misses all execute and write.
### 12. ~~Wire-protocol internals in __all__~~ FIXED
`derive_cache_key` and `build_index_keys` removed from `__all__`.
### 13. Inconsistent API pattern
**Status:** Open. `cache_get`/`cache_put` take explicit args but executor
fetches from globals.
### 14. ~~clear() uses SCAN + DELETE without pipeline~~ FIXED
Now uses pipeline with UNLINK for batched async deletes.
### 15. ~~No Redis connection timeouts~~ FIXED
`socket_connect_timeout=5`, `socket_timeout=5`, `health_check_interval=30`.
### 16. No RedisCache test coverage
**Status:** Open. Only MemoryCache is tested.
### RedisCache lacks test coverage
Only `MemoryCache` is exercised by the suite. `RedisCache` (connection
pooling, TTL, SCAN/UNLINK batching, socket timeouts) is untested.

View File

@@ -56,7 +56,7 @@ def generate_edge_manifest(
manifest: dict[str, Any] = {"version": 1, "contexts": {}, "mutations": {}}
for ctx_name, fn_names in groups.items():
for ctx_name, fn_names in sorted(groups.items()):
param_names: set[str] = set()
functions_meta: list[dict[str, Any]] = []
page_routes: list[str] = []
@@ -107,7 +107,7 @@ def generate_edge_manifest(
manifest["contexts"][ctx_name] = ctx_entry
for fn_name, fn_cls in all_functions.items():
for fn_name, fn_cls in sorted(all_functions.items()):
meta = getattr(fn_cls, "_meta", {})
if not meta.get("affects"):
continue

View File

@@ -14,9 +14,6 @@ from django.conf import settings as django_settings
class mizanSettings:
"""mizan configuration."""
# Whether to expose function names in DEBUG mode errors
debug_expose_names: bool
# Cache HMAC signing secret (required when cache is enabled)
cache_secret: str | None
@@ -36,12 +33,10 @@ def get_settings() -> mizanSettings:
Load mizan settings from Django settings.
Settings:
mizan_DEBUG_EXPOSE_NAMES: Show function names in errors when DEBUG=True (default: True)
MIZAN_CACHE_SECRET: HMAC signing key for cache keys (default: None)
MIZAN_CACHE_REDIS_URL: Redis connection URL (default: None)
"""
return mizanSettings(
debug_expose_names=getattr(django_settings, "mizan_DEBUG_EXPOSE_NAMES", True),
cache_secret=getattr(django_settings, "MIZAN_CACHE_SECRET", None),
cache_redis_url=getattr(django_settings, "MIZAN_CACHE_REDIS_URL", None),
mwt_secret=getattr(django_settings, "MIZAN_MWT_SECRET", None),

View File

@@ -108,37 +108,30 @@ anonymous request. The executor branches on those for `auth=True`,
## Generate the frontend
The codegen is `mizan-generate` (in `protocol/mizan-generate/`). Point a
config at your FastAPI app and run the CLI:
The codegen is the `mizan-generate` Rust binary (source at
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is a thin npm
launcher that dispatches to the platform binary). Point a `mizan.toml` at
your FastAPI app and run the CLI:
```js
// frontend/fastapi.config.mjs
import path from "path"
import { fileURLToPath } from "url"
```toml
# frontend/mizan.toml
output = "src/api"
targets = ["react"]
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, "..")
export default {
source: {
fastapi: {
module: "main", // module to import for @client side effects
cwd: path.join(root, "backend"), // python cwd for module resolution
command: ["uv", "run", "python"], // optional — defaults to ["python"]
},
},
output: "src/api",
}
[source.fastapi]
module = "main" # module to import for @client side effects
cwd = "../backend" # python cwd for module resolution
command = ["uv", "run", "python"] # optional — defaults to ["python"]
```
```bash
npx mizan-generate --config fastapi.config.mjs
mizan-generate --config mizan.toml
```
The codegen drives `python -m mizan_fastapi.cli <module>` under the hood,
then emits Stage 1 (typed `callXxx`/`fetchXxx` over the runtime kernel) +
Stage 2 (`<MizanContext>` provider, per-context providers, `use{Hook}()`
hooks) into `src/api/`.
The codegen drives `python -m mizan_fastapi.ir <module>` under the hood,
parses the emitted KDL IR, then emits Stage 1 (typed `callXxx`/`fetchXxx`
over the runtime kernel) + Stage 2 (`<MizanContext>` provider, per-context
providers, `use{Hook}()` hooks) into `src/api/`.
```tsx
// app.tsx
@@ -171,13 +164,13 @@ uv run pytest
For codegen consumption (or any tooling that wants the Mizan schema):
```bash
python -m mizan_fastapi.cli <module>
python -m mizan_fastapi.ir <module>
```
Imports the named module (which must register every `@client` function as
import-time side effects), then prints the OpenAPI schema as JSON to stdout.
Mirrors mizan-django's `manage.py export_mizan_schema` so the codegen
consumes either backend the same subprocess way.
import-time side effects), then prints the Mizan KDL IR to stdout.
Mirrors mizan-django's `manage.py export_mizan_ir` so the codegen consumes
either backend the same subprocess way.
## Architecture

View File

@@ -52,10 +52,6 @@ function extractParams(fn: Function): ParamDef[] {
})
}
function isResponseReturn(result: any): boolean {
return result instanceof Response
}
/**
* Function wrapper — registers a standalone function.
*

View File

@@ -22,10 +22,6 @@ export interface MizanResponse {
headers: Record<string, string>
}
function sortedStringify(data: any): string {
return JSON.stringify(data, Object.keys(data).sort())
}
/**
* Handle GET /api/mizan/ctx/:contextName/
*

54
cores/mizan-rust-macros/Cargo.lock generated Normal file
View File

@@ -0,0 +1,54 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "mizan-macros"
version = "0.1.0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

View File

@@ -11,16 +11,29 @@ Tree organized by role.
backends/ server protocol adapters
mizan-django/ Django adapter
mizan-fastapi/ FastAPI adapter (AFI-common scope)
mizan-rust-axum/ Rust/Axum adapter (handlers, errors, IR export)
mizan-tauri/ Tauri adapter — Mizan calls served in-process
mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic)
frontends/ client kernel + per-framework adapters
mizan-base/ framework-agnostic kernel; owns data, status, error; adapters subscribe
frontends/ client kernel + per-framework adapters + transports
mizan-base/ framework-agnostic kernel (@mizan/base); owns data, status,
error; adapters subscribe through the MizanTransport interface
mizan-react/ React contexts + hooks over the kernel
mizan-vue/ Vue composables over the kernel
mizan-svelte/ Svelte stores/runes over the kernel
mizan-rust/ Rust client kernel
mizan-tauri-transport/ MizanTransport over Tauri IPC
mizan-webview-transport/ MizanTransport over a webview message channel
mizan-webview-channels/ channel transport over a webview bridge
cores/ shared language-level primitives
mizan-python/ @client decorator, registry, MWT, HMAC cache keys
mizan-rust/ Rust core — IR build (build_ir()), registry
mizan-rust-macros/ #[derive(Mizan)] / #[mizan::client] proc-macros
protocol/ protocol-level tooling
mizan-generate/ codegen — schema in, typed client out
mizan-codegen/ codegen — Rust binary (crate `mizan-codegen`); reads KDL IR,
emits typed clients. Targets: stage1, react, vue, svelte,
channels, python, rust. Askama templates under templates/.
mizan-generate/ thin npm-package launcher (bin/launcher.mjs) dispatching to
the compiled mizan-codegen binary per platform
workers/ runtime workers / bridges
mizan-ssr/ Bun subprocess used by the Django template backend
```
@@ -35,11 +48,16 @@ compose.
## Kernel model
The client kernel (`mizan-base`) is the one hard thing. Per-
framework adapters are thin idiomatic wrappers around it. Codegen
emits typed bindings against the framework adapter's surface, not
against the raw kernel — so a React developer gets `useEcho()` and
`<MizanContext>`, a Vue developer gets `useEcho()` composables, a
The client kernel (`@mizan/base`) is the one hard thing. It owns
`ContextState<T> = {data, status, error}`, the context registry
(`registerContext`), `mizanCall` / `mizanFetch`, server-driven `merge`
and `invalidate`, and `initSession`. It reaches the backend through a
pluggable `MizanTransport` (`call` / `fetch`); the default is the
HTTP `httpTransport()`, swapped via `configure({ transport })` for
Tauri / webview hosts. Per-framework adapters are thin idiomatic
wrappers that subscribe to the kernel. Codegen emits typed bindings
against the framework adapter's surface — a React developer gets
`useEcho()` hooks, a Vue developer gets `useEcho()` composables, a
Svelte developer gets readable stores. Same kernel underneath.
## KDL is the IR
@@ -56,20 +74,23 @@ divergence between adapters is what the IR exists to prevent.
Forward-direction primitives:
- `cores/mizan-python` builds the IR from registered functions
(`build_ir()` walks `mizan_core.registry`, emits KDL)
- A `mizan-schema` package (forthcoming) holds the canonical KDL
grammar / type system definition that every adapter targets
- Each backend adapter emits KDL on stdout from an IR-export command:
FastAPI `python -m mizan_fastapi.ir <module>`, Django
`python manage.py export_mizan_ir`, Rust a consumer-side cargo bin
that calls `mizan_core::build_ir()`. Python's `build_ir()` walks
`mizan_core.registry`. The IR grammar (`type` / `function` /
`context` / `channel` nodes) is parsed by `mizan-codegen`'s
`src/ir.rs`; fixtures live at
`protocol/mizan-codegen/tests/fixtures/*.kdl`.
- `protocol/mizan-codegen/src/fetch.rs` spawns the configured source
command and parses the KDL it writes.
- Codegen reads KDL directly — no OpenAPI envelope, no
`openapi-typescript`, no per-backend converter divergence
- Edge manifest, MWT claims, and other protocol artifacts all derive
from the same KDL
**Current implementation is transitional.** Today the codegen consumes
OpenAPI 3.0 (`x-mizan-functions` + `x-mizan-contexts` extensions over
Pydantic→JSON-Schema), produced via Django Ninja or FastAPI's native
generator. That layered indirection is what introduces adapter
divergence (see the AFI conformance suite). KDL-as-IR collapses it.
`openapi-typescript`, no per-backend converter divergence. The
former JavaScript/Node two-stage codegen (`openapi-typescript` plus
`.mjs` adapters) has been deleted; codegen is now the single Rust
binary.
- Edge manifest, MWT claims, and other protocol artifacts derive from
the same registry/IR.
## Launch surface

View File

@@ -16,14 +16,22 @@ standardized replacement exists.
## Resolution: HMAC cache key (JSON-canonical form)
```
HMAC-SHA256(secret, JSON.stringify({
ctx:{context}:HMAC-SHA256(secret, json.dumps({
"c": context,
"p": sorted_params,
"p": sorted_params, // values normalized to JSON-native strings
"r": rev,
"u": user_id // omitted for public content
}, sort_keys=True))
}, sort_keys=True, separators=(",", ":")))
```
`derive_cache_key(secret, context, params, user_id=None, rev=0)`
`"ctx:{context}:{hmac_hex}"`. The `ctx:{context}:` prefix lets broad
purge SCAN by prefix. Param values are normalized for cross-language
consistency (`True``"true"`, `None``"null"`) before stringification.
Implemented in `cores/mizan-python/src/mizan_core/cache/keys.py` and
`backends/mizan-ts/src/cache/keys.ts` (`deriveCacheKey`); pin tests
verify identical output.
### Key derivation rules
- **Public content** — URL path + query params (standard CDN).
@@ -45,20 +53,24 @@ Mizan claims on `X-Mizan-Token` header. Replaces the old
**Not a compiled binary ABI. Not a pluggable Python protocol.**
Each backend adapter (Python, TypeScript, future PHP/C#/Go)
implements the cache protocol in its own language, backed by Redis.
implements the cache protocol in its own language.
**Conformance verified by a shared test suite.**
### Required operations
- `cache_get`
- `cache_put`
- `cache_purge`
- `cache_purge_user`
- `cache_purge` (scoped recomputes the key; broad SCANs the
`ctx:{context}:*` prefix)
### Storage
Redis only. Handles persistence, cross-worker sharing, crash
recovery.
Two backends behind a `CacheBackend` protocol
(`mizan_core/cache/backend.py`):
- `MemoryCache` — dict-based, for testing.
- `RedisCache` — production; persistence, cross-worker sharing, crash
recovery. Broad purge via SCAN, delete via UNLINK.
## Deploy invalidation

View File

@@ -15,8 +15,10 @@ detection.
| `pkey` | Deterministic hash of user's permission state at issuance |
| `exp` | Configurable short TTL — controls permission staleness window (Django setting) |
| `iat` | Issued at |
| `kid` | Key ID — for secret rotation |
| `kid` | Key ID — for secret rotation. Carried in the JOSE header (RFC 7515), not the payload |
| `aud` | Audience binding — prevents cross-tenant replay |
| `nbf` | Not-before — tolerates clock skew |
| `staff` / `super` | `is_staff` / `is_superuser`, used to build `MWTUser` without a DB query |
## Key decisions
@@ -25,10 +27,14 @@ detection.
- **`X-Mizan-Token` header, not `Authorization: Bearer`.** Avoids
collision with DRF, allauth, and existing JWT systems. Cloudflare
WAF/Access do not inspect custom headers.
- **Replaces `JWTUser` + `_try_jwt_auth` entirely.** Old approach is
deleted.
- **`MWTUser`** is a minimal, DB-free request user built from the
token claims (`cores/mizan-python/src/mizan_core/mwt.py`).
> A separate JWT module (`mizan/jwt/`) still exists for standard
> user-auth access/refresh tokens; MWT is the cache-keying identity
> layer, not a replacement for that module.
- **App handles authentication** (session, social, etc.). Mizan
issues MWT *from* the authenticated identity.
issues MWT *from* the authenticated identity
(`create_mwt(user, secret, ttl, audience, kid)`).
- **Edge Worker** validates MWT, extracts `sub` for HMAC cache key,
checks `exp`.
- **`pkey` computation must be deterministic:**
@@ -43,9 +49,11 @@ detection.
JSON with sorted keys:
```
HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "u": user_id}))
ctx:{context}:HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "r": rev, "u": user_id}))
```
See [CACHE_KEYING.md](CACHE_KEYING.md) for the full derivation.
## What this solves
- DRF token collision
@@ -55,5 +63,8 @@ HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "u": user_id}))
## Usage rule
All cache-layer auth code uses MWT, not Django session or raw JWT.
The `@client(auth=...)` parameter gates on MWT validity.
MWT is the identity Edge/cache layers key on. The `@client(auth=...)`
parameter is enforced server-side in `mizan/client/executor.py`
(`_check_auth_requirement`), which checks `request.user` against the
auth requirement (`required` / `staff` / `superuser` / callable);
`request.user` may be an `MWTUser` (stateless) or a session user.

View File

@@ -22,17 +22,16 @@ multi-state privacy. ~$58K legal costs.
TS "Deploy" exists via Workers for Platforms at no additional
compliance cost.
## Free framework: mizan-cache (origin-side cache)
## Free framework: origin-side cache (`mizan.cache`)
Python package implementing the **full cache protocol locally**
same HMAC key derivation, metadata schema, and purge semantics as
Edge.
Shipped in `mizan_core.cache` (re-exported as `mizan.cache` from the
Django adapter) implementing the **full cache protocol locally**
same HMAC key derivation and purge semantics as Edge.
Three backends:
Two backends behind a `CacheBackend` protocol:
- In-memory dict (default)
- Redis
- SQLite
- `MemoryCache` — in-memory dict (testing)
- `RedisCache` — production
### Dual purpose
@@ -44,8 +43,8 @@ Three backends:
## Spec additions
- `@client(cache=False)` — uncacheable; emits `Cache-Control: no-store`.
- Cache ABI: `get(key)`, `put(key, response, metadata)`,
`purge(context, params)`.
- Cache ABI (`mizan.cache`): `cache_get(secret, backend, context, params)`,
`cache_put(...)`, `cache_purge(backend, context, params=…, secret=…)`.
## Launch compliance (Render only)

View File

@@ -14,6 +14,15 @@ Works on a $5 VPS with local Bun. **No Edge required.** PSR is part
of the protocol; it's available to every Mizan deployment regardless
of hosting.
> Current state: the Edge manifest records each context's
> `render_strategy` (`"psr"` for public, `"dynamic_cached"` for
> user-scoped) — see `mizan/export/` and the `export_edge_manifest`
> management command — and the SSR bridge can render a component to
> HTML. The render-on-mutation orchestration that wires those together
> (mutation → trigger local render → store HTML) is not yet present in
> the open-source backends; it is the manifest-driven behavior the
> Edge layer consumes.
## Edge Delivery — Mizan Render (Paid Product)
Pre-rendered HTML cached globally on Cloudflare CDN.

View File

@@ -10,23 +10,31 @@ rendering engine.
TEMPLATES = [
{
'BACKEND': 'mizan.ssr.MizanTemplates',
...
'DIRS': [BASE_DIR / 'frontend'],
'OPTIONS': {
'worker': 'path/to/mizan-ssr/src/worker.tsx',
'timeout': 5,
},
}
]
```
Then `render(request, 'ProfilePage', context)` calls the Bun
Then `render(request, 'components/Hello.tsx', context)` calls the Bun
subprocess bridge instead of rendering a Django/Jinja2 template.
**The component name IS the template name.**
**The template name IS a `.tsx`/`.jsx` file path**, resolved against
`DIRS`; `get_template` returns a `MizanTemplate` wrapping the absolute
file path. The context dict becomes the component's props (`request`
and `csrf_token` stripped). Rendered output is wrapped in
`<div id="mizan-root">…</div>` plus a
`<script>window.__MIZAN_SSR_DATA__=…</script>` hydration payload.
## AFI boundary
| Side | Responsibility |
|---|---|
| Backend adapter | Implements `mizan.ssr()` — executes context functions, gathers data |
| Frontend adapter | Implements `renderToHTML()` — takes component + props, produces HTML |
| Bun subprocess | Hosts the frontend adapter |
| stdin/stdout JSON-RPC | Transport between the two |
| Backend adapter (`SSRBridge`) | Manages the Bun subprocess lifecycle; gathers props |
| Bun worker (`worker.tsx`) | `import()`s the file path, `renderToString(createElement(Component, props))` |
| stdin/stdout JSON-RPC | Newline-delimited; `{id, method:"render", params:{file, props}}``{id, html}` / `{id, error}`; `ping``{id, pong:true}` |
## Why template backend
@@ -35,16 +43,22 @@ subprocess bridge instead of rendering a Django/Jinja2 template.
- Django developers already use `render(request, template, context)`
— no new API to learn.
- URL routing, views, middleware, auth — all unchanged.
- The template tag `{% mizan_render %}` is a convenience for
developers who *also* use Django templates (e.g., a base.html shell
with Mizan components inside).
> 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 bridge module implements Django's template backend interface:
The SSR backend (`mizan/ssr/backend.py`) implements Django's template
backend interface:
- `BaseEngine` subclass
- `Template` class with `.render(context, request)`
- `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.

View File

@@ -10,11 +10,8 @@ export default defineConfig({
alias: {
'mizan/channels': path.join(reactPkg, 'channels/index.ts'),
'mizan/client/react': path.join(reactPkg, 'client/react.ts'),
'mizan/client/nextjs': path.join(reactPkg, 'client/nextjs.tsx'),
'mizan/client': path.join(reactPkg, 'client/index.ts'),
'mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),
'mizan/allauth/nextjs': path.join(reactPkg, 'allauth/nextjs.tsx'),
'mizan/allauth': path.join(reactPkg, 'allauth/index.ts'),
'mizan': path.join(reactPkg, 'index.ts'),
'@rythazhur/mizan/channels': path.join(reactPkg, 'channels/index.ts'),
'@rythazhur/mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),

View File

@@ -12,39 +12,44 @@ npm install @rythazhur/mizan@git+https://git.impactsoundworks.com/isw/mizan.git#
You don't use this package directly. You use the **generated hooks**.
This is the pre-kernel React adapter: it ships its own `MizanProvider`
(`src/context.tsx`) that owns HTTP/WebSocket/CSRF/session/context state
directly, rather than subscribing to the `@mizan/base` kernel. It is still
the provider the Django + desktop example wires against. (`DjangoContext`,
`useDjango`, etc. are deprecated aliases for the `Mizan*` names.)
### 1. Configure
```js
// django.config.mjs
export default {
source: {
django: {
managePath: '../backend/manage.py',
command: ['uv', 'run', 'python'],
},
},
output: 'src/api/generated.ts',
}
```toml
# mizan.toml
output = "src/api"
targets = ["react"]
[source.django]
manage_path = "../backend/manage.py"
command = ["uv", "run", "python"]
```
### 2. Generate
The codegen is the `mizan-generate` Rust binary (source at
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is the npm launcher):
```bash
npx mizan-generate # once
npx mizan-generate --watch # dev mode
mizan-generate --config mizan.toml
```
### 3. Wrap your app
```tsx
import { DjangoContext } from '@/api'
import { MizanProvider } from '@rythazhur/mizan'
<DjangoContext>
<MizanProvider>
<App />
</DjangoContext>
</MizanProvider>
```
`DjangoContext` is the only provider you need. It handles HTTP, WebSocket, CSRF, session init, context auto-fetching, and channel connections.
`MizanProvider` is the only provider you need. It handles HTTP, WebSocket, CSRF, session init, context auto-fetching, and channel connections.
### 4. Use generated hooks
@@ -71,19 +76,22 @@ chat.messages // typed, reactive
## Generated Files
The Rust codegen emits per-target files into the configured `output`
directory (Stage 1 is auto-included whenever `react` is a target):
| File | Contents |
|------|----------|
| `generated.django.tsx` | `DjangoContext` + typed hooks |
| `generated.mizan.ts` | Pydantic types |
| `generated.forms.ts` | Form hooks with Zod |
| `generated.channels.hooks.tsx` | Channel hooks |
| `index.ts` | Re-exports everything |
| `types.ts` | Pydantic types |
| `contexts/<name>.ts` | Per-context `fetchXxx` bundles |
| `react.tsx` | `<MizanContext>` provider + typed `use{Hook}()` hooks |
| `channels.ts` / `channels.hooks.tsx` | Channel types + hooks (when the schema carries channels) |
| `index.ts` | Stage 1 re-export root |
## Sub-exports
| Import | When to use |
|--------|------------|
| `@rythazhur/mizan` | Core: mizanProvider, hooks, forms, errors |
| `@rythazhur/mizan` | Core: `MizanProvider`, hooks, forms, errors |
| `@rythazhur/mizan/channels` | WebSocket channels |
| `@rythazhur/mizan/jwt` | JWT token management |
| `@rythazhur/mizan/client` | HTTP clients (CSR/SSR) |

View File

@@ -17,10 +17,6 @@
"types": "./dist/client/react.d.ts",
"import": "./dist/client/react.js"
},
"./client/nextjs": {
"types": "./dist/client/nextjs.d.ts",
"import": "./dist/client/nextjs.js"
},
"./channels": {
"types": "./dist/channels/index.d.ts",
"import": "./dist/channels/index.js"
@@ -28,14 +24,6 @@
"./jwt": {
"types": "./dist/jwt/index.d.ts",
"import": "./dist/jwt/index.js"
},
"./allauth": {
"types": "./dist/allauth/index.d.ts",
"import": "./dist/allauth/index.js"
},
"./allauth/nextjs": {
"types": "./dist/allauth/nextjs.d.ts",
"import": "./dist/allauth/nextjs.js"
}
},
"scripts": {

View File

@@ -8,7 +8,6 @@
use std::path::PathBuf;
use askama::Template;
use indexmap::IndexMap;
use crate::config::Config;
use crate::emit::CodegenTarget;

View File

@@ -18,7 +18,7 @@ fn fixture_config() -> Config {
project_id: None,
output: PathBuf::from("/tmp"),
targets: vec!["channels".to_string()],
source: SourceConfig { fastapi: None, django: None },
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
rust_kernel: None,
rust_crate_name: None,
}

View File

@@ -14,10 +14,8 @@ import {
} from 'react'
import {
configure,
initSession,
mizanCall,
mizanFetch,
MizanError,
registerContext,
type ContextState,
} from '@mizan/base'
@@ -45,6 +43,7 @@ function useContextSubscription<T>(
return useSyncExternalStore(handle.subscribe, handle.getState, handle.getState)
}
// Internal — wraps an imperative call() with isPending / error state.
interface MutationHook<TArgs, TResult> {
mutate: (args: TArgs) => Promise<TResult>

View File

@@ -20,7 +20,7 @@ fn fixture_config() -> Config {
project_id: None,
output: PathBuf::from("/tmp"),
targets: vec!["python".to_string()],
source: SourceConfig { fastapi: None, django: None },
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
rust_kernel: None,
rust_crate_name: None,
}

View File

@@ -19,7 +19,7 @@ fn fixture_config() -> Config {
project_id: None,
output: PathBuf::from("/tmp"),
targets: vec!["react".to_string()],
source: SourceConfig { fastapi: None, django: None },
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
rust_kernel: None,
rust_crate_name: None,
}

View File

@@ -24,7 +24,7 @@ fn fixture_config() -> Config {
project_id: None,
output: PathBuf::from("/tmp"),
targets: vec!["rust".to_string()],
source: SourceConfig { fastapi: None, django: None },
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
rust_kernel: Some(RustKernelSpec::Path {
path: "../../../frontends/mizan-rust".to_string(),
}),

View File

@@ -30,7 +30,7 @@ fn synthetic_config() -> Config {
project_id: None,
output: PathBuf::from("/tmp"),
targets: vec!["stage1".to_string()],
source: SourceConfig { fastapi: None, django: None },
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
rust_kernel: None,
rust_crate_name: None,
}

View File

@@ -20,7 +20,7 @@ fn fixture_config(target: &str) -> Config {
project_id: None,
output: PathBuf::from("/tmp"),
targets: vec![target.to_string()],
source: SourceConfig { fastapi: None, django: None },
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
rust_kernel: None,
rust_crate_name: None,
}

View File

@@ -1,29 +0,0 @@
/**
* Test SSR worker — registers simple components for the test suite.
*/
import { registerComponent } from './worker'
// Simple component that renders props
function Hello({ name }: { name: string }) {
return <div>Hello, {name}!</div>
}
// Component that renders a list
function UserProfile({ user_id, name }: { user_id: number; name: string }) {
return (
<div className="profile">
<h1>{name}</h1>
<span>ID: {user_id}</span>
</div>
)
}
// Component that throws during render
function Broken() {
throw new Error('Intentional render error')
}
registerComponent('Hello', Hello)
registerComponent('UserProfile', UserProfile)
registerComponent('Broken', Broken)