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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 19:43:16 -04:00

73 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 whether or not the current source realizes it; an unbuilt claim is surfaced, not dropped.
  • 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; ROADMAP.md § HMAC cache keying).
  • MWT identity layer (docs/MWT_SPEC.md).
  • Free origin-side cache implementing the full protocol locally (docs/PRODUCT_ARCHITECTURE.md § Free framework).
  • 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.
  • OWED (unbuilt): mixing socket and non-socket transport within one context is a registration-time error (INVARIANTS.md § WebSocket Support) — observable when built: registering a websocket=True fn and a plain fn under the same context= raises at registration; no such check exists in the registry today, so this obligation is currently unsubstantiated.
  • OWED (unbuilt): receive defined without send, and affects referencing a non-existent context/function, are registration-time errors (MIZAN.md §6) — validate_registry() warns on unresolved affects targets but does not hard-error, and there is no send/receive class to validate.

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.

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 — OWED (unbuilt):

  • 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 when built: a function declaring an Upload parameter emits a distinguished IR shape and binds a real file object at dispatch; no Upload type exists anywhere in the source today, so this claim is entirely unsubstantiated.

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.

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; ROADMAP.md § Core Consolidation — SSR in the binary).

Owed behavioral mechanisms.

  • the engine composes a real deno_web web-platform layer (TextEncoder/Decoder, MessagePort, timers) rather than a partial shim, evals the trusted bundle once, and renders per request — observable: the fixture bundle renders Hello, World!; a missing global would fail loudly at render, not silently pass (the doc's "partial polyfill is silent-failure-shaped" concern is discharged by using deno_web's real impls).
  • 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.
  • OWED (unbuilt): a PyO3 binding exposes SsrEngine to the Python side as an in-process extension — construct-from-bundle and render(props_json) -> HTML cross the FFI boundary with no process spawn, honoring V8's one-isolate-per-engine / non-Send constraint so the caller holds one engine per (worker thread, bundle) — observable when built: the Django backend imports the engine and calls render(props) in-process, and a prop still crosses as a parsed value (the injection guarantee survives the FFI hop). Today cores/mizan-rust-ssr/src/lib.rs exposes SsrEngine only as a Rust API with Rust-native #[tokio::test] coverage and carries no #[pyclass]/#[pymodule], so the PyO3 surface the AFI-boundary table names is unsubstantiated.

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).
  • Auth enforced at dispatch, rejecting before the body runs, identically across transports (INVARIANTS.md § Auth; MWT_SPEC.md § Usage rule).
  • Both invalidation transports: JSON body and X-Mizan-Invalidate header (ROADMAP.md § Done).
  • Return-type branching: data → RPC path, HttpResponse → view path (ROADMAP.md § Done).
  • Origin-side HMAC cache read/write on context fetch; cache=False/rev policy (docs/CACHE_KEYING.md; docs/PRODUCT_ARCHITECTURE.md § Spec additions).
  • 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 legacy 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 § Free framework; 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; backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md).

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.
  • OWED (open, per KNOWN_ISSUES.md): purge atomicity (index read/delete race), cross-language stringification for all value types (not just bool/None), per-param sub-index cleanup on broad purge, thundering-herd/single-flight protection, cache_get/cache_put argument-shape consistency, and RedisCache test coverage — each is a named correctness/operability obligation the "same protocol as Edge, security-critical" claim rests on and that the source has flagged as not-yet-satisfied.

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

Charter. The WebSocket transport: the ReactChannel 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).
  • WebSocket channels — typed bidirectional communication, real-time (ROADMAP.md § Done).
  • 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 (push/ReactChannel.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.
  • channel schema is exported into the registry's channels extension carrying params/react/django message shapes and a bidirectional flag — observable: a channel with a ReactMessage reports bidirectional: true; a push-only channel reports false and omits react_message, and the codegen channels target emits the matching typed envelopes and useXChannel hook.
  • 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).
  • Forms (schema/validate/submit) and formsets as Django stack extensions (ROADMAP.md § Done).
  • 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.
  • OWED (open, ISSUES.md § Open / ROADMAP.md § Next): a forms codegen target wired to mizanCall from the kernel, retiring the hand-written mizan-react/src/forms.ts — observable when built: the codegen emits form clients against the kernel; today no form codegen target exists, so the frontend form surface still depends on the pre-kernel provider.

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).
  • Shapes — Pydantic + django-readers for typed query projections (ROADMAP.md § Done).
  • 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.
  • OWED (unbuilt): the ReactContext('name') class form with 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 when built: a class defining send/receive generates a read hook and a commit function; committing runs receive and either refetches or uses a returned Shape. Today ReactContext is only a context-name marker with no metaclass, send/receive, or commit endpoint — the class form is unsubstantiated.

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 — Backend adapter row).
  • The component path resolves against DIRS to its built bundle in OPTIONS['bundles'] (produced by mizan-generate's SSR bundling step); the backend gathers props and wraps output for hydration (docs/SSR_ARCHITECTURE.md § AFI boundary — Backend adapter row; the TEMPLATES config block).
  • One engine per (worker thread, bundle) — V8's Locker constraint makes an engine non-Send, so the Django side never shares one across threads (docs/SSR_ARCHITECTURE.md § The engine).
  • SSR is orthogonal to RPC and composable; first paint carries data (INVARIANTS.md § SSR).

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.
  • OWED (unbuilt — the PyO3 cutover): the Django backend binds the engine through PyO3 and renders in-process — no bun run subprocess, no newline-delimited JSON-RPC, no SSRBridge, no ready-signal / auto-restart machinery, and OPTIONS carries bundles (a directory of built bundles) rather than worker (a JS entry file) — observable when built: a render spawns no external JS runtime process and calls the PyO3-bound SsrEngine.render(props) directly; ssr/bridge.py and the workers/mizan-ssr worker are retired. Today this is entirely unsubstantiated: ssr/bridge.py spawns bun run <worker> and correlates requests over JSON-RPC, ssr/backend.py wires OPTIONS['worker'] to that bridge and renders via SSRBridge.render, and the docstrings still describe a "persistent Bun subprocess" — the subprocess architecture the docs (§ The engine, § AFI boundary) no longer describe.
  • OWED (partial, docs/PSR_VS_EDGE.md § Current state): the render-on-mutation orchestration (mutation → trigger local render → store HTML), driven by the manifest's render_strategy, wiring the engine to the PSR path — observable when built: a public-context mutation triggers a local re-render and stores HTML; today the engine renders on request and the manifest records the strategy, but the mutation→render→store wiring is absent, so PSR-on-mutation is unsubstantiated.

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 (access/refresh, session validation) auto-detected, CSRF handled (ROADMAP.md § Done).
  • MWT is issued from an authenticated identity; create_mwt(user, secret, ttl, audience, kid); a separate JWT module still exists for 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; docs/AFI_ARCHITECTURE.md § Forward-direction primitives).
  • Edge manifest export, deterministic (sorted) output; both RPC and view-path functions; records each context's render_strategy (ROADMAP.md § Done; docs/PSR_VS_EDGE.md; ISSUES.md § Resolved — edge manifest non-determinism fixed).
  • Function discovery / registration via the clients.py convention (backends/mizan-django/README.md § Setup; MIZAN.md §6).
  • PSR (render_strategy in manifest) (docs/PSR_VS_EDGE.md).
  • 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; the "passes its own test suites" status rests on this harness.
  • Edge caching is provable before Edge exists — deterministic JSON, correct Cache-Control, header round-trip, auth-differentiated responses (test_core.py § EdgeCompatibilityTests, an explicit 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 Bun ping / crash-recovery / auto-restart assertions are retired with the subprocess bridge they exercised; today test_ssr.py still drives SSRBridge (Bun subprocess, JSON-RPC, killed-worker restart), so this engine-path verification is owed alongside the mizan-django SSR PyO3 cutover.
  • the benchmark suite measures HTTP-vs-executor overhead and throughput with correctness assertions on each path — observable: every benchmark also asserts the function's numeric output, so a green benchmark run is also a correctness run.

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.
  • 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, and the KDL IR CLI. It owns the FastAPI transport surface over mizan_core; Forms/Channels/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).
  • 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).
  • 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.
  • OWED (unbuilt caveats, README.md § Caveat + notes 2,3,5): Axum declares Transport::Websocket in the IR/macro but routes no WebSocket handler; carries is_form/form_role trait stubs but no validate/submit endpoint; and accepts auth= on a function but the dispatch path does not enforce it — observable: a websocket=True function is reachable only over HTTP; an auth=True function is NOT rejected for anonymous callers on this adapter. These are documented gaps the "auth enforced on every adapter" invariant (INVARIANTS.md § Auth) owes and Rust/Axum does not yet meet.

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.
  • OWED (unbuilt caveat, README.md § Caveat + note 5): Tauri's FunctionSpec carries auth/private fields but the dispatch path does not enforce them — observable: an auth=-declared function is not rejected for an unauthorized caller on this adapter; the "auth enforced on every adapter" invariant is not yet met here.

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 TS source is referenced by the docs and adapters but is not inlined in this repo snapshot. Listed so the kernel claims and the adapter-parity claims are surfaced against their real roots. 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 ship as v1 alongside React (docs/AFI_ARCHITECTURE.md § Launch surface).
  • Cross-language HMAC pin: deriveCacheKey in mizan-ts matches the Python key byte-for-byte (docs/CACHE_KEYING.md; README.md § Adapters — TypeScript is the protocol-reference adapter).

Owed behavioral mechanisms.

  • @mizan/base owns the single reconciled view: ContextState, the context registry, mizanCall/mizanFetch, server-driven merge/invalidate, initSession, over a MizanTransport interface — observable: the same behaviors the mizan-rust port pins (stable-key cache identity, merge-splice, scoped-vs-broad refetch, retry, dual-envelope error parse) hold in TS; the Rust port exists precisely to mirror this file.
  • 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.
  • OWED (unbuilt, ISSUES.md § Open / ROADMAP.md § Next): frontends/mizan-vue and frontends/mizan-svelte are runtime kernel-adapter packages — the codegen emits their clients (byte-parity-tested) but no runtime package or live-backend example exists — observable when built: a Vue composable / Svelte store subscribes to @mizan/base and refreshes on invalidation against a live backend; today only React has full integration verification, so the "Vue and Svelte ship as v1" claim is unsubstantiated at the runtime layer.
  • OWED (drift, ISSUES.md § Open): the Svelte codegen target emits Svelte 4 readable stores; Svelte 5 $state/$derived runes are owed — observable when built: the emitted Svelte client uses runes.
  • OWED (migration, ISSUES.md § Open): mizan-react/src/context.tsx is the pre-kernel provider still shipped and imported by the desktop example, coexisting with the codegen-emitted kernel-subscribing MizanContext; retiring it (migrating the example onto the generated provider) is owed so the "every adapter wraps the kernel, none keeps its own truth" invariant holds without exception.

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

Owed behavioral mechanisms.

  • 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.
  • 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.
  • OWED (unbuilt): an SSR bundling step compiles each SSR entry component together with react-dom/server.browser into a self-contained bundle that assigns globalThis.renderApp, emitted into the configured bundles directory — this is the only place node/bun run in the SSR path — observable when built: a mizan-generate run produces one render bundle per SSR entry component, each evaluable standalone by the embedded-V8 engine (assigning renderApp at eval time and rendering from a JSON-parsed props argument). Today the codegen binary emits typed clients (stage1/react/vue/svelte/channels/python/rust) but carries no SSR bundling target in src/emit/ and no bundling path in fetch.rs/config.rs, so the bundling step the docs place with mizan-generate is unsubstantiated.

Unit: AFI conformance (tests/afi)

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.

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.