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>
73 KiB
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.packtargets 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 §1–2).
- 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 —
Uploadfirst-class end to end through IR (INVARIANTS.md § File Uploads).
Owed behavioral mechanisms.
Client Function RPC / decorator:
@clientaccepts the full declared set (context,affects,merge,private,route,methods,websocket,auth,rev,cache) and synthesizes a PydanticInputmodel from the function signature (skipping the request param) — observable: a decorated fn with(request, a: int, b: int)yields anInputwith two typed fields; input validation rejectsa="x"before the body runs.- the return annotation decides wire shape: a primitive/dict return is wrapped as
{result: …}, whileBaseModel/list[BaseModel]/Optional[BaseModel]pass through bare — observable:-> list[Item]reaches the wire as a bare JSON array,-> intas{"result": n}; a missing return annotation raisesTypeErrorat decoration (not a silentAny). context=andaffects=(andmerge=) are enforced mutually exclusive at decoration — observable:@client(context=X, affects=Y)raisesValueError, 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")raisesValueErrornaming 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 produceget_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=Truefn and a plain fn under the samecontext=raises at registration; no such check exists in the registry today, so this obligation is currently unsubstantiated. - OWED (unbuilt):
receivedefined withoutsend, andaffectsreferencing a non-existent context/function, are registration-time errors (MIZAN.md §6) —validate_registry()warns on unresolvedaffectstargets but does not hard-error, and there is nosend/receiveclass 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-bysorted) — 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), producingstruct/alias{list}/enum/optional/unionshapes under canonical<camelName>Input/<camelName>Outputnames, withVec-element sub-types surfaced — observable: a-> list[OrderOutput]fn emitstype "userOrdersOutput" { alias { list { ref "OrderOutput" } } }AND atype "OrderOutput" { struct … };-> Model | Nonesetsoutput-nullable #true. - context param elevation is computed in the IR: a param is
required #trueiff every member of the context declares it, withshared-bynaming the declarers — observable: a two-functionusercontext where both takeuser_idemitsparam "user_id" { type "integer"; required #true; shared-by … }; if only one declarespage,pageisrequired #false. privateand view-path functions are omitted from the emittedfunctionset, and channels are emitted from thechannelsregistry extension — observable:@client(private=True)never appears in the KDL (so it can carry invalidation without being client-callable); a registered channel emits achannelnode with its pascal-name and message-type refs.
HMAC cache keying (protocol-critical cross-language identity):
derive_cache_keyproducesctx:{context}:{hmac_hex}over a JSON-canonical sorted form with param values normalized to JSON-native strings (True→"true",None→"null") anduser_idomitted 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; thectx:prefix supports broad SCAN.- key derivation resists delimiter collision and versions on
rev— observable:context="user", user_id="12"andcontext="user1", user_id="2"produce different keys; bumpingrevproduces a new key, so old entries become unreachable orphans without a purge.
MWT identity layer:
create_mwtplaceskidin the JOSE header per RFC 7515 (not the payload) and computespkeyassha256oversorted(get_all_permissions())plus staff/super flags, withaudandnbfclaims — observable:decode_mwtreadskidfrom the header; a token minted for one audience decodes toNoneunder another;pkeyis deterministic for identical permission state and changes the instant a permission is added.MWTUseris built entirely from claims with no DB query — observable: constructingMWTUser(payload)setspk/is_staff/is_superuser/pkeyfrom the token alone; an expired token decodes toNone.
Cache backends:
MemoryCacheandRedisCacheboth implement get/set/delete plus prefix-scoped purge; the Redis broad purge SCANsctx:{context}:*and UNLINKs, never a full flush — observable:delete_by_prefix("ctx:user:")removes onlyuserentries and leavesctx:products:*and foreign-prefixed keys intact;RedisCacheapplies a TTL safety-net on everyset.
Type-introspection helpers (shared so backend parity cannot drift):
is_structured_outputrecognizesBaseModel/Optional[BaseModel]/ container-of-BaseModelas no-wrap, andtypes_match_for_mergeaccepts direct / list-upsert / list-replace shape matches — observable: a slot typedlist[T]matches a value typedT(upsert-by-id), and a multi-armA | B | Noneunion is returned as-is byextract_optional, not silently narrowed to one arm.
File Uploads — OWED (unbuilt):
- an
Uploadtype 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 anUploadparameter emits a distinguished IR shape and binds a real file object at dispatch; noUploadtype 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.rsand the three-waytests/afi/test_codegen_parity.pydiff Rust output against the canonical Python-emittedafi_ir.kdland 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; anAlias(Primitive)orEnumnamed type inlines at its reference site instead of emitting a standalonetypenode, matching the Python output.
Compile-time registry:
TYPES/CONTEXTS/FUNCTIONSare linkme distributed slices populated at the consumer crate's expansion sites, andlookup_function/context_membersresolve 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_invalidationauto-scopes by matching mutation arg names against the affected context's declared Input params — observable: a mutation carryinguser_idagainst ausercontext whose members declareuser_idemits{context:"user", params:{user_id:…}}, while a non-matching arg emits the bare context string.compute_mergesresolves the slot by structural return-type match against context members (viatypes_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_invariantspanics with a structured message when anaffects/mergetarget names an unregistered context, when amergetarget has no unique matching member, or when a shared context param's type diverges across members — observable: anaffects = "ghost"fails codegen with a named error; amergewhose 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@clientparameter set (backends/mizan-tauri/README.md § Define server functions; backends/mizan-rust-axum README).
Owed behavioral mechanisms.
#[derive(Mizan)]emits aMizanType::shape()matching the Python type introspection, honoring serderename_all/renameso wire names match serialization, and registers aTypeEntry— observable: an enum with#[serde(rename_all="snake_case")]emits IR enum variants in snake form; a struct fieldr#typeemits IR field nametype.#[mizan::client]synthesizes a<camelName>Inputstruct +MizanTypeimpl, registers the canonical<camelName>Input/<camelName>Outputtype entries (and theVecelement type for list outputs), and implementsFunctionSpec::dispatchthat 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>registersuserOrdersOutputas a list alias plusOrderOutput, and dispatch round-trips typed args; aResult<T, MizanError>return?-unwraps so user errors surface as the standard envelope, while the IR still sees only theTshape.#[mizan::client]enforces the same mutual-exclusion as Python (contextvsaffects/merge) and requires anasync fnwith 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 aContextMarkerwith a snake_case (or explicit) name and registers aContextEntry— observable:#[mizan::context("user")]and#[mizan::context] struct UserCtxboth yieldNAME == "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: i64emits IR param nameuser_idand 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_webweb-platform layer (TextEncoder/Decoder, MessagePort, timers) rather than a partial shim, evals the trusted bundle once, and renders per request — observable: the fixture bundle rendersHello, 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 turnsno_rsc.rsred; absence alone is not the guarantee — re-entry is loud. - OWED (unbuilt): a PyO3 binding exposes
SsrEngineto the Python side as an in-process extension — construct-from-bundle andrender(props_json) -> HTMLcross the FFI boundary with no process spawn, honoring V8's one-isolate-per-engine / non-Sendconstraint so the caller holds one engine per (worker thread, bundle) — observable when built: the Django backend imports the engine and callsrender(props)in-process, and a prop still crosses as a parsed value (the injection guarantee survives the FFI hop). Todaycores/mizan-rust-ssr/src/lib.rsexposesSsrEngineonly 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}andmerge(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-Invalidateheader (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/revpolicy (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_functionvalidates input against the function's PydanticInputbefore invoking the body, and rejects private functions from RPC — observable: a missing required field returnsVALIDATION_ERRORwith per-field detail and the body never runs; aprivate=Truefunction returnsFORBIDDENwhen called over/call/.- output serialization walks
BaseModel/list/dictrecursively viato_jsonable_pythonsolist[BaseModel]reaches the wire as a bare array — observable: a-> list[Item]function returns[{…},{…}], not{"result":[…]}; anOptional[Model]returningNoneserializes tonullnot{"result":null}.
Named-context bundle fetch (single request, param-filtered):
execute_contextruns 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=3returns{user_profile:…, user_orders:…}whereuser_profilenever seespage; 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_invalidationauto-scopes by matching mutation args against context param names (Tier 1), falling back to the bare context (Tier 3), and resolves function-levelaffectsto the function name — observable:update_profile(user_id=5,…)against ausercontext 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) andX-Mizan-Invalidate: user;user_id=5(header, URL-encoded soq=hello world→q=hello%20worldand semicolons survive a parse round-trip); a mutation that raises emits neither. _resolve_mergesresolves 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: withmorph_groups: list[MorphGroupMeta]andmorph_layers: list[MorphLayer]in one context, a mutation returningMorphLayermerges intomorph_layersonly; the kernel does no shape inference.
Auth enforced before the body:
_check_auth_requirementruns beforeview.call, handlingrequired/staff/superuser/callable and mapping toUNAUTHORIZED/FORBIDDEN— observable: an anonymous call to@client(auth=True)returnsUNAUTHORIZEDand the function body never executes; a callable raisingPermissionErrorsurfaces its message asFORBIDDEN.- 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 invalidX-Mizan-Tokenreturns 401 without trying session auth; a valid MWT setsrequest.user = MWTUserwith no DB query; CSRF is enforced only on the session path.
Return-type branching + origin cache:
- a function returning an
HttpResponsetakes the view path (invalidation rides the header,Cache-Control: no-store), while a data return takes the RPC path — observable: a-> HttpResponseRedirectmutation returns the 302 withX-Mizan-Invalidateset; the same-decorated-> Shapemutation returns JSON withinvalidatein the body. - context fetch consults the origin cache keyed by the effective
rev(max across members) and effective cache policy (Falseshort-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 carriesX-Mizan-Cache: HIT; a scoped mutation foruser_id=5purges only that entry and leavesuser_id=6a HIT; a context with anycache=Falsemember emitsno-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_purgerecomputes 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_secretandcache_redis_urlpresent, thread-safe and lazily initialized — observable: with only one configured, caching is disabled and logged; concurrentget_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_putargument-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 beforeauthorize/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 sameexecute_function(so validation/auth are identical to HTTP) — observable: an RPC call to an HTTP-only function returnsFORBIDDEN("use POST /call/"); a WS call to awebsocket=Truefn returns the same envelope shape as HTTP; a missingid/fnreturns a structured error. authorize()gates every subscription and exceptions in it are contained — observable:authorizereturningFalseblocks the subscribe with "Not authorized"; anauthorizethat 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 tochat_generalwith the message body; push with no channel layer configured warns rather than raising. - channel schema is exported into the registry's
channelsextension carrying params/react/django message shapes and abidirectionalflag — observable: a channel with aReactMessagereportsbidirectional: true; a push-only channel reportsfalseand omitsreact_message, and the codegen channels target emits the matching typed envelopes anduseXChannelhook. - 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 setsscope["user"]to aJWTUserfrom 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 getsuseXForm()(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), carryingform/form_name/form_rolemeta — observable: defining aContactFormwith amizanFormMeta(name="contact")registerscontact.schema,contact.validate,contact.submit; a form without amizanattribute registers nothing; enablingenable_formsetaddscontact.formset.{schema,validate,submit}.- the schema function projects each Django field into a typed
FieldSchema(mapping field classes to Python types, extracting choices fromModelChoiceFieldsafely, serializing initial values) and carries themizanFormMetadisplay/behavior settings — observable: aCharField/EmailField/Textareaform yields three typed fields with correcttype/widget; aModelChoiceFieldyields JSON-serializable{value,label}choices (noModelChoiceIteratorValueleak). - 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 andsuccess: false; a valid submit runson_submit_successand returns its data; a multipart submit binds files. create_form_instancethreadsrequest/user/instanceinit 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 externalmizan-allauthrepo builds on) — observable: a form declaring arequestkwarg receives it; a form that doesn't acceptrequeststill instantiates rather than raisingTypeError.- OWED (open, ISSUES.md § Open / ROADMAP.md § Next): a forms codegen target wired to
mizanCallfrom the kernel, retiring the hand-writtenmizan-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.querycompiles 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 nestedAuthorCardShapewithbooksruns 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_manyof mixed new+existing items runs one query for the existing set; a nested diff reportscreated/updated/deletedby 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-PKSectionshape diffs correctly;is_published=Falseis not treated as missing; a nullable editor FK returnsNonerather than erroring. - OWED (unbuilt): the
ReactContext('name')class form withsend/receiveand aPOST /ctx/<name>/commit/endpoint that routes committed shape data toreceive, with auto-refetch-or-fresh-return after commit (INVARIANTS.md § Compositions; MIZAN.md §5) — observable when built: a class definingsend/receivegenerates a read hook and a commit function; committing runsreceiveand either refetches or uses a returned Shape. TodayReactContextis 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/.jsxfile 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
DIRSto its built bundle inOPTIONS['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; theTEMPLATESconfig 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.
MizanTemplatesimplements Django's template-backend interface:get_template(name)resolvesnameas a.tsx/.jsxfile path underDIRSand returns aMizanTemplatefor the resolved component;renderstripsrequest/csrf_tokenand passes the remaining context as props — observable:render(request, 'components/Hello.tsx', ctx)renders that component withctxas props;from_stringraises (it renders files, not strings); a missing file raisesTemplateDoesNotExist.- component-to-bundle resolution: the backend resolves each component file to its self-contained render bundle in
OPTIONS['bundles'](themizan-generate-produced bundle assigningglobalThis.renderApp) and hands the bundle to the engine, rather than reading component source directly — observable:renderofcomponents/Hello.tsxloads that component's built bundle from the configuredbundlesdirectory; 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
SsrEngineper (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 runsubprocess, no newline-delimited JSON-RPC, noSSRBridge, no ready-signal / auto-restart machinery, andOPTIONScarriesbundles(a directory of built bundles) rather thanworker(a JS entry file) — observable when built: a render spawns no external JS runtime process and calls the PyO3-boundSsrEngine.render(props)directly;ssr/bridge.pyand theworkers/mizan-ssrworker are retired. Today this is entirely unsubstantiated:ssr/bridge.pyspawnsbun run <worker>and correlates requests over JSON-RPC,ssr/backend.pywiresOPTIONS['worker']to that bridge and renders viaSSRBridge.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/expand are tied to a session key so logout revokes them — observable: a refresh whose underlying session was destroyed returnsNone(immediate revocation); an accessJWTUseris built from claims with no DB query;decode_tokenenforces 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_obtainmints an MWT from the authenticated session viacreate_mwt, requiringMIZAN_MWT_SECRET, andjwt_obtain/jwt_refreshissue/rotate the JWT pair carrying user claims — observable:mwt_obtainon an anonymous request raises; with no secret configured it raises a clear config error; the JWT pair includesis_staff/is_superuserso 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_strategyin manifest) (docs/PSR_VS_EDGE.md). - Session / CSRF init endpoint;
wrap_asgiWebSocket routing (backends/mizan-django/README.md § Setup).
Owed behavioral mechanisms.
export_mizan_irpopulates the registry via discovery, then writes canonical KDL frommizan_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_manifestemits a deterministic (sorted contexts and mutations) JSON mapping contexts to endpoints/params/functions, distinguishing rpc vs view path, markinguser_scopedandrender_strategy(dynamic_cachedfor user-scoped,psrfor public), and mutations with auto-scoped params + private/route — observable: two exports are byte-identical regardless of registration order; a context withuser_idisuser_scoped+dynamic_cached; a view-path function'sroutepopulatespage_routes; a mutation whose args match context params lists them underauto_scoped_params.mizan_clientsdiscoversServerFunctionsubclasses under each app'sclients.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_asgiroutes/ws/to the channels consumer — observable:GET /session/returns{csrfToken}and aSet-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:
HTTPIntegrationTestsandCacheIntegrationTestsassert the JSON body andX-Mizan-Invalidateheader 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
PermissionErrormessage 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-storeon 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
SsrEnginerenders 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 nobun/nodesubprocess. The Bun ping / crash-recovery / auto-restart assertions are retired with the subprocess bridge they exercised; todaytest_ssr.pystill drivesSSRBridge(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 andauthorize-raise both blocking cleanly, duplicate-subscription rejection, room-level per-param authorization, and WS-RPC gated towebsocket=Truefunctions — 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, batcheddiff_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_functionlooks up the registered function, enforcesauthbefore running (matching Django's semantics:True/required/staff/superuser/callable), validates input against the PydanticInput, awaitsview.acall(async handlers on the loop, sync in a threadpool), and serializes viajsonable_encoder— observable: an anonymous call to@client(auth=True)returns 401 before the body; anasync defhandler runs on the loop (a realawaitinside completes);list[BaseModel]/Optional[BaseModel]reach the wire bare.compute_invalidationauto-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_mergesresolves the slot by unique return-type match (types_match_for_merge) and emits{context, slot, value, params?}, dropping ambiguous — observable: themorph_groups/morph_layersfixture routes aMorphLayermutation tomorph_layersonly; a merge-only mutation emitsmergewith emptyinvalidate.- the router exposes
POST /call/,GET /ctx/{name}/,GET /session/and both exception handlers render every failure through{"error":{code,message,details?}}withCache-Control: no-store— observable: an unknown function returns 404 in the envelope; a malformed body returnsBAD_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_calldispatches throughlookup_function+FunctionSpec::dispatch, then attachescompute_invalidationandcompute_mergesoutput, 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_fetchbundles every registered member of the context and coerces string query params to typed JSON via each function'sinput_paramsprimitive table — observable:GET /ctx/user/?user_id=5returns the flat bundle withuser_idcoerced 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
RequestHandleto 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::Websocketin the IR/macro but routes no WebSocket handler; carriesis_form/form_roletrait stubs but no validate/submit endpoint; and acceptsauth=on a function but the dispatch path does not enforce it — observable: awebsocket=Truefunction is reachable only over HTTP; anauth=Truefunction 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). RequestHandlewrapsAppHandleso 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 dispatchescall/fetchthrough the sameFUNCTIONS/CONTEXTSslices 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: aMizanError::ValidationFailedreaches 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 totauri::AppHandlefor managed state / event emission — observable: a function callingreq.downcast::<tauri::AppHandle>()reaches Tauri state; stateless functions ignore the handle.- OWED (unbuilt caveat, README.md § Caveat + note 5): Tauri's
FunctionSpeccarriesauth/privatefields but the dispatch path does not enforce them — observable: anauth=-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 oneContextState {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 toJSON.stringifywith 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_callapplies the response'smergeentries first, then queuesinvalidateentries, then returnsresult— 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_slotupserts byidinto 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 correctcode; an unparseable body yieldsHTTP_500with the raw body. - the PyO3 bridge exposes
call/fetch_context/subscribe_context/invalidatewith 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_threadswraps the blocking call; a subscription callback fires withstatus: "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 pluggableMizanTransport(HTTP default, Tauri/webview swap viaconfigure) (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 /
specifyresolution 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:
deriveCacheKeyinmizan-tsmatches the Python key byte-for-byte (docs/CACHE_KEYING.md; README.md § Adapters — TypeScript is the protocol-reference adapter).
Owed behavioral mechanisms.
@mizan/baseowns the single reconciled view:ContextState, the context registry,mizanCall/mizanFetch, server-drivenmerge/invalidate,initSession, over aMizanTransportinterface — observable: the same behaviors themizan-rustport 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
usercontext where both takeuser_idand only one takespagegenerates a provider with requireduser_idand optionalpage; a per-function override supplies a differentpagefor that one member while the shareduser_idstill 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 incores/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-vueandfrontends/mizan-svelteare 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/baseand 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
readablestores; Svelte 5$state/$derivedrunes are owed — observable when built: the emitted Svelte client uses runes. - OWED (migration, ISSUES.md § Open):
mizan-react/src/context.tsxis the pre-kernel provider still shipped and imported by the desktop example, coexisting with the codegen-emitted kernel-subscribingMizanContext; 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.browserinto a self-contained bundle assigningglobalThis.renderApp, written to thebundlesdirectory — 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.rsspawns the configured source's export command (FastAPI-m mizan_fastapi.ir, Djangomanage.py export_mizan_ir, Rustcargo 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.rsreads 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, andchannels_smoke.rsdiff 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 typedcallXxx/fetchXxx; vue emits composables; svelte emits stores; python emits a Pydantic-typed facade over the PyO3 kernel; rust emits a full crate depending onmizan-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.browserinto a self-contained bundle that assignsglobalThis.renderApp, emitted into the configuredbundlesdirectory — this is the only place node/bun run in the SSR path — observable when built: amizan-generaterun produces one render bundle per SSR entry component, each evaluable standalone by the embedded-V8 engine (assigningrenderAppat 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 insrc/emit/and no bundling path infetch.rs/config.rs, so the bundling step the docs place withmizan-generateis 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.pyand its Rust twinrust_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.pyfails (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.pyboots 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_emittedexercises the codegen-emittedfixture_clienttyped functions (call_echo,fetch_user_context,call_update_profile, the optionalcall_find_user, the mergecall_rename_user) so the generated crate is proven to round-trip, not merely to compile — observable:call_find_user(99999)returnsNone,fetch_user_context(5)returns the bundleduser_profile+user_orders, and any deserialization mismatch fails the driver.