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-django/ Django adapter
mizan-fastapi/ FastAPI adapter (RPC + context + invalidation; AFI-common scope) mizan-fastapi/ FastAPI adapter (RPC + context + invalidation; AFI-common scope)
mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic) 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-base/ framework-agnostic kernel; owns data, status, error; adapters subscribe
mizan-react/ React contexts + hooks over the kernel mizan-react/ React contexts + hooks over the kernel
mizan-vue/ Vue composables over the kernel mizan-vue/ Vue composables over the kernel (codegen target; runtime package unimplemented)
mizan-svelte/ Svelte stores/runes over the kernel 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 cores/ shared language-level primitives
mizan-python/ @client decorator, registry, MWT, HMAC cache keys; consumed by both Python backends 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 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 workers/ runtime workers / bridges
mizan-ssr/ Bun subprocess used by the Django template backend mizan-ssr/ Bun subprocess used by the Django template backend
``` ```
@@ -446,22 +455,18 @@ urlpatterns = [
## Codegen — Current State ## 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` **Targets** (`src/emit/`, each byte-checked by a `*_parity.rs` test):
- 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)
**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) 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`.
- `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 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 the Bun worker resolves a component by **file path** (`import(file)` + `renderToString`), not via a registry.
The SSR pipeline is independent of the codegen — it renders whatever components are registered in the Bun worker.

189
ISSUES.md
View File

@@ -1,178 +1,23 @@
# Mizan — Known Issues # 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 - [ ] **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.
- ~~C2~~ initSession retries 3x, resets on failure - [ ] **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.
- ~~C3~~ SSR backend injects `__MIZAN_SSR_DATA__` script tag - [ ] **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.
- ~~C4~~ SSR bridge uses _write_lock for stdin - [ ] **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.
- ~~C5~~ SSR bridge registers atexit handler - [ ] **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.
- ~~C7~~ View-path mutations now purge origin cache - [ ] **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`.
- ~~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)
## Remaining Critical ## Resolved this pass
### C6. No loading/error/stale states in runtime - [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.
**File:** `mizan-base/src/index.ts` - [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.
The kernel stores only `{params, refetch}`. No `data`, `status`, `error`. Every adapter reinvents loading tracking. Blocks stale-while-revalidate. - [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.
## 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.

View File

@@ -1,14 +1,19 @@
# MIZAN — Named Contexts & Mutation Architecture # 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 ## For Claude Code
This plan was written by Ryth's Claude.ai session after an extended design conversation 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 reviewing the full codebase, the original @compose discussion from January 2025, and
several rounds of architectural refinement. Treat this as the spec. several rounds of architectural refinement.
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.
--- ---

View File

@@ -4,47 +4,38 @@
### Done ### Done
- **`@client` decorator** — `context=`, `affects=`, `auth=`, `websocket=`, `private=`, `route=`, `methods=`, `rev=`, `cache=` - [x] **`@client` decorator** — `context=`, `affects=`, `auth=`, `websocket=`, `private=`, `route=`, `methods=`, `rev=`, `cache=`
- **`ReactContext` class** — type-safe context/affects references with linting - [x] **`ReactContext` class** — type-safe context/affects references with linting
- **Named contexts** — functions sharing a context name grouped into one provider and one fetch - [x] **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 - [x] **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] **Server-driven invalidation (JSON body)** — mutation responses carry `{"result": ..., "invalidate": [...]}`
- **`X-Mizan-Invalidate` header** — second invalidation transport for view-path responses (redirects, HTML) - [x] **`X-Mizan-Invalidate` header** — second invalidation transport for view-path responses (redirects, HTML)
- **Return-type branching** — data return → RPC path; `HttpResponse` return → view path - [x] **Return-type branching** — data return → RPC path; `HttpResponse` return → view path
- **Scoped invalidation** — `affects_params` lambda; runtime supports `{context, params}` form - [x] **Scoped invalidation**`affects_params` lambda; runtime supports `{context, params}` form
- **Auth guards** — `auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable` - [x] **Auth guards**`auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable`
- **JWT + session auth** — auto-detected, CSRF handled - [x] **JWT + session auth** — auto-detected, CSRF handled
- **MWT** — Mizan Web Token for Edge cache keying (separate secret from JWT/cache) - [x] **MWT** — Mizan Web Token for Edge cache keying (separate secret from JWT/cache)
- **Shapes** — Pydantic + django-readers for typed query projections - [x] **Shapes** — Pydantic + django-readers for typed query projections
- **WebSocket channels** — real-time bidirectional communication - [x] **WebSocket channels** — real-time bidirectional communication
- **HMAC cache keying** — origin-side cache with cross-language HMAC conformance (Python + TypeScript pin) - [x] **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 - [x] **Edge manifest**`python manage.py export_edge_manifest`; both RPC and view-path functions; deterministic (sorted) output
- **SSR bridge** — Django template backend → persistent Bun subprocess via JSON-RPC - [x] **SSR bridge** — Django template backend → persistent Bun subprocess via JSON-RPC; the worker resolves components by file path (`import(file)` + `renderToString`)
- **`mizan-base` kernel** — framework-agnostic imperative client primitives (data/status/error owned by kernel) - [x] **`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) - [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.
- **`mizan-ts`** — TypeScript backend adapter; proves the protocol is language-agnostic - [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. - [ ] **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.
- **Legacy `MizanProvider` removal (A1)** — `mizan-react/src/context.tsx` (~750 lines) replaced by codegen-emitted wrappers. Blocks v1 `mizan-react` publishing. - [ ] **Svelte 5 runes** — the Svelte target emits Svelte 4 `readable` stores; migrate to `$state`/`$derived`.
- **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. - [ ] **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.
- **Allauth extraction (A2)** — `legacy/allauth/` becomes `mizan-django-allauth` package consuming Mizan's public API. - [ ] **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.
- **Vue/Svelte e2e validation (A4)** — example apps exercising a live backend end-to-end, like `examples/django-react-site` does for React. - [ ] **Cache hardening**purge atomicity, per-param sub-index cleanup, thundering-herd protection, RedisCache coverage (see `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`).
- **Test coverage gaps** — T1T12 in `ISSUES.md` (kernel state machine, view-path purge, SSR thread safety, retry logic, cross-language HMAC pin, etc.) - [ ] **Package READMEs**`mizan-base`, `mizan-codegen`, and the other packages missing one (see `ISSUES.md`).
---
### 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
--- ---

View File

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

View File

@@ -1,65 +1,40 @@
# Cache Module — Known Issues # 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 ### Purge race condition (non-atomic index operations)
`context_fetch_view` now extracts `user_id` from `request.user.pk` and `cache_purge` reads the index and deletes as separate operations. A
passes it to `cache_get`/`cache_put`. 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) ### Cross-language stringification divergence
`cache_purge` does index reads and deletes as separate operations. Python `str(True)``"True"` vs JS `String(true)``"true"`. `_normalize`
Concurrent `cache_put` between steps can orphan entries. canonicalizes `True`/`False`/`None` today, but the rules for the remaining
**Status:** Partially mitigated by AND semantics fix. Full atomicity value types are not yet pinned in the protocol spec — so Python and
(Lua script or WATCH/MULTI) still needed for Redis backend. TypeScript HMAC keys can still diverge on an un-normalized type.
### 3. ~~No Redis error handling~~ FIXED ## Performance / Operability
All cache operations in `executor.py` wrapped in try/except with
`logger.warning`. Redis failure falls through to uncached execution.
### 4. ~~Scoped purge uses OR semantics~~ FIXED ### Broad purge leaves per-param sub-indexes
Changed to AND (intersection). `{user_id: 5, org_id: 3}` now only A broad `cache_purge(context)` deletes the entries but not the per-param
deletes entries matching BOTH params. 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 ## API shape
`RedisCache.put` now sets `ex=86400` (24h safety-net TTL) by default.
### 6. Cross-language str() vs String() divergence ### cache_get / cache_put argument inconsistency
Python `str(True)` -> `"True"`, JS `String(true)` -> `"true"`. `cache_get`/`cache_put` take explicit args while the executor resolves some
**Status:** Open. Needs canonical stringification rules in protocol spec. inputs from module globals — two access patterns for one concern.
### 7. Broad purge doesn't clean per-param sub-indexes ## Coverage
**Status:** Open. Slow memory leak in Redis.
### 8. ~~build_index_keys doesn't stringify values~~ FIXED ### RedisCache lacks test coverage
Now calls `str(v)` on all values, matching `derive_cache_key`. Only `MemoryCache` is exercised by the suite. `RedisCache` (connection
pooling, TTL, SCAN/UNLINK batching, socket timeouts) is untested.
### 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.

View File

@@ -56,7 +56,7 @@ def generate_edge_manifest(
manifest: dict[str, Any] = {"version": 1, "contexts": {}, "mutations": {}} 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() param_names: set[str] = set()
functions_meta: list[dict[str, Any]] = [] functions_meta: list[dict[str, Any]] = []
page_routes: list[str] = [] page_routes: list[str] = []
@@ -107,7 +107,7 @@ def generate_edge_manifest(
manifest["contexts"][ctx_name] = ctx_entry 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", {}) meta = getattr(fn_cls, "_meta", {})
if not meta.get("affects"): if not meta.get("affects"):
continue continue

View File

@@ -14,9 +14,6 @@ from django.conf import settings as django_settings
class mizanSettings: class mizanSettings:
"""mizan configuration.""" """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 HMAC signing secret (required when cache is enabled)
cache_secret: str | None cache_secret: str | None
@@ -36,12 +33,10 @@ def get_settings() -> mizanSettings:
Load mizan settings from Django settings. Load mizan settings from Django settings.
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_SECRET: HMAC signing key for cache keys (default: None)
MIZAN_CACHE_REDIS_URL: Redis connection URL (default: None) MIZAN_CACHE_REDIS_URL: Redis connection URL (default: None)
""" """
return mizanSettings( return mizanSettings(
debug_expose_names=getattr(django_settings, "mizan_DEBUG_EXPOSE_NAMES", True),
cache_secret=getattr(django_settings, "MIZAN_CACHE_SECRET", None), cache_secret=getattr(django_settings, "MIZAN_CACHE_SECRET", None),
cache_redis_url=getattr(django_settings, "MIZAN_CACHE_REDIS_URL", None), cache_redis_url=getattr(django_settings, "MIZAN_CACHE_REDIS_URL", None),
mwt_secret=getattr(django_settings, "MIZAN_MWT_SECRET", 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 ## Generate the frontend
The codegen is `mizan-generate` (in `protocol/mizan-generate/`). Point a The codegen is the `mizan-generate` Rust binary (source at
config at your FastAPI app and run the CLI: `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 ```toml
// frontend/fastapi.config.mjs # frontend/mizan.toml
import path from "path" output = "src/api"
import { fileURLToPath } from "url" targets = ["react"]
const __dirname = path.dirname(fileURLToPath(import.meta.url)) [source.fastapi]
const root = path.resolve(__dirname, "..") module = "main" # module to import for @client side effects
cwd = "../backend" # python cwd for module resolution
export default { command = ["uv", "run", "python"] # optional — defaults to ["python"]
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",
}
``` ```
```bash ```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, The codegen drives `python -m mizan_fastapi.ir <module>` under the hood,
then emits Stage 1 (typed `callXxx`/`fetchXxx` over the runtime kernel) + parses the emitted KDL IR, then emits Stage 1 (typed `callXxx`/`fetchXxx`
Stage 2 (`<MizanContext>` provider, per-context providers, `use{Hook}()` over the runtime kernel) + Stage 2 (`<MizanContext>` provider, per-context
hooks) into `src/api/`. providers, `use{Hook}()` hooks) into `src/api/`.
```tsx ```tsx
// app.tsx // app.tsx
@@ -171,13 +164,13 @@ uv run pytest
For codegen consumption (or any tooling that wants the Mizan schema): For codegen consumption (or any tooling that wants the Mizan schema):
```bash ```bash
python -m mizan_fastapi.cli <module> python -m mizan_fastapi.ir <module>
``` ```
Imports the named module (which must register every `@client` function as Imports the named module (which must register every `@client` function as
import-time side effects), then prints the OpenAPI schema as JSON to stdout. import-time side effects), then prints the Mizan KDL IR to stdout.
Mirrors mizan-django's `manage.py export_mizan_schema` so the codegen Mirrors mizan-django's `manage.py export_mizan_ir` so the codegen consumes
consumes either backend the same subprocess way. either backend the same subprocess way.
## Architecture ## 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. * Function wrapper — registers a standalone function.
* *

View File

@@ -22,10 +22,6 @@ export interface MizanResponse {
headers: Record<string, string> headers: Record<string, string>
} }
function sortedStringify(data: any): string {
return JSON.stringify(data, Object.keys(data).sort())
}
/** /**
* Handle GET /api/mizan/ctx/:contextName/ * 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 backends/ server protocol adapters
mizan-django/ Django adapter mizan-django/ Django adapter
mizan-fastapi/ FastAPI adapter (AFI-common scope) 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) mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic)
frontends/ client kernel + per-framework adapters frontends/ client kernel + per-framework adapters + transports
mizan-base/ framework-agnostic kernel; owns data, status, error; adapters subscribe 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-react/ React contexts + hooks over the kernel
mizan-vue/ Vue composables over the kernel mizan-vue/ Vue composables over the kernel
mizan-svelte/ Svelte stores/runes 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 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-macros/ #[derive(Mizan)] / #[mizan::client] proc-macros
protocol/ protocol-level tooling 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 workers/ runtime workers / bridges
mizan-ssr/ Bun subprocess used by the Django template backend mizan-ssr/ Bun subprocess used by the Django template backend
``` ```
@@ -35,11 +48,16 @@ compose.
## Kernel model ## Kernel model
The client kernel (`mizan-base`) is the one hard thing. Per- The client kernel (`@mizan/base`) is the one hard thing. It owns
framework adapters are thin idiomatic wrappers around it. Codegen `ContextState<T> = {data, status, error}`, the context registry
emits typed bindings against the framework adapter's surface, not (`registerContext`), `mizanCall` / `mizanFetch`, server-driven `merge`
against the raw kernel — so a React developer gets `useEcho()` and and `invalidate`, and `initSession`. It reaches the backend through a
`<MizanContext>`, a Vue developer gets `useEcho()` composables, 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. Svelte developer gets readable stores. Same kernel underneath.
## KDL is the IR ## KDL is the IR
@@ -56,20 +74,23 @@ divergence between adapters is what the IR exists to prevent.
Forward-direction primitives: Forward-direction primitives:
- `cores/mizan-python` builds the IR from registered functions - Each backend adapter emits KDL on stdout from an IR-export command:
(`build_ir()` walks `mizan_core.registry`, emits KDL) FastAPI `python -m mizan_fastapi.ir <module>`, Django
- A `mizan-schema` package (forthcoming) holds the canonical KDL `python manage.py export_mizan_ir`, Rust a consumer-side cargo bin
grammar / type system definition that every adapter targets 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 - Codegen reads KDL directly — no OpenAPI envelope, no
`openapi-typescript`, no per-backend converter divergence `openapi-typescript`, no per-backend converter divergence. The
- Edge manifest, MWT claims, and other protocol artifacts all derive former JavaScript/Node two-stage codegen (`openapi-typescript` plus
from the same KDL `.mjs` adapters) has been deleted; codegen is now the single Rust
binary.
**Current implementation is transitional.** Today the codegen consumes - Edge manifest, MWT claims, and other protocol artifacts derive from
OpenAPI 3.0 (`x-mizan-functions` + `x-mizan-contexts` extensions over the same registry/IR.
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.
## Launch surface ## Launch surface

View File

@@ -16,14 +16,22 @@ standardized replacement exists.
## Resolution: HMAC cache key (JSON-canonical form) ## Resolution: HMAC cache key (JSON-canonical form)
``` ```
HMAC-SHA256(secret, JSON.stringify({ ctx:{context}:HMAC-SHA256(secret, json.dumps({
"c": context, "c": context,
"p": sorted_params, "p": sorted_params, // values normalized to JSON-native strings
"r": rev, "r": rev,
"u": user_id // omitted for public content "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 ### Key derivation rules
- **Public content** — URL path + query params (standard CDN). - **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.** **Not a compiled binary ABI. Not a pluggable Python protocol.**
Each backend adapter (Python, TypeScript, future PHP/C#/Go) 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.** **Conformance verified by a shared test suite.**
### Required operations ### Required operations
- `cache_get` - `cache_get`
- `cache_put` - `cache_put`
- `cache_purge` - `cache_purge` (scoped recomputes the key; broad SCANs the
- `cache_purge_user` `ctx:{context}:*` prefix)
### Storage ### Storage
Redis only. Handles persistence, cross-worker sharing, crash Two backends behind a `CacheBackend` protocol
recovery. (`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 ## Deploy invalidation

View File

@@ -15,8 +15,10 @@ detection.
| `pkey` | Deterministic hash of user's permission state at issuance | | `pkey` | Deterministic hash of user's permission state at issuance |
| `exp` | Configurable short TTL — controls permission staleness window (Django setting) | | `exp` | Configurable short TTL — controls permission staleness window (Django setting) |
| `iat` | Issued at | | `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 | | `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 ## Key decisions
@@ -25,10 +27,14 @@ detection.
- **`X-Mizan-Token` header, not `Authorization: Bearer`.** Avoids - **`X-Mizan-Token` header, not `Authorization: Bearer`.** Avoids
collision with DRF, allauth, and existing JWT systems. Cloudflare collision with DRF, allauth, and existing JWT systems. Cloudflare
WAF/Access do not inspect custom headers. WAF/Access do not inspect custom headers.
- **Replaces `JWTUser` + `_try_jwt_auth` entirely.** Old approach is - **`MWTUser`** is a minimal, DB-free request user built from the
deleted. 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 - **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, - **Edge Worker** validates MWT, extracts `sub` for HMAC cache key,
checks `exp`. checks `exp`.
- **`pkey` computation must be deterministic:** - **`pkey` computation must be deterministic:**
@@ -43,9 +49,11 @@ detection.
JSON with sorted keys: 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 ## What this solves
- DRF token collision - DRF token collision
@@ -55,5 +63,8 @@ HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "u": user_id}))
## Usage rule ## Usage rule
All cache-layer auth code uses MWT, not Django session or raw JWT. MWT is the identity Edge/cache layers key on. The `@client(auth=...)`
The `@client(auth=...)` parameter gates on MWT validity. 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 TS "Deploy" exists via Workers for Platforms at no additional
compliance cost. compliance cost.
## Free framework: mizan-cache (origin-side cache) ## Free framework: origin-side cache (`mizan.cache`)
Python package implementing the **full cache protocol locally** Shipped in `mizan_core.cache` (re-exported as `mizan.cache` from the
same HMAC key derivation, metadata schema, and purge semantics as Django adapter) implementing the **full cache protocol locally**
Edge. same HMAC key derivation and purge semantics as Edge.
Three backends: Two backends behind a `CacheBackend` protocol:
- In-memory dict (default) - `MemoryCache` — in-memory dict (testing)
- Redis - `RedisCache` — production
- SQLite
### Dual purpose ### Dual purpose
@@ -44,8 +43,8 @@ Three backends:
## Spec additions ## Spec additions
- `@client(cache=False)` — uncacheable; emits `Cache-Control: no-store`. - `@client(cache=False)` — uncacheable; emits `Cache-Control: no-store`.
- Cache ABI: `get(key)`, `put(key, response, metadata)`, - Cache ABI (`mizan.cache`): `cache_get(secret, backend, context, params)`,
`purge(context, params)`. `cache_put(...)`, `cache_purge(backend, context, params=…, secret=…)`.
## Launch compliance (Render only) ## 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 the protocol; it's available to every Mizan deployment regardless
of hosting. 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) ## Edge Delivery — Mizan Render (Paid Product)
Pre-rendered HTML cached globally on Cloudflare CDN. Pre-rendered HTML cached globally on Cloudflare CDN.

View File

@@ -10,23 +10,31 @@ rendering engine.
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'mizan.ssr.MizanTemplates', '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. 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 ## AFI boundary
| Side | Responsibility | | Side | Responsibility |
|---|---| |---|---|
| Backend adapter | Implements `mizan.ssr()` — executes context functions, gathers data | | Backend adapter (`SSRBridge`) | Manages the Bun subprocess lifecycle; gathers props |
| Frontend adapter | Implements `renderToHTML()` — takes component + props, produces HTML | | Bun worker (`worker.tsx`) | `import()`s the file path, `renderToString(createElement(Component, props))` |
| Bun subprocess | Hosts the frontend adapter | | stdin/stdout JSON-RPC | Newline-delimited; `{id, method:"render", params:{file, props}}``{id, html}` / `{id, error}`; `ping``{id, pong:true}` |
| stdin/stdout JSON-RPC | Transport between the two |
## Why template backend ## Why template backend
@@ -35,16 +43,22 @@ subprocess bridge instead of rendering a Django/Jinja2 template.
- Django developers already use `render(request, template, context)` - Django developers already use `render(request, template, context)`
— no new API to learn. — no new API to learn.
- URL routing, views, middleware, auth — all unchanged. - 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 > A `templatetags/` package exists for a future `{% mizan_render %}`
with Mizan components inside). > convenience tag (base.html shell with Mizan components inside), but
> it is currently empty — no tag is implemented yet.
## Implementation surface ## 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 - `MizanTemplates(BaseEngine)` — requires `OPTIONS['worker']` (path to
- `Template` class with `.render(context, request)` `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 Everything Django expects from a template backend, but the actual
rendering routes to Bun. rendering routes to Bun.

View File

@@ -10,11 +10,8 @@ export default defineConfig({
alias: { alias: {
'mizan/channels': path.join(reactPkg, 'channels/index.ts'), 'mizan/channels': path.join(reactPkg, 'channels/index.ts'),
'mizan/client/react': path.join(reactPkg, 'client/react.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/client': path.join(reactPkg, 'client/index.ts'),
'mizan/jwt': path.join(reactPkg, 'jwt/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'), 'mizan': path.join(reactPkg, 'index.ts'),
'@rythazhur/mizan/channels': path.join(reactPkg, 'channels/index.ts'), '@rythazhur/mizan/channels': path.join(reactPkg, 'channels/index.ts'),
'@rythazhur/mizan/jwt': path.join(reactPkg, 'jwt/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**. 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 ### 1. Configure
```js ```toml
// django.config.mjs # mizan.toml
export default { output = "src/api"
source: { targets = ["react"]
django: {
managePath: '../backend/manage.py', [source.django]
command: ['uv', 'run', 'python'], manage_path = "../backend/manage.py"
}, command = ["uv", "run", "python"]
},
output: 'src/api/generated.ts',
}
``` ```
### 2. Generate ### 2. Generate
The codegen is the `mizan-generate` Rust binary (source at
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is the npm launcher):
```bash ```bash
npx mizan-generate # once mizan-generate --config mizan.toml
npx mizan-generate --watch # dev mode
``` ```
### 3. Wrap your app ### 3. Wrap your app
```tsx ```tsx
import { DjangoContext } from '@/api' import { MizanProvider } from '@rythazhur/mizan'
<DjangoContext> <MizanProvider>
<App /> <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 ### 4. Use generated hooks
@@ -71,19 +76,22 @@ chat.messages // typed, reactive
## Generated Files ## 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 | | File | Contents |
|------|----------| |------|----------|
| `generated.django.tsx` | `DjangoContext` + typed hooks | | `types.ts` | Pydantic types |
| `generated.mizan.ts` | Pydantic types | | `contexts/<name>.ts` | Per-context `fetchXxx` bundles |
| `generated.forms.ts` | Form hooks with Zod | | `react.tsx` | `<MizanContext>` provider + typed `use{Hook}()` hooks |
| `generated.channels.hooks.tsx` | Channel hooks | | `channels.ts` / `channels.hooks.tsx` | Channel types + hooks (when the schema carries channels) |
| `index.ts` | Re-exports everything | | `index.ts` | Stage 1 re-export root |
## Sub-exports ## Sub-exports
| Import | When to use | | 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/channels` | WebSocket channels |
| `@rythazhur/mizan/jwt` | JWT token management | | `@rythazhur/mizan/jwt` | JWT token management |
| `@rythazhur/mizan/client` | HTTP clients (CSR/SSR) | | `@rythazhur/mizan/client` | HTTP clients (CSR/SSR) |

View File

@@ -17,10 +17,6 @@
"types": "./dist/client/react.d.ts", "types": "./dist/client/react.d.ts",
"import": "./dist/client/react.js" "import": "./dist/client/react.js"
}, },
"./client/nextjs": {
"types": "./dist/client/nextjs.d.ts",
"import": "./dist/client/nextjs.js"
},
"./channels": { "./channels": {
"types": "./dist/channels/index.d.ts", "types": "./dist/channels/index.d.ts",
"import": "./dist/channels/index.js" "import": "./dist/channels/index.js"
@@ -28,14 +24,6 @@
"./jwt": { "./jwt": {
"types": "./dist/jwt/index.d.ts", "types": "./dist/jwt/index.d.ts",
"import": "./dist/jwt/index.js" "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": { "scripts": {

View File

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

View File

@@ -18,7 +18,7 @@ fn fixture_config() -> Config {
project_id: None, project_id: None,
output: PathBuf::from("/tmp"), output: PathBuf::from("/tmp"),
targets: vec!["channels".to_string()], 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_kernel: None,
rust_crate_name: None, rust_crate_name: None,
} }

View File

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

View File

@@ -20,7 +20,7 @@ fn fixture_config() -> Config {
project_id: None, project_id: None,
output: PathBuf::from("/tmp"), output: PathBuf::from("/tmp"),
targets: vec!["python".to_string()], 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_kernel: None,
rust_crate_name: None, rust_crate_name: None,
} }

View File

@@ -19,7 +19,7 @@ fn fixture_config() -> Config {
project_id: None, project_id: None,
output: PathBuf::from("/tmp"), output: PathBuf::from("/tmp"),
targets: vec!["react".to_string()], 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_kernel: None,
rust_crate_name: None, rust_crate_name: None,
} }

View File

@@ -24,7 +24,7 @@ fn fixture_config() -> Config {
project_id: None, project_id: None,
output: PathBuf::from("/tmp"), output: PathBuf::from("/tmp"),
targets: vec!["rust".to_string()], 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 { rust_kernel: Some(RustKernelSpec::Path {
path: "../../../frontends/mizan-rust".to_string(), path: "../../../frontends/mizan-rust".to_string(),
}), }),

View File

@@ -30,7 +30,7 @@ fn synthetic_config() -> Config {
project_id: None, project_id: None,
output: PathBuf::from("/tmp"), output: PathBuf::from("/tmp"),
targets: vec!["stage1".to_string()], 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_kernel: None,
rust_crate_name: None, rust_crate_name: None,
} }

View File

@@ -20,7 +20,7 @@ fn fixture_config(target: &str) -> Config {
project_id: None, project_id: None,
output: PathBuf::from("/tmp"), output: PathBuf::from("/tmp"),
targets: vec![target.to_string()], 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_kernel: None,
rust_crate_name: 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)