Two new frontend packages let a VSCode webview consume a Mizan backend
through its postMessage channel — peer transports to `mizan-tauri-transport`
and the default `httpTransport()`.
- `@mizan/webview-transport` implements `MizanTransport` (call/fetch)
over postMessage with correlation-id pairing. Drop-in for `configure({
transport: webviewTransport() })`; codegen output and React adapter
are unchanged.
- `@mizan/webview-channels` mirrors mizan-react's WebSocket-based
ChannelConnection — RPC + subscribe over the same postMessage channel
for long-running ops where short request/reply isn't enough.
Both expect an extension-host-side dispatcher that reads envelopes via
`webview.onDidReceiveMessage` and routes them through mizan-ts's
`handleMutationCall` / `handleContextFetch`. First consumer is the
holomorphic VSCode extension.
mizan-codegen: new `[source.script]` generic source. Spawns an arbitrary
command and reads stdout as KDL IR. Keeps mizan-codegen out of the
business of knowing every possible backend language while preserving
the "subprocess emits KDL" contract every other source already follows.
Holomorphic uses it to invoke `python -m holomorphic.emit_ir` against
the mizan_core registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tauri now joins FastAPI/Django/axum as a first-class Mizan backend. The
React frontend calls Mizan-registered functions through Tauri's IPC
with the same {result, invalidate, merge} envelope the HTTP path uses;
the schema flows Pydantic → decoru → Rust → KDL → TS in one
mizan-generate invocation.
New packages:
* backends/mizan-tauri — Tauri plugin exposing a single `mizan_invoke`
command that routes through mizan-core's FUNCTIONS / CONTEXTS
registries. No per-function tauri::command; the linkme slice IS the
dispatch table.
* frontends/mizan-tauri-transport — TS package exporting
tauriTransport() that wraps invoke('plugin:mizan|mizan_invoke', ...)
and re-shapes errors into MizanError. Pairs with mizan-tauri.
@mizan/base — pluggable transport:
* Adds MizanTransport interface + transport config field.
* Existing fetch-based body factored into httpTransport() (default).
* mizanCall/mizanFetch delegate to config.transport; merge/invalidate
side-effects stay in the kernel (transport-agnostic).
* Consumers swap via configure({ transport: tauriTransport() }).
mizan-codegen — Rust source + Pydantic pre-step:
* [source.rust] runs a Cargo bin (cargo run --bin <name>) and parses
KDL from stdout. The bin uses mizan_core::build_ir() after
force-linking the consumer's #[derive(Mizan)] / #[mizan::client]
registrations.
* [source.rust.pydantic] is an optional pre-step that pipes an
embedded Python bridge (scripts/run_decoru.py) to python and writes
decoru-emitted Rust types into the consumer crate. The bridge
auto-discovers BaseModel subclasses AND Enum subclasses
(last-variant-is-default convention so decoru's impl Default keeps
compiling against enum-typed fields without explicit Pydantic
defaults).
* Pure-Rust usage stays intact — omit pydantic block and write Rust
types by hand.
mizan-macros:
* #[mizan::client] now supports Result<T, MizanError> returns. The
dispatch wrapper `?`-unwraps the user fn so server-side errors
surface as the protocol's standard {code, message, details?}
envelope; T-returning functions stay unchanged.
* #[derive(Mizan)] strips the r# raw-identifier prefix and honors
field-level #[serde(rename = "...")] when emitting IR field names.
Matches serde's wire shape — fixes IR-vs-JSON drift for Rust-keyword
fields (e.g. `r#type` → `type`).
react.tsx template:
* Conditionally emits context-related imports / useContextSubscription
helper based on has_global || !named_contexts.is_empty(). Consumers
without contexts (mutation/RPC-only apps like claude-manage) no
longer get dead imports that trip noUnusedLocals.
Verified end-to-end: cargo build clean across mizan-tauri,
mizan-codegen, AFI rust_app; AFI three-way KDL parity tests pass;
claude-manage migration drives the full stack (Pydantic schema →
generated TS api → Tauri-IPC transport → mizan-core dispatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three substrate moves required by the Blazr-session port that surfaced
real cross-backend divergences:
1. Inline-substitution for primitive aliases and string enums. Named
types whose body is `Alias(Primitive(_))` or `Enum(_)` collapse into
their inline TypeShape at every `Ref` use site, and don't emit as
their own `type "X" { ... }` entry. Matches Python's Pydantic Literal
and `Foo = str` alias inlining — codegen consumers see the primitive
directly rather than chasing a single-hop indirection.
2. Reachability tree-shake on the type registry. `#[derive(Mizan)]` now
auto-registers every Mizan type into the TYPES slice; the emitter
then transitively walks Refs from function inputs/outputs and emits
only the reachable subset. Original-named entries from derive
register only when something refs them; canonical-renamed entries
from the function macro are reachable by definition. Mirrors
Python's `_collect_named_types`.
3. `#[serde(rename_all = "...")]` + `#[serde(rename = "...")]`
propagation in `#[derive(Mizan)]` for enums. IR enum variants now
match the on-wire JSON casing (lowercase / snake_case / kebab-case /
etc.), not the Rust variant idents. Supports all serde casings.
AFI codegen + wire parity stays green after these changes (the AFI
fixture's enum-free + Pydantic-shape types are unchanged by the three
substrate extensions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three substrate extensions surfaced by the Blazr session port:
1. **App-state threading.** mizan-axum::router() is now generic over a
user-supplied state type and threads `Arc<dyn Any + Send + Sync>` into
every dispatch via RequestHandle. Handlers downcast to their concrete
AppState. The stateless AFI fixture uses `router_stateless()` (matches
the prior signature). RequestHandle gains a `from_dyn()` constructor
to wrap already-erased trait-object references.
2. **`[T; N]` and `BTreeMap<K, V>` lowering in #[derive(Mizan)].** Fixed
arrays emit as `List<T>` (matches Python `tuple[float,...]` → JSON
array). String-keyed maps emit as `List<V>` — closest approximation
until KDL grows a `dict` shape. Also: vec-element registrations get a
per-function scope suffix so two handlers returning `Vec<Same>` don't
collide at the static-name layer.
3. **`types_match` for merge: upsert-into-list semantics.** Now matches
Python `types_match_for_merge`: direct (T == T), upsert (slot is
`Alias(List(T))`, value is T), and list-replace (both sides list).
The AFI fixture only exercised the direct path; the Blazr port's
`morph_set_value` returning a single `MorphLayer` into a context with
`Vec<MorphLayer>` slot is what surfaced the gap.
AFI codegen + wire parity stays 12/12 green after these substrate changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds first-class Rust-backed Mizan to sit alongside mizan-django and
mizan-fastapi. A Rust dev writes:
#[derive(Mizan, Serialize, Deserialize)]
pub struct ProfileOutput { pub user_id: i64, pub name: String }
#[mizan::context("user")]
pub struct UserCtx;
#[mizan::client(context = UserCtx)]
pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { ... }
…and gets byte-identical KDL to the Python emitters, served over the
same wire protocol the React / Rust / Vue / Svelte kernels speak.
New crates:
- cores/mizan-rust/ (Cargo: mizan-core) — IR types, KDL emitter, traits, registry,
runtime (compute_invalidation / compute_merges
ported from mizan-fastapi), graph_check with
structural type-matching
- cores/mizan-rust-macros/ (Cargo: mizan-macros) — #[derive(Mizan)], #[mizan::context],
#[mizan::client] proc macros
- backends/mizan-rust-axum/ (Cargo: mizan-axum) — axum HTTP adapter: /session/, /call/, /ctx/:name/
- tests/afi/rust_app/ — AFI fixture port + server / export-ir binaries
Substrate-shape moves required by cross-language equivalence:
- IR canonicalization: functions / contexts / context-members / shared-by
now sort alphabetically in both Python and Rust emitters. The IR is a
contract; linkme doesn't preserve declaration order, so canonical sort
is the only stable mapping. afi_ir.kdl + per-target baselines regenerated.
- MizanType::TYPE_NAME is a const (with a default type_name() reader) so
it's usable in linkme TypeEntry static initializers.
- Tree-shaken type registry: #[derive(Mizan)] only emits the trait impl;
the #[mizan::client] macro registers canonical-named entries from
fn signatures, including Vec<T> element types for ref resolution.
- Merge resolution is structural (NamedType shape comparison) rather than
by name — matches the Python types_match_for_merge semantics.
Three-way forcing functions:
- tests/afi/test_codegen_parity.py — Django ≡ FastAPI ≡ Rust on KDL bytes (3 pass)
- tests/rust/run_wire_parity.py — 12/12 probes against FastAPI + Rust (EXIT=0)
Incidental fixes surfaced by the new tests:
- Stale `from .registry import validate_registry` import removed from
mizan-django/setup/discovery.py (referenced a function that no longer
exists; was masking codegen-parity).
- BASE_DIR added to tests/afi/django_app/project/settings.py.
- /session/ endpoint added to mizan-fastapi for protocol-shaped readiness
probe parity (wire-parity harness now polls /api/mizan/session/ on both
backends rather than FastAPI's /openapi.json).
- Root .gitignore picks up Rust target/ across the tree so new crates
don't need per-crate gitignore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the transitional OpenAPI 3.0 + `x-mizan-*` extensions
substrate with the canonical Mizan IR as KDL, per docs/AFI_ARCHITECTURE.md:
"KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it."
End-to-end cutover. No transitional path left on main.
Forward direction:
cores/mizan-python/src/mizan_core/ir.py
build_ir() walks mizan_core.registry, introspects Pydantic
models directly (no JSON-Schema indirection), and emits the
Mizan IR document. The KDL grammar is locked in this file's
module docstring.
Backends emit KDL:
backends/mizan-fastapi/src/mizan_fastapi/ir.py
`python -m mizan_fastapi.ir <module>` — CLI entry point.
backends/mizan-django/.../management/commands/export_mizan_ir.py
`manage.py export_mizan_ir` — Django mgmt command.
Codegen consumes KDL:
protocol/mizan-codegen/Cargo.toml: + kdl = "6"
protocol/mizan-codegen/src/ir.rs: NamedType { Struct/List/Enum/Alias }
+ TypeShape { Primitive/Ref/List/Optional/Enum/Union } sum types,
replacing the JsonSchema sprawl. KDL parser walks the
`kdl::KdlDocument` tree into typed Rust structs.
protocol/mizan-codegen/src/fetch.rs: subprocess command switches
to the new IR-export entry points.
All emit modules (stage1 / react / python / rust / vue / svelte /
channels) port their type-walkers from JsonSchema to the new
sum types — case analysis collapses substantially.
Substrate-honesty wins beyond the moat closure:
- `int | bool` multi-arm unions land as `TypeShape::Union` (was
silently coerced to "string" before).
- `<CamelName>Output = list[T]` returns emit as named alias
types instead of struct-shaped wrappers, so consumer code
`.map()` works directly on the type.
- Pydantic field defaults flow through to `default` properties
in KDL, then back to non-optional shape in every target.
Deleted:
- backends/mizan-fastapi/src/mizan_fastapi/{cli,schema}.py
- backends/mizan-django/.../export_mizan_schema.py
- openapi-bearing half of mizan/export/__init__.py (edge
manifest generator preserved — separate concern).
- tests/afi/schema_normalizer.py
- tests/fixtures/{afi_schema.json, channels_schema.json}
- tests/fixtures/js_* baseline directories.
Verification:
- 20 mizan-codegen unit tests green (IR deserialization,
byte-equivalence parity across stage1/rust/python/react/vue/svelte
against fresh KDL-driven baselines, channels structural).
- tests/rust/run_wire_parity.py: 12/12 probes green driving
the binary end-to-end through KDL.
- Blazr studio-ui typechecks against the regenerated React client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The @client(merge=[context, ...]) decorator lets a mutation patch its
return value directly into the cached context bundle by matching the
mutation's Output type against each context-function's Output type
to identify the slot, then splicing server-side. Kernel runs
splice_slot on the response to apply locally — no refetch, no
invalidate-cascade.
Lands H14, H15, H16, M19, M20 from ISSUES.md.
Backends (Django + FastAPI):
_resolve_merges() in both executors walks @client(merge=...) targets,
resolves the per-context slot via types_match_for_merge, and emits
{context, slot, value, params?} entries on the response. Param
auto-scoping mirrors _resolve_invalidation's tier-1 logic.
Frontend kernel (mizan-base):
Response handler reads the merge[] array and applies splice_slot
for each entry — locates the cached context bundle by name+params,
overwrites the named slot with the new value, notifies subscribers.
Core (mizan-python):
@client decorator extended with merge= parameter. Schema export
threads merge metadata onto the OpenAPI x-mizan-functions entries.
Examples / fixtures:
fastapi-react-site harness exercises merge + Playwright spec covers
the end-to-end happy path (mutation → instant UI update without
network refetch). AFI fixture's rename_user function is the
canonical merge target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Mizan codegen substrate moves off JavaScript template-literal emission
onto a compiled Rust binary that consumes the same OpenAPI + x-mizan-* IR
the JS substrate consumed. Three structural wins fall out of one move:
1. Moat closes. The codegen logic (how `affects` becomes auto-invalidation,
how named contexts collapse onto bundled fetches, how the registry-to-
Provider mapping is shaped) ships compiled instead of as source bytes
in every consumer's node_modules.
2. Pattern F (lines.push append-walls) becomes structurally unauthorable.
The emit substrate is askama templates in templates/<target>/*.j2 —
actual target-language files with {{ ... }} substitution markers,
syntax-highlighted natively, type-checked against the render context
structs at compile time. The Rust emit modules build typed render
contexts and call .render(); no string-builder surface exists.
3. OpenAPI `default`-bearing fields now emit as non-optional in TS / Python
/ Rust — the server always populates them, so consumer code reads them
without nullable checks. Surfaced by Blazr's typecheck on regeneration.
Layout:
frontends/mizan-rust/ — Rust port of @mizan/base; #[cfg(feature="pyo3")]
exposes PyMizanClient for the Python target.
protocol/mizan-codegen/ — codegen binary source + askama templates.
protocol/mizan-generate/ — npm-package shim. bin/launcher.mjs dispatches
to the platform-appropriate prebuilt binary.
Old generator/ JS tree deleted.
tests/rust/ — wire-parity drivers. drive_kernel exercises
raw client.call() / fetch_context(); drive_emitted
exercises the typed crate the codegen emits.
tests/afi/afi_codegen_app.py — codegen entrypoint module (imports + registers).
backends/mizan-fastapi/.../schema.py — adds outputNullable so the Rust
codegen can wrap T | None responses in Option<T>.
Verification:
- 20 mizan-codegen tests green (IR deserialization, byte-equivalent
parity vs JS baseline for stage1/rust/python/react/vue/svelte,
structural test for channels).
- tests/rust/run_wire_parity.py — 12/12 probes green via the Rust binary
driving the FastAPI fixture end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The codegen used to live in mizan-react before mizan-django before
protocol/mizan-generate. Each move left sediment in the previous
home; the bin entry in particular shadowed mizan-generate's own bin
in node_modules/.bin/, breaking `npx mizan-generate`. Caught at
integration time when the harness install picked up the stale link.
frontends/mizan-react/package.json:
- Removed bin entry pointing at the long-gone ./dist/generator/cli.mjs.
- Simplified the build script — dropped `cpSync('src/generator',
'dist/generator', ...)`. src/generator hasn't existed in this package
since the first move; the cpSync would silently fail at every build.
- Removed optionalDependencies (chokidar, minimatch, openapi-typescript) —
these were codegen-watcher deps, no longer relevant to the React adapter.
examples/{django,fastapi}-react-site/harness/package.json:
- Added `mizan-generate` as a file: devDep so `npx mizan-generate
--config <config.mjs>` resolves to the right binary in the monorepo.
Mirrors the install pattern the README documents for downstream users.
Verified: mizan-react vitest 33/33 (78 skipped — integration tests).
Codegen runs from harness via npx for both example apps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The codegen consumes a schema from any backend and emits typed client
code for any frontend — it doesn't belong inside a backend adapter.
That placement was historical sediment from when there was only a
Django backend; it predates the AFI generalization.
New top-level slot: `protocol/` for protocol-level tooling. Tree is
now:
backends/ server protocol adapters
frontends/ client kernel + per-framework adapters
cores/ shared language-level primitives
protocol/ protocol-level tooling
workers/ runtime workers / bridges
Codegen moves to `protocol/mizan-generate/`. Same file layout under
`generator/` (cli.mjs, lib/), preserved via git mv.
Package metadata cleaned up:
- name: "generate" (placeholder) → "mizan-generate"
- description filled in
- type: module (cli.mjs is .mjs ESM, was previously declared "commonjs")
- bin entry added so `npx mizan-generate --config <config.mjs>` works
once the package is published, instead of `node path/to/cli.mjs`.
Path-reference fixups:
- backends/mizan-django/README.md: `node path/to/...` → `npx mizan-generate`
- backends/mizan-fastapi/README.md: same
- ISSUES.md: file paths in three issue entries
- CLAUDE.md: codegen description + Package Layout section refreshed
(added protocol/, mizan-fastapi entry, mizan-python entry)
- docs/AFI_ARCHITECTURE.md: Package Layout refreshed identically
Verified codegen runs from new location: regenerated the FastAPI
example harness's api/ output, identical to pre-move.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mizan-django/README.md:
- Updated install path (was pointing at the old `subdirectory=django` git
layout from before the backends/ restructure).
- Dropped the dead "monorepo root README" link (the root README was
removed earlier in the substrate-restoration work).
- Fixed the apps.py example — convention is `clients.py` per MIZAN.md,
not `mizan_clients.py`.
- Added the `mizan_clients()` auto-discovery pattern (it was missing).
- Added a Generate-the-frontend section: config shape + CLI invocation
+ the resulting <MizanContext>/use{Hook}() React surface.
- Tightened decorator-parameter overview to a single block covering the
full @client surface.
mizan-fastapi/README.md (new):
- Mirrors mizan-django's structure for consistency.
- Opens with the AFI-common scope: forms/channels/shapes/SSR are out of
scope on the FastAPI side; FastAPI projects use native equivalents.
- Setup shows app.add_exception_handler wiring for MizanError +
RequestValidationError so every error surface goes through the same
envelope the kernel parses.
- Calls out explicit register() (no AppConfig.ready() analog on FastAPI;
registrations live in main.py or an imported clients.py).
- Auth-integration section explains the request.state.user middleware
contract the executor expects.
- Codegen section shows the source.fastapi config shape that points at
the new `python -m mizan_fastapi.cli <module>` schema export.
- Closes with pointers to AFI conformance + the e2e harness so a reader
can verify the adapter's claims.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Demonstration milestone. The substrate work earlier in the session
established that mizan-fastapi can dispatch RPC, bundle context
fetches, and emit invalidation envelopes via TestClient (in-process
ASGI). This commit closes the demonstration gap: a real FastAPI server
on port 8001 + a real React harness on port 5175 + Playwright in real
Chromium, exercising generated hooks.
What ships:
backends/mizan-fastapi/src/mizan_fastapi/cli.py — schema-export CLI:
- `python -m mizan_fastapi.cli <module>` imports the named module
(triggering @client decorations + register() side effects), then
prints the OpenAPI schema to stdout. Mirrors mizan-django's
`manage.py export_mizan_schema` so the codegen consumes either
backend the same subprocess way.
backends/mizan-django/generate/generator/lib/fetch.mjs — codegen now
dispatches on source.django vs source.fastapi. Refactored the
subprocess plumbing into a shared runSubprocess helper. The codegen
package is still named "mizan-django" by historical accident — it's
the framework-agnostic CLI now (a rename for later).
backends/mizan-fastapi/src/mizan_fastapi/executor.py — bug fix:
mizan_core's @client decorator normalizes auth=True to
meta['auth']='required'. The executor's match was only handling True,
not 'required', so any auth-required endpoint failed with
INTERNAL_ERROR. Now matches both. Caught when wiring up the FastAPI
example backend's whoami fixture; would have surfaced first time any
real FastAPI app used auth=True.
backends/mizan-fastapi/tests/test_dispatch.py — added AuthTests
covering the auth=True path so the bug fix has unit coverage. Suite
now 12/12.
examples/fastapi-react-site/ — parallel to examples/django-react-site/:
- backend/main.py: FastAPI app with 11 @client fixtures matching the
harness surface (echo, add, multiply, whoami, staff/superuser/
verified-only, notImplementedFn, buggyFn, permissionCheckFn,
current_user context). Drops Django-only stuff (forms, channels,
ws-whoami, session-bound JWT).
- harness/: vite proxy → FastAPI on 8001; generated api/ produced by
the codegen against fastapi.config.mjs.
- mizan.spec.ts: Playwright suite, 14 tests covering the same axes
as Django minus channel-chat.
- ContextCurrentUser fixture renders 'loading' until data arrives
rather than emitting <pre>null</pre> — fixes a race the Django
harness has too (just doesn't trip in practice).
Verified:
- mizan-fastapi unit: 12/12 (incl. new auth=True coverage)
- mizan-fastapi e2e: 14/14 (Playwright via real Chromium)
- mizan-core unit: 15/15
- mizan-django unit: 348 pass, 21 skip
- AFI conformance: 3/3
- mizan-django e2e: 14/15 (1 skip — channels, deferred)
What remains for FastAPI side:
- Dockerfile.test + docker-compose.test.yml so CI can run the e2e
in the same containerized way as the Django example.
- Makefile test-integration target for symmetry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-stating the architectural intent that got silently swapped to "JSON
schema" during the doc-restoration commit (6eca514). The original
AFI_ARCHITECTURE positioned KDL as the LLVM-IR-equivalent of the
system; I read the stale-package-name note at the bottom as
invalidating the whole architecture description and substituted
JSON-schema references thinking that was the more-current intent.
That conflated implementation drift (mizan-ast/mizan-schema package
names) with substrate intent (KDL is the IR). The packages had
drifted; the IR target had not. Restoring the section.
Frames KDL-as-IR explicitly as forward-direction. The current
OpenAPI/JSON-Schema codegen path is transitional — it's the layered
indirection where adapter divergence lives (AFI conformance suite
demonstrates this). Real KDL implementation lives in a forthcoming
mizan-schema package; today's path is sediment around the eventual
substrate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substrate-level gate: same @client fixture registered in both backends
emits equivalent schemas, therefore the codegen produces equivalent
TypeScript regardless of which backend the frontend is generated against.
Catches adapter symmetry problems (Pydantic→OpenAPI converter divergence,
metadata leakage, ordering non-determinism) without docker, browser, or
Playwright.
What ships:
backends/mizan-fastapi/src/mizan_fastapi/schema.py — build_schema():
- Builds OpenAPI 3.0 from registered Mizan functions, mirroring the
shape mizan-django's export emits.
- Drives FastAPI's native OpenAPI generation by registering a stub POST
endpoint per function with its Input/Output Pydantic models, then
appends x-mizan-functions and x-mizan-contexts extensions.
- Param-elevation logic mirrors mizan-django/src/mizan/export/__init__.py
exactly (sharedBy tracking, required iff every function in context has
the param).
- snake_to_camel and metadata field shapes match Django for byte-equality
on the AFI surface.
tests/afi/ — the conformance harness:
- fixture.py: 5 @client functions covering the protocol axes (plain,
context, mutation+affects). No channels/forms — those aren't AFI-common.
- django_app/: minimal Django project (settings, urls, AppConfig.ready
registers the fixture). manage.py adds tests/afi/ to sys.path so both
backends import the same fixture module.
- fastapi_app.py: thin make_app() that registers fixture and mounts router.
- schema_normalizer.py: drops backend-specific framing — Ninja-vs-FastAPI
envelope differences (info/servers/tags), Django-only function fields
(form metadata), x-mizan-channels. Plus afi_subset() and
function_io_schemas() helpers for narrower comparisons.
- test_codegen_parity.py: three gates
1. x-mizan-functions match across backends
2. x-mizan-contexts match across backends
3. Per-function Input/Output OpenAPI schemas match (what codegen feeds
to openapi-typescript for type generation)
The full normalized OpenAPI envelopes do diverge — FastAPI adds
HTTPValidationError, the two converters wrap things slightly differently
in non-AFI-essential ways. That's not in the test scope. The codegen
only consumes x-mizan-functions, x-mizan-contexts, and the per-function
type schemas; those are what the test gates.
Makefile: test-afi target added; rolls into the test aggregate.
Verified: 3/3 conformance tests pass. Other surfaces unaffected —
mizan-core 15/15, mizan-django 348 pass, mizan-fastapi 11/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the React-codegen rework, ran the full e2e harness against the
docker-stack backend. Surfaced and fixed real friction:
mizan-base/src/index.ts (kernel):
- MizanError now parses both error envelopes — the FastAPI shape
({"error": {"code", "message", "details"}}) and the Django shape
({"error": true, "code", "message", "details"}). Exposes .code and
.details on the thrown error so consumer code can branch on them.
This was needed for the harness's `instanceof MizanError && error.code
=== 'NOT_FOUND'` pattern to work; the previous MizanError only carried
status + raw body, leaving callers to parse the body themselves.
examples/django-react-site/Dockerfile.test:
- Backend image now copies and installs cores/mizan-python before
installing mizan-django (which imports from mizan_core after the
Layer 1 extraction).
harness/src/fixtures.tsx:
- useRun helper updated for the new mutation-hook shape: pulls
{ mutate } off the hook result instead of treating the hook return
as a callable. Same for ValidationError fixture.
mizan.spec.ts:
- DjangoError → MizanError (kernel error class is backend-agnostic).
- Form tests removed (forms codegen deferred per Blazr scope).
- Channel test marked test.skip (channels deferred per Blazr scope).
.gitignore: ignore Playwright test-results/.
Final verification across all surfaces:
- mizan-core unit: 15/15
- mizan-django unit: 348 pass, 21 skip
- mizan-fastapi unit: 11/11
- mizan-ts edge-compat: 34/34 (cross-language HMAC pin)
- harness e2e (Playwright): 14/15 (1 skip = channels deferred)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The harness was written against the MIZAN.md oracle (<MizanContext>,
provider-per-context, useMizan, etc.) but the codegen had been narrowed
to just hooks-direct-on-kernel after the kernel split. Restoring the
React-idiomatic layer on top of the kernel.
backends/mizan-django/generate/generator/lib/adapters/react.mjs:
- Emits <MizanContext baseUrl="…"> root provider that calls configure()
once and (if a global context is registered) wraps children in
<GlobalContextProvider>.
- Emits <GlobalContextProvider> + <{Name}Context> per named context —
kernel registration happens once per provider mount, not per hook
call. Consumers read from React Context.
- Base hooks: useGlobalContext() / use{Name}Context() return full
ContextState<T> (data + status + error).
- Convenience hooks per context-function (use{Fn}() returns data | null)
and per regular function/mutation (use{Fn}() returns
{ mutate, isPending, error }).
- useMizan() returns { call, fetch } as an imperative escape hatch
for test harnesses or rare cases where typed hooks don't fit.
- Re-exports MizanError, configure, initSession, ContextState from
@mizan/base.
backends/mizan-django/generate/generator/cli.mjs:
- After Stage 2, appends `export * from './<adapter>'` to index.ts so
`import { useEcho, MizanContext } from './api'` works as a barrel.
Bug fixes surfaced during integration:
- react.mjs was generating `from '../index'` (wrong path); flat layout
needs `./index`.
- harness django.config.mjs had `output: 'src/api/generated.ts'` which
the codegen treated as a directory; corrected to `output: 'src/api'`.
- example testapp/clients.py imported from the deleted
mizan.setup.registry path; routed through mizan.setup aggregator.
harness/package.json: adds @mizan/base dep so the generated react.tsx
can resolve its kernel imports.
harness/src/fixtures.tsx:
- DjangoError → MizanError (kernel error class, backend-agnostic).
- useChatChannel sourced from ./api/channels.hooks directly (not
re-exported from the unified index for now).
- Form fixtures removed — forms codegen deferred per Blazr scope.
Verified: harness `vite build` succeeds, 53 modules transformed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reworked the MVP code along the lines Ryth flagged. Same behavior
(11/11 tests still pass), tighter idiom.
executor.py:
- Replaced FunctionResult / FunctionError dataclasses with a MizanError
exception hierarchy (NotFound, BadRequest, ValidationFailed,
Unauthorized, Forbidden, NotImplementedYet, InternalError). Each
carries its own ErrorCode + HTTP status; the dispatcher path raises
rather than returning sentinel objects.
- Auth check uses match/case for the requirement (True / 'staff' /
'superuser' / callable / other) — single declarative dispatch instead
of an if/elif chain.
- Broke up the single 80-line execute_function into focused helpers:
_resolve_function, _enforce_auth, _validate_input, _serialize,
_invalidation_target. The execute_function body now reads as five
declarative steps.
- Input validation uses Pydantic's model_fields[name].is_required()
directly and a list comprehension for required-field reporting,
instead of round-tripping through model_json_schema().
router.py:
- POST /call/ now declares its body as a Pydantic CallBody model;
FastAPI handles parsing + envelope validation. No more manual
await request.json() + dict[get] dancing.
- Endpoint bodies shrink to 3-5 lines each. Context fetch uses a
dict comprehension over the function group.
- mizan_exception_handler renders MizanError to the protocol's
{error: {code, message, details}} envelope.
- mizan_validation_handler maps FastAPI's RequestValidationError to
the same envelope under BAD_REQUEST so the wire format is uniform
whether the failure is body-shape or business validation.
__init__.py: exposes the full exception hierarchy + both handlers
so consumers can wire them onto their FastAPI app declaratively:
app.add_exception_handler(MizanError, mizan_exception_handler)
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
Verified: mizan-core 15/15, mizan-django 348 pass, mizan-fastapi 11/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Blazr-critical surface for FastAPI. Forms, Channels, Shapes, SSR,
and MWT are out of scope (Ryth's call: defer until Blazr exercises
them; FastAPI projects use native equivalents anyway).
What ships:
- POST /api/mizan/call/ RPC dispatch with Pydantic input validation
- GET /api/mizan/ctx/{name}/ bundled context fetch (all functions in
the named context, parallel-evaluated, single
JSON response)
- JSON-body invalidation transport (the 'invalidate' field on mutation
responses, with auto-scoping when mutation arg names match context params)
- Auth check infrastructure expecting request.state.user populated by
FastAPI middleware/deps (matches FastAPI idioms)
- Cache-Control: no-store on all responses
Built on existing mizan-core: registry (function lookup, context groups,
invalidation metadata), client.function (the @client decorator + ServerFunction
+ _FunctionWrapper). No code copied or duplicated from mizan-django — the
shared substrate is genuinely shared.
Package layout:
backends/mizan-fastapi/
pyproject.toml distribution=mizan-fastapi, module=mizan_fastapi
src/mizan_fastapi/
executor.py dispatch + auth + invalidation
router.py FastAPI APIRouter with the two endpoints
tests/test_dispatch.py 11 e2e tests against TestClient
Test fixture establishes the registration pattern: explicit
register(fn_class, "name") after each @client. mizan-fastapi doesn't
ship discovery — apps register their functions explicitly. (mizan-django
keeps its DjangoAppVisitor discovery; FastAPI's lack of an app system
makes auto-discovery less natural.)
Makefile: install + test targets now include mizan-fastapi alongside
the other packages. New test-core / test-fastapi targets added for
symmetry.
Verified:
- mizan-core: 15/15
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-fastapi: 11/11
- mizan-ts edge-compat: 34/34
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The @client decorator + ServerFunction base + composition machinery is
mostly framework-agnostic. The only Django couplings were typing
(HttpRequest in __init__ and submit_handler signatures) and runtime
view-path detection (HttpResponseBase isinstance/issubclass checks).
Replaced both with backend-extension hooks:
- HttpRequest type hints → Any. Type Protocol can be tightened later.
- HttpResponseBase view-path detection → set_framework_response_base(cls)
hook in mizan_core.client.function. Backends register their framework's
response base at import time. is_framework_response(obj_or_cls) handles
both instance and subclass checks via the registered base.
mizan-django registers HttpResponseBase via mizan/client/__init__.py
before any @client-decorated code is loaded. FastAPI would similarly
register starlette.responses.Response.
Direct consumers updated:
- mizan/setup/discovery.py: ServerFunction import path
- mizan/forms/__init__.py: ServerFunction + create_form_functions imports
mizan/client/__init__.py keeps its public re-export surface stable so
'from mizan.client import client, ServerFunction, …' continues to work
for downstream Django consumers.
Verified:
- mizan-core: 15/15
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-ts edge-compat: 34/34
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original registry tangled function, channel, composition, and form
registration in a single file with polymorphic register() dispatch.
That predates the household discipline; it was the design that was
supposed to ship but didn't. Re-implementing the original intent.
cores/mizan-python/src/mizan_core/registry.py (new):
- _functions, _compositions dicts
- register() — ServerFunction-only, no polymorphic dispatch
- register_as(), register_compose()
- register_extension(name, extension) — hook interface
- get_function/get_compose/get_all_functions/get_all_compositions
- get_contexts, get_context_groups
- get_registry, get_schema — aggregate extension contributions
- validate_registry, clear_registry — cascade-clear extensions
RegistryExtension Protocol:
- schema() returns the extension's schema subdict (keyed under its name)
- clear() resets extension state (called by clear_registry)
mizan-django/src/mizan/channels/__init__.py:
- _ChannelsExtension wraps the channel _registry, plugs into core via
register_extension('channels', ...). Schema output preserves the
same shape codegen consumed before (snake_case keys, type+bidirectional).
mizan-django/src/mizan/forms/__init__.py:
- register_form() and get_forms() helpers moved here (were in setup/registry.py)
- Both use mizan_core.registry under the hood. Forms don't need a
separate extension because form sub-functions register as regular
ServerFunctions with meta.form set.
mizan-django/src/mizan/setup/registry.py: deleted.
mizan-django/src/mizan/setup/__init__.py: re-exports the registry helpers
from mizan_core.registry / mizan.channels / mizan.forms — the Django
adapter's curated public API surface stays stable for users.
Consumers updated: ~10 files imported `from mizan.setup.registry`;
all switched to direct imports from mizan_core.registry, mizan.channels,
or mizan.forms as appropriate. ChannelTests in test_core.py rewritten
to use mizan.channels.register directly (no more polymorphic
@register_as on ReactChannel subclasses).
Verified:
- mizan-core: 15/15
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-ts edge-compat: 34/34 (cross-language pin holds)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cache/backend.py is pure framework-agnostic key-value abstraction —
CacheBackend Protocol, MemoryCache, RedisCache. No Django imports.
Moves to cores/mizan-python/src/mizan_core/cache/backend.py with no
content changes; mizan-django re-imports.
Verified: mizan-core 15/15, mizan-django 348 pass / 21 skip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull cache/keys.py (HMAC cache key derivation) and mwt.py (Mizan Web Token)
out of backends/mizan-django and into a new cores/mizan-python package.
mizan-django re-imports them via the new mizan_core module.
Naming: directory cores/mizan-python/, distribution mizan-core, importable
module mizan_core. mizan-django keeps its existing 'mizan' distribution slot
on PyPI; the two coexist as distinct packages.
Wiring:
- backends/mizan-django/pyproject.toml gains a 'mizan-core' dep with a
[tool.uv.sources] path entry (editable install from ../../cores/mizan-python).
- Makefile install target prepends 'cd cores/mizan-python && uv pip install -e .'
- 3 import sites in mizan-django updated: cache/__init__.py, jwt/functions.py,
client/executor.py — all now import from mizan_core.
Test split:
- 3 unit-test classes (CacheKeyDerivationTests, MWTCreationTests,
PermissionKeyTests) move to cores/mizan-python/tests/, rewritten against
unittest.TestCase (no Django dep). The cross-language pin test (pinned
HMAC hex digests against mizan-ts) moves with CacheKeyDerivationTests.
- Integration tests stay in mizan-django (CacheBackendTests, CachePurgeTests,
CacheIntegrationTests, RevParameterTests, MWTAuthIntegrationTests) — they
need the Django request flow.
Verified:
- mizan-core: 15/15 pass (incl. cross-language pin)
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-ts: edge-compat 34/34 pass — protocol invariant holds, the moved
Python derive_cache_key still produces the exact hex digests TS pins against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fe39fcb commit captured the file moves (git mv stages those automatically)
but didn't catch the content edits I made afterward — npm package rename
(@mizan/runtime → @mizan/base), path updates in Makefile/Dockerfile/examples,
and doc updates were all left unstaged at commit time.
This commit lands those:
- npm rename: 3 frontend package.jsons (base/vue/svelte) + mizan-base/src/index.ts + 4 codegen templates
- path updates: Makefile, Dockerfile.test, two Gitea workflows, four example/harness configs
- doc updates: CLAUDE.md, ROADMAP.md, ISSUES.md, docs/AFI_ARCHITECTURE.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
packages/ flattens into:
backends/ server protocol adapters (mizan-django, mizan-ts)
frontends/ client kernel + framework adapters (mizan-base, mizan-react, mizan-vue, mizan-svelte)
workers/ runtime workers (mizan-ssr)
cores/ shared language-level primitives (empty for now; mizan-python forthcoming)
The frontend kernel (was packages/mizan-runtime, now frontends/mizan-base) is
renamed to reflect its role — it's the shared base that frontend adapters
depend on directly. Reflects the substrate position that per-framework adapters
wrap a single shared kernel; codegen targets the adapter, not the raw kernel.
Path updates landed in: Makefile, two Gitea workflows, Dockerfile.test, four
example/harness config files, .claude/settings.local.json, four docs
(CLAUDE/ISSUES/ROADMAP/AFI_ARCHITECTURE), four codegen templates (stage1 +
react/vue/svelte adapters), and three package.jsons (the mizan-base rename
plus mizan-vue/svelte peerDeps).
Generated files under examples/django-react-site/harness/src/api/ still
reference @mizan/runtime — left as-is; they're regenerated artifacts and
the harness is non-functional pending the React wrapper-layer codegen.
Also folded in a pre-existing fix: the Gitea workflows had
working-directory: react / django pointing at a layout that predates
packages/, never updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ROADMAP: done items moved out of "Next" (codegen rewrite, SSR bridge,
edge manifest, X-Mizan-Invalidate, return-type branching, affects_params,
kernel extraction, two-stage codegen, mizan-ts). Real "Next" in:
framework-adapter wrapper layer (MizanContext + useMizan + DjangoError
on top of the kernel) for React/Vue/Svelte; A1–A4 from ISSUES.md.
CLAUDE: 4-package layout replaced with the actual 7-package layered
architecture (backend protocol adapters + frontend kernel + framework
adapters + SSR worker). "STALE codegen" section rewritten to describe
what's emitted vs. the wrapper layer that isn't yet.
docs/ now tracked (6 files). AFI_ARCHITECTURE rewritten — replaced
the speculative `mizan-ast`/`mizan-csr`/`mizan-rpc`/`mizan-schema`
package names with the real layout, dropped KDL-schema language for
the actual schema-export format. The other 5 docs/ files were already
current and are tracked as-is.
ARCHITECTURE-REWORK.md deleted — same expert review is re-tracked in
the fresher ISSUES.md, two parallel trackers was sediment.
README.md deleted — drift was beyond surgical fixes (`mizan_clients.py`
convention, `<DjangoContext>` provider, removed `@compose` and
`context='local'`, wrong codegen output filenames, 3-package structure
vs. 7). Rewrite waits for the wrapper-layer codegen to land so
user-facing examples reflect reality.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovery convention per MIZAN.md is `clients.py`. The example backend's
asgi.py was still importing the older `mizan_clients` name, causing the
example Django container to fail to start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added 6 architectural/cleanup items (A1-A6):
- Legacy MizanProvider not yet removed
- Allauth pending extraction to own package
- Forms codegen not adapted to kernel
- Vue/Svelte adapters not validated end-to-end
- ROADMAP.md and CLAUDE.md likely stale
Added 12 test coverage gaps (T1-T12):
- No tests for C6 kernel state machine
- No tests for generated Vue/Svelte output
- No tests verifying recent fixes (C3/C4/C5/C7/H3/H10/H11/H13)
- No end-to-end integration test
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The kernel is no longer a blind refetch pipe. Each context entry has:
{ data, status: idle|loading|success|error, error }
registerContext() returns { getState, subscribe, refetch, unregister }.
Adapters subscribe to state changes via callbacks. The kernel does
the fetch and notifies subscribers with the new state.
React adapter uses useSyncExternalStore for tear-free reads.
Vue adapter uses ref + subscribe callback.
Svelte adapter uses readable store backed by kernel subscription.
All three adapters also get:
- Mutation hooks with { mutate, isPending, error } (fixes H5)
- Vue: onServerPrefetch for Nuxt SSR (fixes M9)
- Svelte: readable store auto-cleans up on unsubscribe (fixes H9)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
H3: mizanFetch retries 2x on server errors (5xx) and network
failures. 200ms/400ms backoff. Mutations NOT retried (not idempotent).
H6: refreshContext now uses GET /ctx/<name>/ instead of POST /call/.
Context reads go to the context endpoint, not the mutation endpoint.
H11: Python cache key derivation normalizes True→"true",
False→"false", None→"null" for cross-language HMAC consistency
with JavaScript's String() behavior.
H13: Forms isValid now checks that all required fields have been
touched, not just that touched fields have no errors.
M11: execute_function return type updated to include HttpResponseBase
for view-path functions.
M18: registerContext cleanup uses ?. instead of ! to prevent crash
if Map was cleared (already fixed in H2 commit but documenting).
373 Django + 33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
C1+C7: Cache purge now passes user_id and works for view-path
mutations. Extracted _purge_cache_for_invalidation() shared helper
used by both RPC and view-path branches.
C2: initSession retries 3x with backoff. Resets on total failure
so next call tries again instead of permanently broken CSRF.
C3: SSR template backend injects __MIZAN_SSR_DATA__ script tag
with serialized props for client-side hydration.
C4: SSR bridge uses _write_lock to serialize stdin writes from
concurrent Django threads. Prevents JSON interleaving.
C5: SSR bridge registers atexit handler for process cleanup.
No more orphaned Bun processes on Django reload/shutdown.
H1: pendingScoped changed from Map to Array — multiple scoped
invalidations for the same context no longer overwrite.
H2: registerContext uses stableKey() (sorted JSON) instead of
bare JSON.stringify. Property order no longer matters.
H4: Named context providers skip refetch if SSR data exists
(matches global context behavior).
H10: _meta always assigned as fresh dict, preventing shared-dict
mutation across ServerFunction subclasses.
373 Django + 33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Renamed:
DjangoError → MizanError
DjangoHTTPClient → MizanHTTPClient
DjangoFormState → MizanFormState
DjangoFormsetState → MizanFormsetState
createDjangoCSRClient → createMizanCSRClient
createDjangoSSRClient → createMizanSSRClient
ensureDjangoSession → ensureMizanSession
useDjangoCSRClient → useMizanCSRClient
TDjangoMessage → TServerMessage
Made CSRF configurable:
configureCsrf(cookieName, headerName) — defaults to Django
conventions but works with any backend that uses CSRF tokens.
Cookie name and header name are no longer hardcoded.
All old names preserved as deprecated aliases in index.ts exports
for backwards compatibility.
Removed dead RouterAdapter re-export (file moved to legacy/).
33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
allauth/ (44 files) is a django-allauth React UI — a separate concern
from the Mizan protocol. Moved to legacy/ pending extraction into a
standalone mizan-django-allauth package.
Also moved to legacy/:
- client/AuthContext.tsx — generic auth state from /me endpoint
- client/RouterContext.tsx — framework-agnostic router adapter
- client/routing.tsx — UserRoute/StaffRoute/AnonymousRoute guards
- client/nextjs.tsx — Next.js router adapter for auth
These are auth UI infrastructure, not Mizan protocol. The Mizan core
only needs JWT for auth header selection (jwt/ stays — MizanProvider
depends on useJWT() to decide between Bearer and session auth).
Cleaned up re-exports in client/react.ts and vitest aliases.
33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The worker receives a file path in the JSON message, dynamically
imports it, renders it. No registerComponent API, no app entry file,
no export maps. Django's template backend resolves the template name
to an absolute path against DIRS, same as every other template engine.
render(request, 'components/Hello.tsx', {'name': 'World'})
Verified working: curl http://localhost:8000/hello/ returns
<div id="mizan-root"><div>Hello, World!</div></div>
Changes:
- worker.tsx: receives file path, dynamic import with cache
- bridge.py: sends file path instead of component name
- backend.py: resolves template name against DIRS to absolute path
- Fix bridge.py:147 bug (referenced deleted 'component' variable)
- Example app: Hello.tsx component, /hello/ view, template config
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix testapp/apps.py: import djarea_clients (file was never renamed)
- Fix fetch.mjs: command is export_djarea_schema not export_mizan_schema
- Fix harness package.json: dependency path to mizan-react after restructure
- Add package.json for generator (openapi-typescript dependency)
- Regenerate all example code with new protocol format:
- generated.provider.tsx uses raw context responses + SSR hydration
- generated.server.ts uses GET /ctx/global/ with response.ok check
- generated.forms.ts, channels.ts, channels.hooks.tsx refreshed
- Remove stale generated.django.tsx and generated.django.server.ts
- Update imports: fixtures.tsx and main.tsx import from ./api (index)
- Use MizanContext instead of deprecated DjangoContext in examples
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs fixed:
1. MizanProvider.call() read data.data but server returns data.result.
Now reads data.result and processes data.invalidate for server-driven
invalidation (triggering refetch on mounted context providers).
2. GlobalContextLoader expected {error, data} wrapper but context GET
returns raw bundled data. Fixed to iterate response directly.
3. Named context providers had same wrapper assumption. Fixed to
setData(result) directly.
Two features added:
1. SSR hydration: GlobalContextLoader checks window.__MIZAN_SSR_DATA__
on mount. If present, populates contexts from it and skips fetch.
2. SSR hydration: Named context providers check __MIZAN_SSR_DATA__ in
useState initializer. If SSR data exists for their functions, they
render immediately without fetching.
3. Server-driven invalidation in MizanProvider.call(): reads the
invalidate array from mutation responses and triggers refetch on
mounted providers. Generated mutation hooks' hardcoded invalidation
is now redundant but idempotent — both paths coexist safely.
Also fixed FunctionSuccessResponse type to match new protocol:
{ result: T, invalidate?: [...] }
373 Django + 33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents the three protocols (RPC, Invalidation-on-Mutation,
Frontend-Agnostic Rendering), the full @client decorator API surface
with all parameters and _meta structure, the HMAC cache key derivation
scheme, Redis/Memory backends, the MWT/JWT token systems with secret
separation, the SSR template backend + Bun worker bridge, the Edge
manifest format, and the current codegen gap.
Written from reading every source file, not from memory or prior docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mizan's SSR is a Django template backend. Configure in TEMPLATES:
TEMPLATES = [{
'BACKEND': 'mizan.ssr.MizanTemplates',
'OPTIONS': {'worker_path': 'frontend/ssr-worker.tsx'},
}]
Then render(request, 'ProfilePage', {'user_id': 5}) renders the React
component via a persistent Bun subprocess. The component name is the
template name. The context dict becomes props.
Architecture:
- Bun worker: stdin/stdout JSON-RPC, renderToString, component registry
- Django bridge: subprocess lifecycle, crash recovery, concurrent renders
- Template backend: implements Django's BaseEngine interface
This is the AFI's SSR boundary:
- Backend adapter implements mizan.ssr() (data gathering)
- Frontend adapter implements renderToHTML() (component rendering)
- Bun subprocess is the runtime hosting the frontend adapter
11 tests: ping, render, error handling, crash recovery, concurrent
renders (5 threads), template backend integration. All require Bun.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mizan's protocol layers (origin Redis cache, Edge Worker) handle caching
autonomously. The origin emits Cache-Control: no-store on ALL responses —
browsers and non-Mizan intermediaries must not cache. The Edge Worker
controls CDN caching via cf object, independent of origin headers.
Also fixes:
- TS localeCompare → byte-order sort (localeCompare is locale-sensitive,
would produce different HMAC keys for non-ASCII params vs Python)
- Python cache_purge: empty {} params no longer treated as falsy
(was inconsistent with JS where {} is truthy)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The reverse index approach (Redis sets tracking HMAC keys per context)
was over-engineered. Scoped purge doesn't need an index — recompute
the HMAC key from the invalidation params and DELETE directly. One
Redis command, no TOCTOU race, no atomicity concern, no stale members.
Broad purge uses key-prefix scan (keys are now "ctx:{context}:{hmac}").
This is rare (Tier 3 fallback) and acceptable as a SCAN operation.
Eliminated from both Python and TypeScript:
- All SET/SADD/SMEMBERS/SREM index operations
- CacheBackend.get_index, remove_from_index, delete_index, delete_indexes_by_prefix
- build_index_keys function
- Pipeline transaction complexity
- TOCTOU race condition (was critical, now impossible)
Backend interface is now 5 methods: get, set, delete, delete_by_prefix, clear.
Redis tests updated — prefix isolation test added, connection leak fixed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 tests hitting Redis on localhost:6399 (docker run redis:alpine):
- get/put/delete, index tracking, remove_from_index, delete_by_prefix
- TTL verification on cache entries AND index sets
- Pipeline atomicity (value + indexes written together)
- Scoped purge (AND semantics) against real Redis
- Broad purge with sub-index cleanup
- Tests skip gracefully if Redis is not available
No mocks, no fakes. Real Redis or skip.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port of Python's origin-side cache to TypeScript:
- cache/keys.ts: deriveCacheKey with stableStringify for JSON-canonical HMAC
- cache/backend.ts: MemoryCache (same API as Python)
- cache/index.ts: cacheGet, cachePut, cachePurge with AND semantics
Integrated into dispatch.ts:
- handleContextFetch: cache lookup before execution, store after
- handleMutationCall: purge on invalidation
Cross-language pin test proves Python and TypeScript produce identical
HMAC-SHA256 output for the same inputs:
Public: 605a1ca5ad5994e9b765c8d1b330474c2a0d51a7b8fbbdc402f992da7ba902f6
User-scoped: 30fc08eb46ee4ff2cf7d317e97dca90fd616511e0587304416f71dc863338dc2
34 TypeScript tests (9 new), 165 Python tests (1 new pin test).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical:
- Separate MIZAN_MWT_SECRET from MIZAN_CACHE_SECRET — compromising one
no longer compromises the other (token forgery vs cache poisoning)
- Move kid from JWT payload to JOSE header per RFC 7515 — standard
libraries use header kid for key selection before payload decode
High:
- Full SHA-256 pkey (64 chars) instead of truncated 16 — no reason to
reduce collision resistance
- Add nbf (not-before) claim for clock skew protection
- Log warnings in _try_mwt_auth on missing secret and decode failures
instead of silent swallow
- Rename _csrf_protect_unless_jwt to _csrf_protect_unless_token (accuracy)
- decode_mwt logs at DEBUG level on failures for observability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>