Files
mizan/OWED_SURFACE.md
Ryth Azhur e00b3a177e Docs carry the shape of the system, not its status
MIZAN.md's phase-numbered implementation order, PRODUCT_ARCHITECTURE's
"Deferred until Render revenue funds it" and "Shipped in", PSR_VS_EDGE's
current-state section, and the READMEs' passing-test counts were all reporting
where the work stood rather than what the system is. OWED_SURFACE keeps its
subject — surface that is specified but unbuilt — stated as the shape each unit
owes.

The channel sections follow the renamed slots: Params / ClientMessage /
ServerMessage, and Channel as the base class on both backends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:03:29 -04:00

78 KiB
Raw Blame History

Owed Surface

Contract

This document declares, per crate/package unit, the behavioral mechanisms the unit owes to substantiate the documentation's claims.

  • The owed surface is derived from the documentation's CLAIMS. For each non-trivial claim, it enumerates the behaviors the code must exhibit to prove the claim — to a hostile auditor, an IP lawyer, and a paying customer — at maximal performance, efficiency, and hygiene, never a minimal technicality.
  • A mechanism is stated as observable behavior with the criterion that distinguishes its maximal realization from a degenerate stub, observably enough that a skeptic can check it.
  • A mechanism is owed by the unit's charter and claims. The enumeration is derived from the claims, never from an inventory of the source, so it does not move when the source does. Whether a given mechanism holds right now is answered by the test suite, CI, git history, and the issue tracker.
  • A unit is one crate/package/build target, identified by its root path, sized to emit whole in one .pack. A .pack targets the units whose roots contain its files.
  • This is a declared contract the authoring agent holds and honors when it emits a pack. The PreToolUse gate enforces only that a stance is declared (this document, or an exemption) before code is authored; it does not test the packs.

The AFI's single load-bearing thesis (README.md, docs/AFI_ARCHITECTURE.md § Why the AFI shape): the backends × frontends quadratic collapses to linear because one KDL IR is the only contract that crosses the backend↔frontend boundary. Every mechanism below is, in the end, in service of that: N backends emit byte-identical KDL for the same registered functions, and M frontends are generated from it, so a bug can only live in the KDL contract or its edges — nowhere in between.

backends/mizan-django/src/mizan exceeds the single-emit budget (137K est. tokens); it is decomposed below into sub-units along the documented feature seams (dispatch, cache, channels, forms, shapes, ssr, jwt, registration/export) plus its verification harness, which itself exceeds budget and is cut into two test sub-units at the protocol-vs-adversarial seam. Every other unit emits whole.


Unit: mizan_core (cores/mizan-python/src/mizan_core)

Charter. The framework-agnostic Python substrate every Python backend adapter stands on: the @client decorator and the function-to-IR machinery, the registry, canonical KDL IR emission, HMAC cache-key derivation, cache backends, MWT identity, and the type-introspection helpers the adapters share. It owns language-level primitives; it does not own transport, dispatch, or any Django/FastAPI mechanics.

Claims substantiated here.

  • Client Function RPC — decorated functions carrying the full variadic/kwarg set (INVARIANTS.md § Client Function RPC).
  • Named Contexts — functions sharing a context name grouped at registration into one provider/one fetch (INVARIANTS.md § Named Contexts; MIZAN.md §12).
  • Mutation Invalidation & merge — affects=/merge= carried in the IR, never middleware (INVARIANTS.md § Mutation Invalidation; MIZAN.md §4).
  • Auth as a property of the declared function, carried in the IR (INVARIANTS.md § Auth; MWT_SPEC.md § Usage rule).
  • Canonical KDL IR — every backend emits KDL describing functions/contexts/types/invalidation graph; the IR is the only contract (INVARIANTS.md § Canonical IR & Codegen; docs/AFI_ARCHITECTURE.md § KDL is the IR).
  • HMAC cache keying with cross-language conformance (docs/CACHE_KEYING.md § Invariant).
  • MWT identity layer (docs/MWT_SPEC.md).
  • Free origin-side cache implementing the full protocol locally (docs/PRODUCT_ARCHITECTURE.md § Origin-side cache).
  • File Uploads — Upload first-class end to end through IR (INVARIANTS.md § File Uploads).

Owed behavioral mechanisms.

Client Function RPC / decorator:

  • @client accepts the full declared set (context, affects, merge, private, route, methods, websocket, auth, rev, cache) and synthesizes a Pydantic Input model from the function signature (skipping the request param) — observable: a decorated fn with (request, a: int, b: int) yields an Input with two typed fields; input validation rejects a="x" before the body runs.
  • the return annotation decides wire shape: a primitive/dict return is wrapped as {result: …}, while BaseModel / list[BaseModel] / Optional[BaseModel] pass through bare — observable: -> list[Item] reaches the wire as a bare JSON array, -> int as {"result": n}; a missing return annotation raises TypeError at decoration (not a silent Any).
  • context= and affects= (and merge=) are enforced mutually exclusive at decoration — observable: @client(context=X, affects=Y) raises ValueError, so a function cannot be simultaneously a reader and a mutation.
  • auth= is normalized and validated at decoration (True"required", callables kept, "staff"/"superuser" allowed) — observable: @client(auth="admin") raises ValueError naming the valid set, not a runtime surprise at dispatch.

Named Contexts grouping (the "one provider, one fetch" invariant's registry half):

  • the registry groups every function by its context string so a named context is a single fetch unit, never N callables — observable: two @client(context="user") functions produce get_context_groups()["user"] == [both names]; "global" is just a reserved name in the same map, not a separate mechanism.
  • mixing socket and non-socket transport within one context is a registration-time error (INVARIANTS.md § WebSocket Support) — observable: registering a websocket=True fn and a plain fn under the same context= raises at registration rather than producing a context half of whose members are unreachable over the bundle fetch.
  • receive defined without send, and affects referencing a non-existent context or function, are registration-time errors (MIZAN.md §6) — observable: validate_registry() raises on an affects target that resolves to neither a registered context nor a registered function, and on a receive with no paired send, so a typo cannot reach codegen as a silently-dead invalidation edge.

Canonical KDL IR (build_ir) — the contract every codegen target reads:

  • IR is emitted in a canonical order independent of registration order (functions alphabetical by wire name, contexts alphabetical, params alphabetical, shared-by sorted) — observable: registering the same functions in two different orders yields byte-identical KDL; this is the property the three-way parity test rests on.
  • types are introspected from the Pydantic models directly (never routed through JSON-Schema $ref), producing struct / alias{list} / enum / optional / union shapes under canonical <camelName>Input / <camelName>Output names, with Vec-element sub-types surfaced — observable: a -> list[OrderOutput] fn emits type "userOrdersOutput" { alias { list { ref "OrderOutput" } } } AND a type "OrderOutput" { struct … }; -> Model | None sets output-nullable #true.
  • context param elevation is computed in the IR: a param is required #true iff every member of the context declares it, with shared-by naming the declarers — observable: a two-function user context where both take user_id emits param "user_id" { type "integer"; required #true; shared-by … }; if only one declares page, page is required #false.
  • private and view-path functions are omitted from the emitted function set, and channels are emitted from the channels registry extension — observable: @client(private=True) never appears in the KDL (so it can carry invalidation without being client-callable); a registered channel emits a channel node with its pascal-name and message-type refs.
  • channel slot names in the IR are backend-neutral and named from the client's side: params, client-message, server-message, in that order — observable: every emitter and parser iterates the three slots in that order, so a channel declared on Django and the same channel declared on FastAPI emit the identical channel node.
  • wire_to_pascal is the single derivation of a channel's emitted type names — the wire name split on [._-], each part title-cased and joined, yielding <Pascal>Params / <Pascal>ClientMessage / <Pascal>ServerMessage — observable: a backend that also publishes an OpenAPI slot table names each type through this same function, so the two documents cannot disagree about what one type is called.

HMAC cache keying (protocol-critical cross-language identity):

  • derive_cache_key produces ctx:{context}:{hmac_hex} over a JSON-canonical sorted form with param values normalized to JSON-native strings (True"true", None"null") and user_id omitted for public content — observable: the pinned test vectors (ctx:user:605a1ca5… public, ctx:user:30fc08eb… user-scoped) match the TypeScript adapter byte-for-byte; param ordering does not change the key; the ctx: prefix supports broad SCAN.
  • key derivation resists delimiter collision and versions on rev — observable: context="user", user_id="12" and context="user1", user_id="2" produce different keys; bumping rev produces a new key, so old entries become unreachable orphans without a purge.

MWT identity layer:

  • create_mwt places kid in the JOSE header per RFC 7515 (not the payload) and computes pkey as sha256 over sorted(get_all_permissions()) plus staff/super flags, with aud and nbf claims — observable: decode_mwt reads kid from the header; a token minted for one audience decodes to None under another; pkey is deterministic for identical permission state and changes the instant a permission is added.
  • MWTUser is built entirely from claims with no DB query — observable: constructing MWTUser(payload) sets pk/is_staff/is_superuser/pkey from the token alone; an expired token decodes to None.

Cache backends:

  • MemoryCache and RedisCache both implement get/set/delete plus prefix-scoped purge; the Redis broad purge SCANs ctx:{context}:* and UNLINKs, never a full flush — observable: delete_by_prefix("ctx:user:") removes only user entries and leaves ctx:products:* and foreign-prefixed keys intact; RedisCache applies a TTL safety-net on every set.

Type-introspection helpers (shared so backend parity cannot drift):

  • is_structured_output recognizes BaseModel / Optional[BaseModel] / container-of-BaseModel as no-wrap, and types_match_for_merge accepts direct / list-upsert / list-replace shape matches — observable: a slot typed list[T] matches a value typed T (upsert-by-id), and a multi-arm A | B | None union is returned as-is by extract_optional, not silently narrowed to one arm.

File Uploads:

  • an Upload type is a first-class argument carried through IR, codegen, and dispatch binding, bound from multipart over HTTP and from the envelope over IPC (INVARIANTS.md § File Uploads) — observable: a function declaring an Upload parameter emits a distinguished IR shape rather than degrading to an opaque string, and dispatch binds a real file object the body can read.

Unit: mizan-rust core (cores/mizan-rust)

Charter. The Rust analog of mizan_core: the IR data model, a KDL emitter that is byte-equivalent to the Python emitter, the compile-time (linkme) registry, the runtime invalidation/merge resolvers the HTTP and Tauri adapters call, and the cross-function graph checks. It owns the Rust side of the same IR contract; it does not own transport.

Claims substantiated here.

  • Canonical KDL IR — "the IR must be validated against multiple adapters"; Rust is an IR authority (docs/AFI_ARCHITECTURE.md § KDL is the IR; README.md note 6).
  • Mutation invalidation auto-scoping (three-tier) and merge on the Rust adapters (README.md § Adapters; § Merge via mizan-tauri/mizan-rust-axum).
  • The IR is the only contract — divergence between adapters is what it exists to prevent (docs/AFI_ARCHITECTURE.md § KDL is the IR).

Owed behavioral mechanisms.

Byte-equivalent KDL emission:

  • build_ir() produces KDL byte-identical to the Python emitter against the same registered functions/types/contexts — observable: cores/mizan-rust/tests/afi_parity.rs and the three-way tests/afi/test_codegen_parity.py diff Rust output against the canonical Python-emitted afi_ir.kdl and require exact equality (line-by-line failure on any drift).
  • the emitter reproduces the Python emitter's canonicalization exactly: alphabetical functions/contexts, sorted params, shared-by, snake→camel conversion, primitive-alias/enum inlining, and tree-shaking to types reachable from a registered function's input/output — observable: a #[derive(Mizan)] type not referenced by any function is omitted; an Alias(Primitive) or Enum named type inlines at its reference site instead of emitting a standalone type node, matching the Python output.
  • channel nodes emit the same three client-named slots in the same order as the Python emitter (params, client-message, server-message) with the same pascal derivation — observable: the three-way parity fixture carrying a channel diffs byte-identical across Django, FastAPI, and Rust, so no backend can reintroduce a backend-shaped slot name into a backend-neutral IR.

Compile-time registry:

  • TYPES / CONTEXTS / FUNCTIONS are linkme distributed slices populated at the consumer crate's expansion sites, and lookup_function / context_members resolve against them — observable: an IR-export bin that references one symbol per module force-links its registrations; dropping the reference drops the function from the emitted IR (the documented force-link requirement is real, not decorative).

Runtime invalidation & merge (must match the Python executor's semantics):

  • compute_invalidation auto-scopes by matching mutation arg names against the affected context's declared Input params — observable: a mutation carrying user_id against a user context whose members declare user_id emits {context:"user", params:{user_id:…}}, while a non-matching arg emits the bare context string.
  • compute_merges resolves the slot by structural return-type match against context members (via types_match), emitting {context, slot, value} only on a unique match and dropping ambiguous/no-match — observable: with two context members of different output shapes, a mutation's value routes to the single member whose type matches; two matching members drop the merge (fall back to refetch), never a bundle-order guess.

Cross-function graph checks (fail at IR-build time, before any client is emitted):

  • verify_invariants panics with a structured message when an affects/merge target names an unregistered context, when a merge target has no unique matching member, or when a shared context param's type diverges across members — observable: an affects = "ghost" fails codegen with a named error; a merge whose context has two same-type members fails naming both; this is the whole-graph consistency the "IR prevents divergence" claim rests on.

Unit: mizan-rust-macros (cores/mizan-rust-macros)

Charter. The proc macros — #[derive(Mizan)], #[mizan::context], #[mizan::client] — that make the Rust consumer surface author the same registry and IR shapes the Python decorator produces. It owns the compile-time codegen that emits MizanType/FunctionSpec impls and linkme registrations; it does not own runtime behavior.

Claims substantiated here.

  • Rust/Tauri are "the IR authority via the #[mizan::client] macro + linkme registry" (README.md note 6).
  • The #[mizan::client] surface mirrors the Python @client parameter set (backends/mizan-tauri/README.md § Define server functions; backends/mizan-rust-axum README).

Owed behavioral mechanisms.

  • #[derive(Mizan)] emits a MizanType::shape() matching the Python type introspection, honoring serde rename_all/rename so wire names match serialization, and registers a TypeEntry — observable: an enum with #[serde(rename_all="snake_case")] emits IR enum variants in snake form; a struct field r#type emits IR field name type.
  • #[mizan::client] synthesizes a <camelName>Input struct + MizanType impl, registers the canonical <camelName>Input/<camelName>Output type entries (and the Vec element type for list outputs), and implements FunctionSpec::dispatch that deserializes JSON args into the typed input, awaits the body, and serializes the result — observable: async fn user_orders(req, user_id: i64) -> Vec<OrderOutput> registers userOrdersOutput as a list alias plus OrderOutput, and dispatch round-trips typed args; a Result<T, MizanError> return ?-unwraps so user errors surface as the standard envelope, while the IR still sees only the T shape.
  • #[mizan::client] enforces the same mutual-exclusion as Python (context vs affects/merge) and requires an async fn with an explicit return type — observable: #[mizan::client(context = X, affects = Y)] is a compile error; a non-async or return-typeless fn is a compile error.
  • #[mizan::context] emits a ContextMarker with a snake_case (or explicit) name and registers a ContextEntry — observable: #[mizan::context("user")] and #[mizan::context] struct UserCtx both yield NAME == "user"; a non-unit struct is a compile error.
  • input-param wire names strip the Rust _-underscore convention and bridge it with #[serde(rename)] — observable: _user_id: i64 emits IR param name user_id and the synthesized Input renames the JSON key so dispatch deserializes the wire form.

Unit: mizan-rust-ssr (cores/mizan-rust-ssr)

Charter. The embedded-V8 SSR engine, its PyO3 binding, and the anti-RSC guard. It owns rendering a build-time JS bundle to HTML in-process via deno_core, exposing that engine to the Python side across an in-process FFI boundary, and the structural guarantee that the SSR surface never imports an RSC/Flight runtime. It does not own the Django template backend (that is mizan-django/ssr) or the bundling that produces the JS bundle (that is mizan-generate).

Claims substantiated here.

  • SSR is hand-rolled; no frontend adapter imports an SSR runtime or meta-framework (Next/Nuxt/SvelteKit/RSC/Flight) — the CVE-2025-55182 pre-auth-RCE deserialization class (HOLOMORPHICS/Mizan project note; MEMORY: mizan-ssr-no-framework-runtimes; enforced by cores/mizan-rust-ssr/tests/no_rsc.rs).
  • SSR renders synchronously from props, injected as validated data (the AFI provides the typed one-way version).
  • The Python side binds the engine through PyO3 — in-process FFI, no subprocess, no JSON-RPC framing; behavior lives in the binary, languages bind to it (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary).

Owed behavioral mechanisms.

  • the engine composes a real deno_web web-platform layer (TextEncoder/Decoder, MessagePort, timers) rather than a partial shim, evals the trusted bundle once, and renders per request — observable: the fixture bundle renders Hello, World!; a missing global fails loudly at render, not silently (a partial polyfill is silent-failure-shaped, which is why deno_web's real impls carry this).
  • props cross as a v8::json::parsed value passed as a function argument, never spliced into evaluated source — observable: the injection test feeds a prop string crafted to break out of a string-built call; it renders as inert text and does not set a global, so code injection is structurally absent.
  • the no-RSC guard scans authored SSR source and dependencies for the forbidden token set (react-server-dom, renderToReadableStream, renderToPipeableStream, createFromReadableStream/Fetch, use server, next/, nuxt, @sveltejs/kit) and fails on presence — observable: adding any RSC/Flight/meta-framework import to the scanned fixtures turns no_rsc.rs red; absence alone is not the guarantee — re-entry is loud.
  • a PyO3 binding exposes SsrEngine to the Python side as an in-process extension: construct-from-bundle and render(props_json) -> HTML cross the FFI boundary with no process spawn, honoring V8's one-isolate-per-engine / non-Send constraint so the caller holds one engine per (worker thread, bundle) — observable: the Django backend imports the engine and calls render(props) in-process, and a prop still crosses as a parsed value, so the injection guarantee survives the FFI hop.

Unit: mizan-django dispatch (backends/mizan-django/src/mizan/client)

Charter. The Django HTTP/RPC dispatch surface: the executor that validates input, enforces auth, runs the function, and branches RPC-vs-view; the invalidation and merge resolvers; the context-bundle fetch; JWT/MWT request authentication. It owns per-request Django dispatch semantics; it does not own the registry, the IR, or the cache implementation (it calls them).

Claims substantiated here.

  • RPC call dispatch returning {result, invalidate} and merge (README.md; MIZAN.md §4).
  • Named-context bundle fetch — one GET returns all functions in the context, never N round-trips (INVARIANTS.md § Named Contexts; MIZAN.md §3).
  • Mutation invalidation with three-tier auto-scoping; on failure nothing invalidates; developer writes no cache key (INVARIANTS.md § Mutation Invalidation).
  • Invalidation travels two transports — the JSON body and the X-Mizan-Invalidate header — because a view-path response has no JSON body to carry it (INVARIANTS.md § Mutation Invalidation).
  • Return-type branching: a data return takes the RPC path, an HttpResponse return takes the view path (backends/mizan-django/README.md § @client parameters).
  • Auth enforced at dispatch, rejecting before the body runs, identically across transports (INVARIANTS.md § Auth; MWT_SPEC.md § Usage rule).
  • Origin-side HMAC cache read/write on context fetch; cache=False/rev policy (docs/CACHE_KEYING.md; docs/PRODUCT_ARCHITECTURE.md § Spec surface).
  • MWT/JWT server-side auth enforcement in the executor (_check_auth_requirement) (docs/MWT_SPEC.md § Usage rule).

Owed behavioral mechanisms.

Dispatch & validation:

  • execute_function validates input against the function's Pydantic Input before invoking the body, and rejects private functions from RPC — observable: a missing required field returns VALIDATION_ERROR with per-field detail and the body never runs; a private=True function returns FORBIDDEN when called over /call/.
  • output serialization walks BaseModel/list/dict recursively via to_jsonable_python so list[BaseModel] reaches the wire as a bare array — observable: a -> list[Item] function returns [{…},{…}], not {"result":[…]}; an Optional[Model] returning None serializes to null not {"result":null}.

Named-context bundle fetch (single request, param-filtered):

  • execute_context runs every function in the group in one request, passing each only the params it declares, and fails the whole bundle if any member fails auth/validation — observable: GET /ctx/user/?user_id=5&page=3 returns {user_profile:…, user_orders:…} where user_profile never sees page; if one member requires auth and the request is anonymous, the whole fetch returns the auth error, not a partial bundle.

Three-tier invalidation (the invariant that separates the AFI from typed RPC):

  • _resolve_invalidation auto-scopes by matching mutation args against context param names (Tier 1), falling back to the bare context (Tier 3), and resolves function-level affects to the function name — observable: update_profile(user_id=5,…) against a user context emits [{context:"user", params:{user_id:5}}]; a mutation whose args don't overlap emits ["user"]; affects="user_profile" emits the function name as the key.
  • invalidation is emitted on both transports and only on success — observable: a successful mutation carries both response["invalidate"] (JSON body) and X-Mizan-Invalidate: user;user_id=5 (header, URL-encoded so q=hello worldq=hello%20world and semicolons survive a parse round-trip); a mutation that raises emits neither.
  • _resolve_merges resolves the merge slot server-side by matching the mutation's Output type against context members' Output types (types_match_for_merge), emitting {context, slot, value, params?} only on a unique match — observable: with morph_groups: list[MorphGroupMeta] and morph_layers: list[MorphLayer] in one context, a mutation returning MorphLayer merges into morph_layers only; the kernel does no shape inference.

Auth enforced before the body:

  • _check_auth_requirement runs before view.call, handling required/staff/superuser/callable and mapping to UNAUTHORIZED/FORBIDDEN — observable: an anonymous call to @client(auth=True) returns UNAUTHORIZED and the function body never executes; a callable raising PermissionError surfaces its message as FORBIDDEN.
  • MWT is checked first (X-Mizan-Token), then JWT (Authorization: Bearer), then session+CSRF; a present-but-invalid token is rejected (never a silent fall-through to session) — observable: an invalid X-Mizan-Token returns 401 without trying session auth; a valid MWT sets request.user = MWTUser with no DB query; CSRF is enforced only on the session path.

Return-type branching + origin cache:

  • a function returning an HttpResponse takes the view path (invalidation rides the header, Cache-Control: no-store), while a data return takes the RPC path — observable: a -> HttpResponseRedirect mutation returns the 302 with X-Mizan-Invalidate set; the same-decorated -> Shape mutation returns JSON with invalidate in the body.
  • context fetch consults the origin cache keyed by the effective rev (max across members) and effective cache policy (False short-circuits), stores deterministic (sorted-key) JSON on miss, and purges scoped/broad on mutation — observable: two identical fetches return byte-identical bodies and the second carries X-Mizan-Cache: HIT; a scoped mutation for user_id=5 purges only that entry and leaves user_id=6 a HIT; a context with any cache=False member emits no-store.

Unit: mizan-django cache (backends/mizan-django/src/mizan/cache)

Charter. The Django-side origin cache facade over mizan_core's backends and key derivation — the free, unit-testable local cache that implements the same HMAC key and purge semantics as the paid Edge. It owns cache lifecycle/config resolution and the scoped-vs-broad purge dispatch; it does not own key derivation (delegates to core).

Claims substantiated here.

  • Free framework origin-side cache implementing the full cache protocol locally, same HMAC key and purge as Edge (docs/PRODUCT_ARCHITECTURE.md § Origin-side cache; docs/CACHE_KEYING.md § Cache architecture).
  • Scoped purge recomputes the key and deletes directly; broad purge SCANs the ctx:{context}:* prefix (docs/CACHE_KEYING.md § Required operations).

Owed behavioral mechanisms.

  • cache_purge recomputes the exact HMAC key for a scoped purge (one DELETE) and prefix-scans for a broad purge, so scoped invalidation touches exactly one entry — observable: cache_purge(ctx, {user_id:5}, secret) deletes only user 5's entry (returns 1) and leaves user 6; cache_purge(ctx) with no params removes every entry under the prefix.
  • cache enablement is gated on both cache_secret and cache_redis_url present, thread-safe and lazily initialized — observable: with only one configured, caching is disabled and logged; concurrent get_cache() calls initialize once.
  • the operability obligations the "same protocol as Edge, security-critical" claim rests on hold under concurrency and across languages: purge atomicity, cross-language stringification for every value type rather than bool/None alone, per-param sub-index cleanup on broad purge, single-flight protection against a thundering herd, one argument shape shared by cache_get/cache_put, and RedisCache exercised by the same suite as MemoryCache — observable: an index read racing a delete cannot resurrect a purged key; a broad purge leaves no orphaned per-param sub-index behind; N simultaneous misses on one key issue one origin fetch; a float or nested value stringifies identically in Python and TypeScript.

Unit: mizan-django channels (backends/mizan-django/src/mizan/channels)

Charter. The WebSocket transport: the Channel base + registry, the multiplexed consumer that handles channel subscribe/message and RPC-over-WS, server push, and channel schema export. It owns real-time bidirectional messaging and WS-transported RPC; it does not own HTTP dispatch (reuses the executor).

Claims substantiated here.

  • WebSocket support: websocket= dispatched over a persistent connection; server-initiated messages reach subscribed contexts; declaration and wire semantics uniform across adapters (INVARIANTS.md § WebSocket Support).
  • A channel is typed in both directions — typed params, a typed client→server message, a typed server→client message — so real-time traffic carries the same type contract RPC does (INVARIANTS.md § WebSocket Support; backends/mizan-django/README.md § Channels).
  • Channels compose into the IR channel section (docs/AFI_ARCHITECTURE.md — codegen channels target consumes the channel nodes).
  • Auth/authorization checked before any channel or RPC body runs (INVARIANTS.md § Auth; consumer security).

Owed behavioral mechanisms.

  • the consumer multiplexes many channel subscriptions and RPC calls over one socket, keyed by (channel, params_json), and validates Pydantic params/messages before authorize/receive — observable: subscribing with a wrong-typed param returns an error before authorization; a duplicate subscription to the same (channel, params) is rejected; unsubscribe leaves zero lingering subscriptions after rapid subscribe/unsubscribe cycles.
  • WS-RPC only dispatches functions explicitly marked websocket=True, running the same execute_function (so validation/auth are identical to HTTP) — observable: an RPC call to an HTTP-only function returns FORBIDDEN ("use POST /call/"); a WS call to a websocket=True fn returns the same envelope shape as HTTP; a missing id/fn returns a structured error.
  • authorize() gates every subscription and exceptions in it are contained — observable: authorize returning False blocks the subscribe with "Not authorized"; an authorize that raises returns an error rather than crashing the socket; room-level authorization enforces per-param access (room 1 allowed, room 999 rejected).
  • server push (Channel.push) broadcasts to the channel-layer group, converting Pydantic to JSON, so a server function can reach subscribers — observable: ChatChannel.push(room="general", message=…) sends to chat_general with the message body; push with no channel layer configured warns rather than raising.
  • a channel declares its wire types under the backend-neutral slot names the Channel base reads — Params, ClientMessage (travels client → server), ServerMessage (travels server → client) — and any slot left undeclared makes that direction unavailable — observable: a model declared under any other attribute name is invisible to the registry extension and to the IR, so the channel is exported as though that direction were absent.
  • channel schema is exported into the registry's channels extension carrying the params / client_message / server_message shapes the channel declares plus a bidirectional flag — observable: a channel declaring a ClientMessage reports bidirectional: true; a push-only channel reports false and omits client_message while still carrying server_message; the KDL channel node names the same three slots as params / client-message / server-message, and the codegen channels target emits the matching typed envelopes and useXChannel hook.
  • the OpenAPI channel document names each slot type through mizan_core.ir.wire_to_pascal, the same derivation the IR uses, and tabulates the per-channel slots under x-mizan-channels — observable: the paramsType / clientMessageType / serverMessageType entries and the hasParams / hasClientMessage / hasServerMessage flags name exactly the types the KDL channel node refers to, so the OpenAPI view and the IR view of one channel cannot disagree.
  • JWT auth over the WS handshake authenticates from the ?token= query param without a DB query, taking precedence over session — observable: a valid access token sets scope["user"] to a JWTUser from claims; an invalid token falls back to session rather than rejecting the socket.

Unit: mizan-django forms (backends/mizan-django/src/mizan/forms)

Charter. The Forms composition: mizanFormMixin/mizanFormMeta turning a Django Form into the three role-tagged server functions (schema/validate/submit), plus formsets, and the field schema/validation projection. It owns Django-Form-to-server-function translation; it does not own generic RPC dispatch. Auth-provider (django-allauth) forms are out of scope — the docs place them in a dedicated external mizan-allauth repository built on this mixin; this unit owes only the primitive they build on.

Claims substantiated here.

  • Forms are three role-tagged client functions (schema / validate / submit) plus field validation, composed from RPC + validation (INVARIANTS.md § Compositions — Forms).
  • A formset is the same three-role composition applied to a collection, so it introduces no fourth role (backends/mizan-django/README.md § Forms).
  • Auto-registers {name}.schema / .validate / .submit; frontend gets useXForm() (backends/mizan-django/README.md § Forms).

Owed behavioral mechanisms.

  • mizanFormMixin.__init_subclass__ auto-registers exactly three role-tagged server functions per concrete form (and formset variants when enabled), carrying form/form_name/form_role meta — observable: defining a ContactForm with a mizanFormMeta(name="contact") registers contact.schema, contact.validate, contact.submit; a form without a mizan attribute registers nothing; enabling enable_formset adds contact.formset.{schema,validate,submit}.
  • the schema function projects each Django field into a typed FieldSchema (mapping field classes to Python types, extracting choices from ModelChoiceField safely, serializing initial values) and carries the mizanFormMeta display/behavior settings — observable: a CharField/EmailField/Textarea form yields three typed fields with correct type/widget; a ModelChoiceField yields JSON-serializable {value,label} choices (no ModelChoiceIteratorValue leak).
  • validate runs the real Django form validation and returns structured per-field errors; submit branches multipart-vs-JSON, calls the form's on_submit_success/on_submit_failure, and returns pass/fail with data — observable: submitting an invalid email returns field errors and success: false; a valid submit runs on_submit_success and returns its data; a multipart submit binds files.
  • create_form_instance threads request/user/instance init kwargs into the Django form and gracefully drops any the form doesn't accept, so the mixin is a reusable primitive for forms that need request context (the base the external mizan-allauth repo builds on) — observable: a form declaring a request kwarg receives it; a form that doesn't accept request still instantiates rather than raising TypeError.
  • a forms codegen target emits the form clients against mizanCall from the kernel, so the frontend form surface stands on no hand-written provider — observable: useXForm() is generated from the IR's is-form/form-name/form-role fields and reaches the server through the kernel, exactly as every other generated client does.

Unit: mizan-django shapes (backends/mizan-django/src/mizan/shapes)

Charter. The "API Shapes" primitive: Pydantic-typed queryset projection over django-readers, PK-keyed structural diffing (add/modify/delete) across nested relations. It owns ORM projection and diff derivation; it does not own dispatch.

Claims substantiated here.

  • API Shapes to the fullest extent: ORM integration, auto-diffing by primary key (add/modify/delete, Django as reference), authorable near the used function (INVARIANTS.md § API Shapes).
  • A Shape is one declaration serving as both the wire type and the query plan — the Pydantic field set compiles to a django-readers projection (INVARIANTS.md § API Shapes).
  • Context classes send/receive with Shape diffing (INVARIANTS.md § Compositions — Context classes; MIZAN.md §5).

Owed behavioral mechanisms.

  • Shape.query compiles a django-readers projection from the Pydantic field set + nested Shapes, executing minimal queries (single query for flat, prefetch for nested) and validating each row — observable: a flat shape query runs one SQL query; a nested AuthorCardShape with books runs two (prefetch), not N+1; per-relation querysets filter nested rows (books=lambda qs: qs.filter(is_published=True)).
  • diffing computes add/modify/delete by primary key across nested relations, using a single batched query for existing rows and strict access to nested diffs — observable: diff_many of mixed new+existing items runs one query for the existing set; a nested diff reports created/updated/deleted by child PK; accessing a mistyped nested name raises (KeyError/AttributeError) rather than silently returning empty.
  • PK/type resolution handles integer, slug, and UUID primary keys, two FKs to the same model, self-referential and nullable FKs, and treats False/0/"" as present values — observable: a UUID-PK Section shape diffs correctly; is_published=False is not treated as missing; a nullable editor FK returns None rather than erroring.
  • the ReactContext('name') class form carries send/receive and a POST /ctx/<name>/commit/ endpoint that routes committed shape data to receive, with auto-refetch-or-fresh-return after commit (INVARIANTS.md § Compositions; MIZAN.md §5) — observable: a class defining send/receive generates a read hook and a commit function; committing runs receive and either refetches the context or splices the Shape receive returned, so the client never holds a post-commit stale bundle.

Unit: mizan-django SSR (backends/mizan-django/src/mizan/ssr)

Charter. The SSR product's Django half: a Django template backend that renders .tsx/.jsx component files by resolving each to its built bundle and driving the PyO3-bound SsrEngine in-process, wrapping output with a hydration payload. It owns the Django-template-engine integration, component-to-bundle resolution, prop gathering, and the per-(worker-thread, bundle) engine lifecycle; it does not own the V8 render (that is cores/mizan-rust-ssr's SsrEngine) or the bundling that produces the render bundle (that is mizan-generate).

Claims substantiated here.

  • SSR is a Django template backend replacing the rendering engine; the template name IS a .tsx/.jsx file path; context dict becomes props; output wrapped in <div id="mizan-root"> + window.__MIZAN_SSR_DATA__ hydration (docs/SSR_ARCHITECTURE.md).
  • The render engine is embedded-V8 inside the Mizan Rust binary, bound via PyO3 — in-process FFI, no external JS runtime serving requests, no subprocess, no JSON-RPC framing (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary).
  • The component path resolves against DIRS to its built bundle in OPTIONS['bundles'] (produced by mizan-generate's SSR bundling step); the backend gathers props and wraps output for hydration (docs/SSR_ARCHITECTURE.md § AFI boundary; the TEMPLATES config block).
  • One engine per (worker thread, bundle) — V8's Locker constraint makes an engine non-Send, so the Django side never shares one across threads (docs/SSR_ARCHITECTURE.md § The engine).
  • SSR is orthogonal to RPC and composable; first paint carries data (INVARIANTS.md § SSR).

Owed behavioral mechanisms.

  • MizanTemplates implements Django's template-backend interface: get_template(name) resolves name as a .tsx/.jsx file path under DIRS and returns a MizanTemplate for the resolved component; render strips request/csrf_token and passes the remaining context as props — observable: render(request, 'components/Hello.tsx', ctx) renders that component with ctx as props; from_string raises (it renders files, not strings); a missing file raises TemplateDoesNotExist.
  • component-to-bundle resolution: the backend resolves each component file to its self-contained render bundle in OPTIONS['bundles'] (the mizan-generate-produced bundle assigning globalThis.renderApp) and hands the bundle to the engine, rather than reading component source directly — observable: render of components/Hello.tsx loads that component's built bundle from the configured bundles directory; a component with no built bundle raises rather than rendering stale or empty HTML.
  • rendered output is wrapped for client hydration — observable: output contains <div id="mizan-root">…</div> plus <script>window.__MIZAN_SSR_DATA__={sorted-json}</script>, so first paint carries the props the client hydrates from.
  • engine lifecycle: the backend constructs one SsrEngine per (worker thread, bundle) pair and reuses it across requests, never sharing a single engine across threads (the engine is non-Send) — observable: concurrent renders on different worker threads each use their own thread-local engine and return correct results with no interleaving; the engine is built once per bundle, not per render.
  • the Django backend binds the engine through PyO3 and renders in-process: no external JS runtime process, no newline-delimited JSON-RPC correlation, no ready-signal or auto-restart machinery, and OPTIONS carries bundles (a directory of built bundles) rather than worker (a JS entry file) — observable: a render spawns no subprocess and calls the PyO3-bound SsrEngine.render(props) directly, so a render failure is a Python exception rather than a lost correlation id.
  • render-on-mutation orchestration (mutation → trigger local render → store HTML) is driven by the manifest's render_strategy, wiring the engine to the PSR path (docs/PSR_VS_EDGE.md) — observable: a mutation against a public context triggers a local re-render and stores the HTML, so the next request for that page is served pre-rendered rather than rendered on demand.

Unit: mizan-django JWT/MWT (backends/mizan-django/src/mizan/jwt)

Charter. The Django identity layer: JWT access/refresh tokens tied to sessions, the MWT-mint server functions, JWT settings/algorithm resolution, and the Ninja security class. It owns Django-session-bound token issuance and validation; the MWT format itself lives in mizan_core.mwt.

Claims substantiated here.

  • JWT auth is session-bound: the access/refresh pair is auto-detected at dispatch, and CSRF is handled on the session path only (backends/mizan-django/README.md § Setup; INVARIANTS.md § Auth).
  • MWT is issued from an authenticated identity; create_mwt(user, secret, ttl, audience, kid); a separate JWT module carries user-auth tokens (docs/MWT_SPEC.md § Key decisions).
  • MWT is the cache-keying identity, not a replacement for JWT auth (docs/MWT_SPEC.md).

Owed behavioral mechanisms.

  • JWT tokens carry sub/sid/staff/super/type/iat/exp and are tied to a session key so logout revokes them — observable: a refresh whose underlying session was destroyed returns None (immediate revocation); an access JWTUser is built from claims with no DB query; decode_token enforces the expected token type.
  • settings auto-detect algorithm from key shape (PEM→RS256 else HS256) and derive the public key from the private RSA key when absent — observable: an HS256 secret works with public_key == private_key; a PEM private key auto-selects RS256 and extracts the public key.
  • mwt_obtain mints an MWT from the authenticated session via create_mwt, requiring MIZAN_MWT_SECRET, and jwt_obtain/jwt_refresh issue/rotate the JWT pair carrying user claims — observable: mwt_obtain on an anonymous request raises; with no secret configured it raises a clear config error; the JWT pair includes is_staff/is_superuser so downstream auth needs no DB query.

Unit: mizan-django registration & export (backends/mizan-django/src/mizan/export, .../management, .../setup, .../__init__.py, .../urls.py, .../_vendor)

Charter. The Django discovery/registration glue and the two protocol export surfaces: the Edge manifest generator and the KDL IR management command, plus URL wiring, session-init, and the ASGI/channels wrapper. It owns clients.py auto-discovery and the manifest/IR export commands; it delegates registry and IR shape to mizan_core.

Claims substantiated here.

  • Codegen IR export (KDL) via python manage.py export_mizan_ir (backends/mizan-django/README.md § Generate the frontend; docs/AFI_ARCHITECTURE.md § KDL is the IR).
  • The Edge manifest is a deterministic (sorted) derivation of the registry covering both RPC and view-path functions, and records each context's render_strategy (docs/PSR_VS_EDGE.md § PSR — Preemptive Static Rendering).
  • Function discovery / registration via the clients.py convention (backends/mizan-django/README.md § Setup; MIZAN.md §6).
  • Session / CSRF init endpoint; wrap_asgi WebSocket routing (backends/mizan-django/README.md § Setup).

Owed behavioral mechanisms.

  • export_mizan_ir populates the registry via discovery, then writes canonical KDL from mizan_core.ir.build_ir — observable: the Django-emitted KDL is byte-identical to the FastAPI and Rust emissions for the same fixture (tests/afi/test_codegen_parity.py); this is the "IR is the only contract, validated against multiple adapters" claim made checkable.
  • generate_edge_manifest emits a deterministic (sorted contexts and mutations) JSON mapping contexts to endpoints/params/functions, distinguishing rpc vs view path, marking user_scoped and render_strategy (dynamic_cached for user-scoped, psr for public), and mutations with auto-scoped params + private/route — observable: two exports are byte-identical regardless of registration order; a context with user_id is user_scoped+dynamic_cached; a view-path function's route populates page_routes; a mutation whose args match context params lists them under auto_scoped_params.
  • mizan_clients discovers ServerFunction subclasses under each app's clients.py/clients/ layer and registers them idempotently — observable: re-running discovery does not double-register; a class already registered under a different name is skipped rather than clobbered.
  • the session-init view sets the CSRF cookie and returns the token, and wrap_asgi routes /ws/ to the channels consumer — observable: GET /session/ returns {csrfToken} and a Set-Cookie: csrftoken=…, so SSR/clients can establish CSRF before an authenticated call; wrap_asgi(get_asgi_application()) produces a ProtocolTypeRouter dispatching http vs websocket.

Unit: mizan-django protocol tests (backends/mizan-django/src/mizan/tests — test_core.py, test_auth.py, test_ssr.py, test_benchmarks.py)

Charter. The Django backend's protocol-and-integration verification: the executor/registry/invalidation/merge/cache/manifest/edge-compatibility/auth/SSR/throughput suites. It holds the evidence that the dispatch, invalidation, cache, auth, and SSR mechanisms above behave as claimed against the real HTTP stack; it authors no production mechanism.

Claims substantiated here.

  • The dispatch, invalidation, merge, cache, auth, and SSR claims of the mizan-django dispatch/cache/ssr/jwt sub-units are verified here.
  • Edge caching is provable without an Edge — deterministic JSON, correct Cache-Control, header round-trip, auth-differentiated responses (test_core.py § EdgeCompatibilityTests, a doc-shaped claim carried in the suite).

Owed behavioral mechanisms.

  • the suite exercises the real HTTP stack (Django test client / LiveServer) — not just RequestFactory — for dispatch, three-tier invalidation, merge, view-path branching, and cache HIT/MISS/scoped-purge — observable: HTTPIntegrationTests and CacheIntegrationTests assert the JSON body and X-Mizan-Invalidate header agree, that a scoped mutation preserves other users' cached entries, and that a second identical fetch is a HIT.
  • the auth suite covers every axis (JWT valid/invalid/expired, MWT, session, staff/superuser/callable/PermissionError) and asserts the body never runs on failure — observable: an invalid token returns 401 without session fall-through; an anonymous call to an auth-required function returns before the body; a callable's PermissionError message surfaces verbatim.
  • the Edge-compatibility suite asserts the properties a CDN cares about (deterministic byte-identical bodies, sorted JSON keys, URL-encoded delimiter-safe headers, no-store on errors/mutations, header↔body invalidation agreement, auth-differentiated responses for the same URL) — observable: these tests go red if any of those properties regress, so "Edge caching is possible" is checkable without a CDN.
  • the SSR suite verifies the engine-based render path — template-backend resolution, bundle-driven in-process render, the hydration wrapper, and concurrent renders — and asserts that no external JS runtime process is spawned during the suite (the PyO3-bound SsrEngine renders in-process) — observable: a render produces the <div id="mizan-root"> + __MIZAN_SSR_DATA__ wrapper from the resolved bundle, concurrent renders across worker threads each use their own engine and return correct results, and the suite spawns no bun/node subprocess.
  • the benchmark suite measures HTTP-vs-executor overhead and throughput with correctness assertions on each path — observable: every benchmark also asserts the function's numeric output, so a green benchmark run is also a correctness run.

Unit: mizan-django adversarial & feature tests (backends/mizan-django/src/mizan/tests — test_pentest.py, test_security.py, test_channels.py, test_shapes.py)

Charter. The Django backend's adversarial and feature-specific verification: the penetration/security suites (attacker-shaped defenses) and the channels/shapes suites (feature behavior). It holds the evidence that validation, authorization, channel subscription, and shape diffing behave as claimed against hostile and edge inputs; it authors no production mechanism.

Claims substantiated here.

  • The auth-guard, input-validation, and no-info-disclosure claims (INVARIANTS.md § Auth; executor validation) are verified adversarially here.
  • The WebSocket channel authorization/subscription and API Shapes diff/query claims (INVARIANTS.md § WebSocket Support, § API Shapes) are verified here.

Owed behavioral mechanisms.

  • the pentest and security suites assert the properties an attacker probes: validation-runs-before-execution, private/internal functions unreachable over RPC, no sensitive detail in production error messages, injection strings (SQL/command/template/prototype-pollution/unicode-lookalike/zero-width) treated as inert data, and no function-existence timing leak — observable: these tests go red if the executor ever runs a body before validation, leaks a secret in a 500, or executes an injection payload.
  • the channels suite verifies subscription lifecycle and authorization: param validation before authorize, authorize-false and authorize-raise both blocking cleanly, duplicate-subscription rejection, room-level per-param authorization, and WS-RPC gated to websocket=True functions — observable: subscribing to a room the user cannot access is rejected; an RPC to an HTTP-only function returns FORBIDDEN over the socket.
  • every channel fixture these suites register subclasses the Channel base and declares its wire models under the Params / ClientMessage / ServerMessage slot names that base reads — observable: a fixture's declared message model reaches the exported schema and the IR; a model declared under any other attribute name is invisible to both, so the suite would be asserting against a channel the contract sees as slotless.
  • the extension-schema tests pin both directions of the bidirectional flag — observable: a fixture declaring ClientMessage asserts bidirectional true with both client_message and server_message present, and a push-only fixture asserts bidirectional false with client_message absent, so neither the flag nor the slot keys can be renamed without turning the suite red.
  • the shapes suite verifies query efficiency and diff correctness across the hard cases: single-query flat, prefetch nested (no N+1), UUID/slug/int PKs, two-FKs-to-same-model, self-referential and nullable FKs, False/0/"" treated as present, batched diff_many, and strict nested-diff access raising on typos — observable: a nested query asserts exactly the prefetch count; a mistyped nested-diff name raises rather than silently returning empty.

Unit: mizan-fastapi (backends/mizan-fastapi/src/mizan_fastapi)

Charter. The FastAPI adapter targeting the AFI-common subset: RPC dispatch, context bundling, JSON-body invalidation + merge, auth gating, the error envelope, the channel registry extension and socket handler, and the KDL IR CLI. It owns the FastAPI transport surface over mizan_core; Forms/Shapes/SSR are explicitly out of scope.

Claims substantiated here.

  • RPC call dispatch, named-context bundle fetch, JSON-body invalidation, three-tier auto-scoping, function registration, KDL IR export (README.md § Adapters; backends/mizan-fastapi/README.md § Scope).
  • Auth-guard enforcement (auth= rejects) (backends/mizan-fastapi/README.md § Auth integration).
  • The same core primitives as Django, proving the protocol is not Django-specific; IR-shape parity with Django and Rust (README.md § Conformance; docs/AFI_ARCHITECTURE.md).
  • Every error path renders through the Mizan envelope; GET /session/ returns a null CSRF token for wire parity (backends/mizan-fastapi/README.md § Setup; README.md § Adapters note 7).

Owed behavioral mechanisms.

  • execute_function looks up the registered function, enforces auth before running (matching Django's semantics: True/required/staff/superuser/callable), validates input against the Pydantic Input, awaits view.acall (async handlers on the loop, sync in a threadpool), and serializes via jsonable_encoder — observable: an anonymous call to @client(auth=True) returns 401 before the body; an async def handler runs on the loop (a real await inside completes); list[BaseModel]/Optional[BaseModel] reach the wire bare.
  • compute_invalidation auto-scopes by matching args against the context's declared Input fields, emitting a bare context or a {context, params} object — observable: a mutation with a matching arg emits the scoped form, a non-matching arg the bare context string; identical to the Django resolver's output.
  • compute_merges resolves the slot by unique return-type match (types_match_for_merge) and emits {context, slot, value, params?}, dropping ambiguous — observable: the morph_groups/morph_layers fixture routes a MorphLayer mutation to morph_layers only; a merge-only mutation emits merge with empty invalidate.
  • the router exposes POST /call/, GET /ctx/{name}/, GET /session/ and both exception handlers render every failure through {"error":{code,message,details?}} with Cache-Control: no-store — observable: an unknown function returns 404 in the envelope; a malformed body returns BAD_REQUEST; a validation failure returns 422; /session/ returns {csrfToken: null} (parity, since CSRF is Django-only).
  • the Channel base declares the same three backend-neutral slots as the Django base (Params, ClientMessage, ServerMessage) and its channels registry extension exports the same key set (name, type, bidirectional, plus the declared params/client_message/server_message schemas) — observable: a channel declaring ClientMessage reports bidirectional: true and a client_message schema whose entry is comparable key-for-key with Django's entry for an identically-declared channel, so the slot names carry on a backend with no Django in it.
  • the socket handler fans a message out to exactly the subscribers whose params key the group, dropping a socket that fails to take a frame — observable: two subscribers with the same params receive one push; a departed socket is discarded from the group with the failure surfaced rather than swallowed, so one dead client cannot wedge the broadcast.
  • python -m mizan_fastapi.ir <module> imports the module (triggering registration) and writes canonical KDL — observable: its output equals the Django management command's output for the same fixture (three-way parity).

Unit: mizan-rust-axum (backends/mizan-rust-axum)

Charter. The Rust/Axum HTTP adapter: the /call/, /ctx/:name/, /session/ handlers, the error envelope, and app-state threading, dispatching through mizan-core's FUNCTIONS registry. It owns the Axum wire surface; dispatch/invalidation/merge logic is mizan-core.

Claims substantiated here.

  • RPC call dispatch, named-context bundle fetch, JSON-body invalidation, three-tier auto-scoping, KDL IR export (README.md § Adapters; note 6).
  • Axum error envelope mirrors FastAPI's with Cache-Control: no-store (backends/mizan-rust-axum/src/errors.rs).
  • Query params are coerced to typed JSON via the per-function input params (handlers.rs).

Owed behavioral mechanisms.

  • function_call dispatches through lookup_function + FunctionSpec::dispatch, then attaches compute_invalidation and compute_merges output, mirroring the FastAPI response shape {result, invalidate, merge?} — observable: the wire-parity drivers (tests/rust/drive_kernel.rs, drive_emitted.rs) run the same probes against the Axum server and FastAPI and require the same JSON shapes and invalidate/merge semantics.
  • context_fetch bundles every registered member of the context and coerces string query params to typed JSON via each function's input_params primitive table — observable: GET /ctx/user/?user_id=5 returns the flat bundle with user_id coerced to an integer before dispatch; an unknown context returns the envelope 404.
  • app state is type-erased into the handle and downcast in user functions — observable: a handler downcasts RequestHandle to the concrete state type; the stateless router variant threads a unit handle.
  • the adapter honors every declaration it accepts, so no function reaches the wire with a declared property the transport ignores (README.md § Caveat, notes 2/3/5; INVARIANTS.md § Auth): a Transport::Websocket function is routed through a WebSocket handler, an is_form/form_role function is reachable through validate/submit endpoints, and auth= is enforced in the dispatch path — observable: an auth=True function is rejected for an anonymous caller on this adapter exactly as it is on Django and FastAPI, which is what makes "auth enforced on every adapter" a single claim rather than a per-adapter one.

Unit: mizan-tauri (backends/mizan-tauri)

Charter. The Tauri adapter: a plugin exposing a single mizan_invoke command that routes op-tagged call/fetch envelopes through the shared mizan-core registry over Tauri IPC. It owns the IPC wire surface; dispatch/invalidation/merge are mizan-core.

Claims substantiated here.

  • RPC call dispatch, named-context bundle fetch, invalidation (JSON body only), three-tier auto-scoping (README.md § Adapters; note 1).
  • Transport is Tauri IPC (a single #[tauri::command] envelope), not HTTP; invalidation rides the response body; no header channel (README.md note 1; backends/mizan-tauri/README.md § Wire protocol).
  • RequestHandle wraps AppHandle so functions can access managed state; Result<T, MizanError> supported (backends/mizan-tauri/README.md § App-state access).

Owed behavioral mechanisms.

  • the plugin registers exactly one command (plugin:mizan|mizan_invoke) that deserializes the op-tagged envelope and dispatches call/fetch through the same FUNCTIONS/CONTEXTS slices the HTTP adapter uses — observable: {op:"call", fn, args} returns {result, invalidate, merge?} and {op:"fetch", context, params} returns the flat bundle, identical shapes to the axum adapter minus the header channel; there is no per-function #[tauri::command].
  • errors flow through Tauri's reject path re-wrapped into the {code, message, details?} shape — observable: a MizanError::ValidationFailed reaches the JS transport as the same envelope an HTTP 422 would carry, so consumer error handling is transport-agnostic.
  • RequestHandle::new(app) lets a function downcast to tauri::AppHandle for managed state / event emission — observable: a function calling req.downcast::<tauri::AppHandle>() reaches Tauri state; stateless functions ignore the handle.
  • the auth/private fields Tauri's FunctionSpec carries are enforced in the dispatch path (README.md § Caveat, note 5; INVARIANTS.md § Auth) — observable: an auth=-declared function is rejected for an unauthorized caller over IPC, and a private=True function is unreachable through mizan_invoke, so a desktop build cannot be the one transport where a declared guard is decorative.

Unit: mizan-rust client kernel (frontends/mizan-rust)

Charter. The Rust port of the shared client kernel: the reconciled cache (context registry + state), transport (HTTP with retry, CSRF), merge splicing, the debounced invalidation queue, error-envelope parsing, and the PyO3 bridge that exposes the kernel to Python. It owns the client-side reconciled view; framework rendering lives in adapters.

Claims substantiated here.

  • The client kernel owns the reconciled cache — context state, status, error, server-driven merge and invalidate, session init — reached through a pluggable transport; no adapter keeps its own copy of the truth (INVARIANTS.md § Client Kernel; docs/AFI_ARCHITECTURE.md § Kernel model).
  • Mutation invalidation auto-refetches affected contexts; on failure nothing invalidates (INVARIANTS.md § Mutation Invalidation).
  • Merge splices the return value into the cached entry rather than refetching (the merge= path; MIZAN.md §5 fresh-return optimization generalized).
  • Transports are pluggable (HTTP, Tauri IPC, webview) via configure (docs/AFI_ARCHITECTURE.md § Kernel model; frontends/mizan-tauri-transport/README.md).
  • The Python client is a typed facade over this kernel via PyO3 (protocol/mizan-codegen python target; baselines/python/client.py).

Owed behavioral mechanisms.

  • the context registry keys entries by context name + stable_key(params), holds one ContextState {data, status, error} per entry, and notifies subscribers via a watch channel that coalesces to the latest state — observable: stable_key({b,a}) == stable_key({a,b}) (byte-identical to JSON.stringify with sorted keys), so the same params hit the same cache entry regardless of key order; a refetch advances the entry through Loading→Success visible to subscribers.
  • mizan_call applies the response's merge entries first, then queues invalidate entries, then returns result — observable: a mutation response {result, merge, invalidate} splices the merged slot into the cached bundle AND schedules refetch; a failed call (4xx) surfaces the error and invalidates nothing.
  • splice_slot upserts by id into an array slot, replaces an array slot with a new array, replaces a scalar, and no-ops a merge into a slot absent from the bundle — observable: merging {id:1,name:"A"} into [{id:1,…},{id:2,…}] replaces entry 1 in place; merging into a missing slot leaves the bundle untouched (no fabricated slot on a stale cache).
  • the invalidation queue debounces within one async tick, and broad invalidations subsume scoped ones for the same context — observable: two invalidations queued in the same tick flush once; a broad invalidate refetches every param variant while a scoped invalidate refetches only the matching entry.
  • transport is HTTP-with-retry (3 attempts, linear backoff, retry on 5xx/network, surface 4xx immediately), reads the CSRF cookie into the configured header per call, and is swappable — observable: a 5xx retries then errors; a 4xx returns immediately; swapping the transport (Tauri/webview) leaves the generated call/fetch code unchanged (transport read from config).
  • the error envelope parses both the FastAPI nested shape and the Django flat shape, falling back to HTTP_<status> — observable: {"error":{"code":…}} and {"error":true,"code":…} both yield the correct code; an unparseable body yields HTTP_500 with the raw body.
  • the PyO3 bridge exposes call/fetch_context/subscribe_context/invalidate with the GIL released across the network round-trip, and fires the Python subscription callback on each watch change with a {data,status,error} dict — observable: py.allow_threads wraps the blocking call; a subscription callback fires with status: "success" and the decoded data; cancelling ends the watcher.

Unit: mizan-base and framework adapters (frontends/mizan-base, frontends/mizan-react, frontends/mizan-vue, frontends/mizan-svelte)

Charter. The TypeScript client kernel (@mizan/base) and the per-framework idiomatic adapters (React hooks, Vue composables, Svelte stores) that subscribe to it. @mizan/base is the authoritative kernel the frontends/mizan-rust unit ports. The mizan-ts cross-language HMAC pin (deriveCacheKey) also lives on the TS side.

Claims substantiated here.

  • Every frontend adapter is a thin idiomatic wrapper over one shared kernel; the kernel owns ContextState<T> = {data,status,error}, registerContext, mizanCall/mizanFetch, server-driven merge/invalidate, initSession, and a pluggable MizanTransport (HTTP default, Tauri/webview swap via configure) (INVARIANTS.md § Client Kernel; docs/AFI_ARCHITECTURE.md § Kernel model).
  • Shared parameters elevate to required provider props; non-shared params elevate to optional props with per-function override (INVARIANTS.md § Named Contexts; MIZAN.md §2 param elevation / specify resolution order).
  • Codegen targets the adapter surface, never the raw kernel; React devs get hooks, Vue composables, Svelte stores, same kernel underneath (docs/AFI_ARCHITECTURE.md § Kernel model).
  • Vue and Svelte are co-equal codegen targets over the same kernel, not React-derived (docs/AFI_ARCHITECTURE.md § Authoring surface).
  • Cross-language HMAC pin: deriveCacheKey in mizan-ts matches the Python key byte-for-byte (docs/CACHE_KEYING.md; README.md § Adapters — TypeScript is the protocol-reference adapter).

Owed behavioral mechanisms.

  • @mizan/base owns the single reconciled view: ContextState, the context registry, mizanCall/mizanFetch, server-driven merge/invalidate, initSession, over a MizanTransport interface — observable: the same behaviors the mizan-rust port pins (stable-key cache identity, merge-splice, scoped-vs-broad refetch, retry, dual-envelope error parse) hold in TS; the Rust port mirrors this file behavior-for-behavior.
  • the generated provider elevates a context's shared params to required props and its non-shared params to optional props, and resolves each member's effective params by overlaying per-function overrides onto the provider props at fetch time (INVARIANTS.md § Named Contexts; MIZAN.md §2 resolution order) — observable: a two-function user context where both take user_id and only one takes page generates a provider with required user_id and optional page; a per-function override supplies a different page for that one member while the shared user_id still covers both, and a member still missing a required param at fetch time is a runtime error, not a silent undefined.
  • deriveCacheKey (mizan-ts) reproduces the Python HMAC key byte-for-byte — observable: the pinned vectors in cores/mizan-python/tests/test_keys.py::test_cross_language_pin (ctx:user:605a1ca5…, ctx:user:30fc08eb…) are asserted against the TS output; any normalization drift (bool/None stringification, key ordering) breaks the pin, which the doc marks a security vulnerability.
  • adapters subscribe to the kernel and render in their own idiom without keeping a parallel copy of the truth — observable: a React hook and a Vue composable over the same context read the same kernel entry; mutating in one path updates both because the truth lives once in the kernel.
  • the channel subscription surface (useChannel and the ChannelSubscription<Params, Server, Client> type) orders its type parameters by direction, so a hook's inbound and outbound message types cannot be transposed — observable: a push-only channel's generated hook types its outbound message as never, making a client-side send on that channel a compile error rather than a runtime drop.
  • frontends/mizan-vue and frontends/mizan-svelte are runtime kernel-adapter packages, not codegen output alone — observable: a Vue composable and a Svelte store each subscribe to @mizan/base and refresh on invalidation against a live backend, so co-equality is checkable at the runtime layer and not only at the byte-parity layer.
  • the Svelte codegen target emits Svelte 5 $state/$derived runes (INVARIANTS.md § Client Kernel — "Svelte runes") — observable: the emitted Svelte client reads context state through runes rather than through readable stores.
  • every React provider that ships subscribes to the kernel, so no adapter module holds context state of its own (INVARIANTS.md § Client Kernel) — observable: the desktop example imports the codegen-emitted kernel-subscribing MizanContext, and mizan-react exposes no second provider keeping a parallel copy of the truth.
  • the generated channel client in the example harness names the channel slots from the client's side — observable: examples/django-react-site/harness/src/api/channels.ts declares <Pascal>ClientMessage/<Pascal>ServerMessage types with hasClientMessage/hasServerMessage flags and clientMessageType/serverMessageType keys, matching what protocol/mizan-codegen's channels target emits from the IR, so the harness is regenerable rather than divergent.

Unit: mizan-codegen (protocol/mizan-codegen)

Charter. The single Rust codegen binary that reads KDL IR and emits typed clients for every target (stage1, react, vue, svelte, channels, python, rust), plus the source-fetching that spawns each backend's IR-export command and the Pydantic-pre-step, plus the SSR bundling step that compiles each SSR entry component into a self-contained render bundle. It owns the IR→client transform and the SSR bundle production; it does not emit IR (backends do) or run the render engine (that is cores/mizan-rust-ssr).

Claims substantiated here.

  • Codegen reads KDL directly — no OpenAPI envelope, no openapi-typescript, no per-backend converter (docs/AFI_ARCHITECTURE.md § KDL is the IR).
  • Every frontend client is generated from the IR; each target is byte-parity-tested (INVARIANTS.md § Canonical IR & Codegen).
  • The codegen drives the backend's IR-export command as a subprocess and parses the KDL it writes (docs/AFI_ARCHITECTURE.md; backends/*/README.md § Generate the frontend).
  • Stage 1 (typed callXxx/fetchXxx) + Stage 2 (<MizanContext> provider, per-context providers, use{Hook}()) emission (backends/*/README.md § Generate the frontend).
  • Pydantic + Rust DX: a decoru pre-step authors Rust types from Pydantic before the cargo IR bin runs; a generic [source.script] source spawns any command emitting KDL (backends/mizan-tauri/README.md § Generate the frontend; config.rs).
  • mizan-generate's SSR bundling step compiles each SSR entry component together with react-dom/server.browser into a self-contained bundle assigning globalThis.renderApp, written to the bundles directory — the only place node/bun run in the SSR path (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary).

Owed behavioral mechanisms.

  • fetch.rs spawns the configured source's export command (FastAPI -m mizan_fastapi.ir, Django manage.py export_mizan_ir, Rust cargo run --bin, or a generic script) and parses stdout as KDL — no OpenAPI/converter anywhere in the path — observable: a codegen run against a live FastAPI backend consumes only the KDL the CLI writes; the Rust source runs the cargo bin and the optional decoru pre-step first.
  • the KDL parser reconstructs the full typed IR (types with struct/list/enum/alias shapes, functions with input/output/nullable/context/affects/merge/form, contexts with param elevation, channels) — observable: ir_deserialization.rs reads the AFI fixture back into typed structs and asserts the function set, per-function fields, param elevation, and named-type presence.
  • the channel half of the parser reads the params / client-message / server-message slots in that order and the emitter renders them as paramsType / clientMessageType / serverMessageType — observable: a KDL channel node carrying only server-message yields a channel view whose client slot is absent, and the emitted use{Pascal}Channel hook types its outbound message as never.
  • each target emits deterministically and is byte-parity-tested against a committed baseline — observable: stage1_parity.rs, react_parity.rs, rust_parity.rs, python_parity.rs, vue_svelte_parity.rs, and channels_smoke.rs diff emitter output against baselines and fail on any byte drift; two different runs produce identical output.
  • the emitters produce genuinely different, correct artifacts per target — not one shape behind distinct labels — observable: the react target emits <MizanContext> + per-context providers + use{Hook}() reading React context (Stage 2), Stage 1 emits the framework-agnostic typed callXxx/fetchXxx; vue emits composables; svelte emits stores; python emits a Pydantic-typed facade over the PyO3 kernel; rust emits a full crate depending on mizan-rust — each byte-checked against its own baseline, and stage1 is auto-included whenever a framework target is requested.
  • the codegen tree-shakes and canonicalizes types to match the backend emitters, and hoists inline enums into named Rust/TS types — observable: an unreferenced type is not emitted; an inline field { enum … } becomes a top-level Rust enum the struct field references; the channels target emits zero files when the IR carries no channels.
  • an SSR bundling step compiles each SSR entry component together with react-dom/server.browser into a self-contained bundle that assigns globalThis.renderApp, emitted into the configured bundles directory — the only place node/bun run in the SSR path — observable: a mizan-generate run produces one render bundle per SSR entry component, each evaluable standalone by the embedded-V8 engine (assigning renderApp at eval time and rendering from a JSON-parsed props argument).

Unit: AFI conformance (tests/afi)

Charter. The cross-adapter conformance gate: one fixture registered identically in Django, FastAPI, and a Rust app, asserting all three emit byte-identical KDL. It is the executable form of "the IR is the only contract"; it authors no production mechanism.

Claims substantiated here.

  • Adapter parity is gated by the AFI conformance suite asserting IR-shape parity — the same fixture through Django, FastAPI, and Rust emits byte-identical KDL (README.md § Conformance; docs/AFI_ARCHITECTURE.md § KDL is the IR — "divergence between adapters is what the IR exists to prevent").

Owed behavioral mechanisms.

  • one shared fixture (fixture.py and its Rust twin rust_app) registers the same 7 functions / 5 types / context+affects+merge graph across all three backends, and the parity test diffs the three KDL emissions requiring exact three-way equality — observable: test_codegen_parity.py fails (naming the divergent pair) the instant any adapter's type introspection, ordering, or param elevation drifts; the fixture spans the AFI axes (plain fn, no-input fn, shared-param context, affects mutation, optional return, merge mutation) so the gate is not a degenerate single-shape check.
  • the fixture spans the channel axis as well as the function axes, so the client-named slots are gated by the same three-way equality — observable: a channel declared once per backend emits an identical channel node with identical <Pascal>Params / <Pascal>ClientMessage / <Pascal>ServerMessage refs, so a backend-shaped slot name cannot re-enter the IR through one adapter without turning this gate red.

Unit: wire-parity drivers (tests/rust, tests/rust/fixture_client)

Charter. The runtime wire-contract gate: Rust drivers (drive_kernel, drive_emitted) that hit a live FastAPI fixture and a live Rust/Axum fixture and assert the same JSON shapes and invalidate/merge semantics, plus the codegen-emitted fixture_client crate they exercise. It proves the runtime wire equivalence the static IR parity does not, and authors no production mechanism.

Claims substantiated here.

  • The Rust adapters honor the same wire contract as FastAPI beyond static IR equivalence — same JSON shapes, same invalidate/merge semantics (README.md § Adapters; the "IR prevents divergence" claim taken to the runtime).
  • The codegen-emitted typed client round-trips cleanly through the kernel (protocol/mizan-codegen rust target).

Owed behavioral mechanisms.

  • run_wire_parity.py boots each backend, probes the readiness surface /api/mizan/session/ (Mizan-protocol-shaped, so the harness reads the same surface across backends), then runs both the raw-kernel and emitted-typed drivers against each, propagating any non-zero exit — observable: the drivers hit every fixture endpoint (plain functions, the two-function context, the optional-return path, the merge mutation) against both FastAPI and Rust/Axum and require the same responses; a wire drift on either backend turns the harness red.
  • drive_emitted exercises the codegen-emitted fixture_client typed functions (call_echo, fetch_user_context, call_update_profile, the optional call_find_user, the merge call_rename_user) so the generated crate is proven to round-trip, not merely to compile — observable: call_find_user(99999) returns None, fetch_user_context(5) returns the bundled user_profile+user_orders, and any deserialization mismatch fails the driver.