MIZAN.md's phase-numbered implementation order, PRODUCT_ARCHITECTURE's "Deferred until Render revenue funds it" and "Shipped in", PSR_VS_EDGE's current-state section, and the READMEs' passing-test counts were all reporting where the work stood rather than what the system is. OWED_SURFACE keeps its subject — surface that is specified but unbuilt — stated as the shape each unit owes. The channel sections follow the renamed slots: Params / ClientMessage / ServerMessage, and Channel as the base class on both backends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
483 lines
78 KiB
Markdown
483 lines
78 KiB
Markdown
# Owed Surface
|
||
|
||
## Contract
|
||
|
||
This document declares, per crate/package unit, the behavioral mechanisms the unit owes to substantiate the documentation's claims.
|
||
|
||
- The owed surface is derived from the documentation's CLAIMS. For each non-trivial claim, it enumerates the behaviors the code must exhibit to prove the claim — to a hostile auditor, an IP lawyer, and a paying customer — at maximal performance, efficiency, and hygiene, never a minimal technicality.
|
||
- A mechanism is stated as observable behavior with the criterion that distinguishes its maximal realization from a degenerate stub, observably enough that a skeptic can check it.
|
||
- A mechanism is owed by the unit's charter and claims. The enumeration is derived from the claims, never from an inventory of the source, so it does not move when the source does. Whether a given mechanism holds right now is answered by the test suite, CI, git history, and the issue tracker.
|
||
- A **unit** is one crate/package/build target, identified by its root path, sized to emit whole in one `.pack`. A `.pack` targets the units whose roots contain its files.
|
||
- This is a declared contract the authoring agent holds and honors when it emits a pack. The PreToolUse gate enforces only that a stance is declared (this document, or an exemption) before code is authored; it does not test the packs.
|
||
|
||
The AFI's single load-bearing thesis (README.md, docs/AFI_ARCHITECTURE.md § Why the AFI shape): the backends × frontends quadratic collapses to linear because **one KDL IR is the only contract that crosses the backend↔frontend boundary**. Every mechanism below is, in the end, in service of that: N backends emit byte-identical KDL for the same registered functions, and M frontends are generated from it, so a bug can only live in the KDL contract or its edges — nowhere in between.
|
||
|
||
`backends/mizan-django/src/mizan` exceeds the single-emit budget (137K est. tokens); it is decomposed below into sub-units along the documented feature seams (dispatch, cache, channels, forms, shapes, ssr, jwt, registration/export) plus its verification harness, which itself exceeds budget and is cut into two test sub-units at the protocol-vs-adversarial seam. Every other unit emits whole.
|
||
|
||
---
|
||
|
||
## Unit: mizan_core (`cores/mizan-python/src/mizan_core`)
|
||
|
||
**Charter.** The framework-agnostic Python substrate every Python backend adapter stands on: the `@client` decorator and the function-to-IR machinery, the registry, canonical KDL IR emission, HMAC cache-key derivation, cache backends, MWT identity, and the type-introspection helpers the adapters share. It owns *language-level* primitives; it does not own transport, dispatch, or any Django/FastAPI mechanics.
|
||
|
||
**Claims substantiated here.**
|
||
- Client Function RPC — decorated functions carrying the full variadic/kwarg set (INVARIANTS.md § Client Function RPC).
|
||
- Named Contexts — functions sharing a context name grouped at registration into one provider/one fetch (INVARIANTS.md § Named Contexts; MIZAN.md §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 § Invariant).
|
||
- MWT identity layer (docs/MWT_SPEC.md).
|
||
- Free origin-side cache implementing the full protocol locally (docs/PRODUCT_ARCHITECTURE.md § Origin-side cache).
|
||
- File Uploads — `Upload` first-class end to end through IR (INVARIANTS.md § File Uploads).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
|
||
Client Function RPC / decorator:
|
||
- `@client` accepts the full declared set (`context`, `affects`, `merge`, `private`, `route`, `methods`, `websocket`, `auth`, `rev`, `cache`) and synthesizes a Pydantic `Input` model from the function signature (skipping the request param) — observable: a decorated fn with `(request, a: int, b: int)` yields an `Input` with two typed fields; input validation rejects `a="x"` before the body runs.
|
||
- the return annotation decides wire shape: a primitive/dict return is wrapped as `{result: …}`, while `BaseModel` / `list[BaseModel]` / `Optional[BaseModel]` pass through bare — observable: `-> list[Item]` reaches the wire as a bare JSON array, `-> int` as `{"result": n}`; a missing return annotation raises `TypeError` at decoration (not a silent `Any`).
|
||
- `context=` and `affects=` (and `merge=`) are enforced mutually exclusive at decoration — observable: `@client(context=X, affects=Y)` raises `ValueError`, so a function cannot be simultaneously a reader and a mutation.
|
||
- `auth=` is normalized and validated at decoration (`True`→`"required"`, callables kept, `"staff"/"superuser"` allowed) — observable: `@client(auth="admin")` raises `ValueError` naming the valid set, not a runtime surprise at dispatch.
|
||
|
||
Named Contexts grouping (the "one provider, one fetch" invariant's registry half):
|
||
- the registry groups every function by its context string so a named context is a single fetch unit, never N callables — observable: two `@client(context="user")` functions produce `get_context_groups()["user"] == [both names]`; `"global"` is just a reserved name in the same map, not a separate mechanism.
|
||
- mixing socket and non-socket transport within one context is a registration-time error (INVARIANTS.md § WebSocket Support) — observable: registering a `websocket=True` fn and a plain fn under the same `context=` raises at registration rather than producing a context half of whose members are unreachable over the bundle fetch.
|
||
- `receive` defined without `send`, and `affects` referencing a non-existent context or function, are registration-time errors (MIZAN.md §6) — observable: `validate_registry()` raises on an `affects` target that resolves to neither a registered context nor a registered function, and on a `receive` with no paired `send`, so a typo cannot reach codegen as a silently-dead invalidation edge.
|
||
|
||
Canonical KDL IR (`build_ir`) — the contract every codegen target reads:
|
||
- IR is emitted in a canonical order independent of registration order (functions alphabetical by wire name, contexts alphabetical, params alphabetical, `shared-by` sorted) — observable: registering the same functions in two different orders yields byte-identical KDL; this is the property the three-way parity test rests on.
|
||
- types are introspected from the Pydantic models directly (never routed through JSON-Schema `$ref`), producing `struct` / `alias{list}` / `enum` / `optional` / `union` shapes under canonical `<camelName>Input` / `<camelName>Output` names, with `Vec`-element sub-types surfaced — observable: a `-> list[OrderOutput]` fn emits `type "userOrdersOutput" { alias { list { ref "OrderOutput" } } }` AND a `type "OrderOutput" { struct … }`; `-> Model | None` sets `output-nullable #true`.
|
||
- context param elevation is computed in the IR: a param is `required #true` iff every member of the context declares it, with `shared-by` naming the declarers — observable: a two-function `user` context where both take `user_id` emits `param "user_id" { type "integer"; required #true; shared-by … }`; if only one declares `page`, `page` is `required #false`.
|
||
- `private` and view-path functions are omitted from the emitted `function` set, and channels are emitted from the `channels` registry extension — observable: `@client(private=True)` never appears in the KDL (so it can carry invalidation without being client-callable); a registered channel emits a `channel` node with its pascal-name and message-type refs.
|
||
- channel slot names in the IR are backend-neutral and named from the client's side: `params`, `client-message`, `server-message`, in that order — observable: every emitter and parser iterates the three slots in that order, so a channel declared on Django and the same channel declared on FastAPI emit the identical `channel` node.
|
||
- `wire_to_pascal` is the single derivation of a channel's emitted type names — the wire name split on `[._-]`, each part title-cased and joined, yielding `<Pascal>Params` / `<Pascal>ClientMessage` / `<Pascal>ServerMessage` — observable: a backend that also publishes an OpenAPI slot table names each type through this same function, so the two documents cannot disagree about what one type is called.
|
||
|
||
HMAC cache keying (protocol-critical cross-language identity):
|
||
- `derive_cache_key` produces `ctx:{context}:{hmac_hex}` over a JSON-canonical sorted form with param values normalized to JSON-native strings (`True`→`"true"`, `None`→`"null"`) and `user_id` omitted for public content — observable: the pinned test vectors (`ctx:user:605a1ca5…` public, `ctx:user:30fc08eb…` user-scoped) match the TypeScript adapter byte-for-byte; param ordering does not change the key; the `ctx:` prefix supports broad SCAN.
|
||
- key derivation resists delimiter collision and versions on `rev` — observable: `context="user", user_id="12"` and `context="user1", user_id="2"` produce different keys; bumping `rev` produces a new key, so old entries become unreachable orphans without a purge.
|
||
|
||
MWT identity layer:
|
||
- `create_mwt` places `kid` in the JOSE header per RFC 7515 (not the payload) and computes `pkey` as `sha256` over `sorted(get_all_permissions())` plus staff/super flags, with `aud` and `nbf` claims — observable: `decode_mwt` reads `kid` from the header; a token minted for one audience decodes to `None` under another; `pkey` is deterministic for identical permission state and changes the instant a permission is added.
|
||
- `MWTUser` is built entirely from claims with no DB query — observable: constructing `MWTUser(payload)` sets `pk`/`is_staff`/`is_superuser`/`pkey` from the token alone; an expired token decodes to `None`.
|
||
|
||
Cache backends:
|
||
- `MemoryCache` and `RedisCache` both implement get/set/delete plus prefix-scoped purge; the Redis broad purge SCANs `ctx:{context}:*` and UNLINKs, never a full flush — observable: `delete_by_prefix("ctx:user:")` removes only `user` entries and leaves `ctx:products:*` and foreign-prefixed keys intact; `RedisCache` applies a TTL safety-net on every `set`.
|
||
|
||
Type-introspection helpers (shared so backend parity cannot drift):
|
||
- `is_structured_output` recognizes `BaseModel` / `Optional[BaseModel]` / container-of-`BaseModel` as no-wrap, and `types_match_for_merge` accepts direct / list-upsert / list-replace shape matches — observable: a slot typed `list[T]` matches a value typed `T` (upsert-by-id), and a multi-arm `A | B | None` union is returned as-is by `extract_optional`, not silently narrowed to one arm.
|
||
|
||
File Uploads:
|
||
- an `Upload` type is a first-class argument carried through IR, codegen, and dispatch binding, bound from multipart over HTTP and from the envelope over IPC (INVARIANTS.md § File Uploads) — observable: a function declaring an `Upload` parameter emits a distinguished IR shape rather than degrading to an opaque string, and dispatch binds a real file object the body can read.
|
||
|
||
---
|
||
|
||
## Unit: mizan-rust core (`cores/mizan-rust`)
|
||
|
||
**Charter.** The Rust analog of `mizan_core`: the IR data model, a KDL emitter that is byte-equivalent to the Python emitter, the compile-time (linkme) registry, the runtime invalidation/merge resolvers the HTTP and Tauri adapters call, and the cross-function graph checks. It owns the Rust side of the *same* IR contract; it does not own transport.
|
||
|
||
**Claims substantiated here.**
|
||
- Canonical KDL IR — "the IR must be validated against multiple adapters"; Rust is an IR authority (docs/AFI_ARCHITECTURE.md § KDL is the IR; README.md note 6).
|
||
- Mutation invalidation auto-scoping (three-tier) and merge on the Rust adapters (README.md § Adapters; § Merge via `mizan-tauri`/`mizan-rust-axum`).
|
||
- The IR is the only contract — divergence between adapters is what it exists to prevent (docs/AFI_ARCHITECTURE.md § KDL is the IR).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
|
||
Byte-equivalent KDL emission:
|
||
- `build_ir()` produces KDL byte-identical to the Python emitter against the same registered functions/types/contexts — observable: `cores/mizan-rust/tests/afi_parity.rs` and the three-way `tests/afi/test_codegen_parity.py` diff Rust output against the canonical Python-emitted `afi_ir.kdl` and require exact equality (line-by-line failure on any drift).
|
||
- the emitter reproduces the Python emitter's canonicalization exactly: alphabetical functions/contexts, sorted params, `shared-by`, snake→camel conversion, primitive-alias/enum inlining, and tree-shaking to types reachable from a registered function's input/output — observable: a `#[derive(Mizan)]` type not referenced by any function is omitted; an `Alias(Primitive)` or `Enum` named type inlines at its reference site instead of emitting a standalone `type` node, matching the Python output.
|
||
- channel nodes emit the same three client-named slots in the same order as the Python emitter (`params`, `client-message`, `server-message`) with the same pascal derivation — observable: the three-way parity fixture carrying a channel diffs byte-identical across Django, FastAPI, and Rust, so no backend can reintroduce a backend-shaped slot name into a backend-neutral IR.
|
||
|
||
Compile-time registry:
|
||
- `TYPES` / `CONTEXTS` / `FUNCTIONS` are linkme distributed slices populated at the consumer crate's expansion sites, and `lookup_function` / `context_members` resolve against them — observable: an IR-export bin that references one symbol per module force-links its registrations; dropping the reference drops the function from the emitted IR (the documented force-link requirement is real, not decorative).
|
||
|
||
Runtime invalidation & merge (must match the Python executor's semantics):
|
||
- `compute_invalidation` auto-scopes by matching mutation arg names against the affected context's declared Input params — observable: a mutation carrying `user_id` against a `user` context whose members declare `user_id` emits `{context:"user", params:{user_id:…}}`, while a non-matching arg emits the bare context string.
|
||
- `compute_merges` resolves the slot by structural return-type match against context members (via `types_match`), emitting `{context, slot, value}` only on a unique match and dropping ambiguous/no-match — observable: with two context members of different output shapes, a mutation's value routes to the single member whose type matches; two matching members drop the merge (fall back to refetch), never a bundle-order guess.
|
||
|
||
Cross-function graph checks (fail at IR-build time, before any client is emitted):
|
||
- `verify_invariants` panics with a structured message when an `affects`/`merge` target names an unregistered context, when a `merge` target has no unique matching member, or when a shared context param's type diverges across members — observable: an `affects = "ghost"` fails codegen with a named error; a `merge` whose context has two same-type members fails naming both; this is the whole-graph consistency the "IR prevents divergence" claim rests on.
|
||
|
||
---
|
||
|
||
## Unit: mizan-rust-macros (`cores/mizan-rust-macros`)
|
||
|
||
**Charter.** The proc macros — `#[derive(Mizan)]`, `#[mizan::context]`, `#[mizan::client]` — that make the Rust consumer surface author the same registry and IR shapes the Python decorator produces. It owns the compile-time codegen that emits `MizanType`/`FunctionSpec` impls and linkme registrations; it does not own runtime behavior.
|
||
|
||
**Claims substantiated here.**
|
||
- Rust/Tauri are "the IR authority via the `#[mizan::client]` macro + linkme registry" (README.md note 6).
|
||
- The `#[mizan::client]` surface mirrors the Python `@client` parameter set (backends/mizan-tauri/README.md § Define server functions; backends/mizan-rust-axum README).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `#[derive(Mizan)]` emits a `MizanType::shape()` matching the Python type introspection, honoring serde `rename_all`/`rename` so wire names match serialization, and registers a `TypeEntry` — observable: an enum with `#[serde(rename_all="snake_case")]` emits IR enum variants in snake form; a struct field `r#type` emits IR field name `type`.
|
||
- `#[mizan::client]` synthesizes a `<camelName>Input` struct + `MizanType` impl, registers the canonical `<camelName>Input`/`<camelName>Output` type entries (and the `Vec` element type for list outputs), and implements `FunctionSpec::dispatch` that deserializes JSON args into the typed input, awaits the body, and serializes the result — observable: `async fn user_orders(req, user_id: i64) -> Vec<OrderOutput>` registers `userOrdersOutput` as a list alias plus `OrderOutput`, and dispatch round-trips typed args; a `Result<T, MizanError>` return `?`-unwraps so user errors surface as the standard envelope, while the IR still sees only the `T` shape.
|
||
- `#[mizan::client]` enforces the same mutual-exclusion as Python (`context` vs `affects`/`merge`) and requires an `async fn` with an explicit return type — observable: `#[mizan::client(context = X, affects = Y)]` is a compile error; a non-async or return-typeless fn is a compile error.
|
||
- `#[mizan::context]` emits a `ContextMarker` with a snake_case (or explicit) name and registers a `ContextEntry` — observable: `#[mizan::context("user")]` and `#[mizan::context] struct UserCtx` both yield `NAME == "user"`; a non-unit struct is a compile error.
|
||
- input-param wire names strip the Rust `_`-underscore convention and bridge it with `#[serde(rename)]` — observable: `_user_id: i64` emits IR param name `user_id` and the synthesized Input renames the JSON key so dispatch deserializes the wire form.
|
||
|
||
---
|
||
|
||
## Unit: mizan-rust-ssr (`cores/mizan-rust-ssr`)
|
||
|
||
**Charter.** The embedded-V8 SSR engine, its PyO3 binding, and the anti-RSC guard. It owns rendering a build-time JS bundle to HTML in-process via `deno_core`, exposing that engine to the Python side across an in-process FFI boundary, and the structural guarantee that the SSR surface never imports an RSC/Flight runtime. It does not own the Django template backend (that is `mizan-django/ssr`) or the bundling that produces the JS bundle (that is `mizan-generate`).
|
||
|
||
**Claims substantiated here.**
|
||
- SSR is hand-rolled; no frontend adapter imports an SSR runtime or meta-framework (Next/Nuxt/SvelteKit/RSC/Flight) — the CVE-2025-55182 pre-auth-RCE deserialization class (HOLOMORPHICS/Mizan project note; MEMORY: mizan-ssr-no-framework-runtimes; enforced by `cores/mizan-rust-ssr/tests/no_rsc.rs`).
|
||
- SSR renders synchronously from props, injected as validated data (the AFI provides the typed one-way version).
|
||
- The Python side binds the engine through PyO3 — in-process FFI, no subprocess, no JSON-RPC framing; behavior lives in the binary, languages bind to it (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- the engine composes a real `deno_web` web-platform layer (TextEncoder/Decoder, MessagePort, timers) rather than a partial shim, evals the trusted bundle once, and renders per request — observable: the fixture bundle renders `Hello, World!`; a missing global fails loudly at render, not silently (a partial polyfill is silent-failure-shaped, which is why deno_web's real impls carry this).
|
||
- props cross as a `v8::json::parse`d value passed as a function argument, never spliced into evaluated source — observable: the injection test feeds a prop string crafted to break out of a string-built call; it renders as inert text and does not set a global, so code injection is structurally absent.
|
||
- the no-RSC guard scans authored SSR source and dependencies for the forbidden token set (`react-server-dom`, `renderToReadableStream`, `renderToPipeableStream`, `createFromReadableStream`/`Fetch`, `use server`, `next/`, `nuxt`, `@sveltejs/kit`) and fails on presence — observable: adding any RSC/Flight/meta-framework import to the scanned fixtures turns `no_rsc.rs` red; absence alone is not the guarantee — re-entry is loud.
|
||
- a PyO3 binding exposes `SsrEngine` to the Python side as an in-process extension: construct-from-bundle and `render(props_json) -> HTML` cross the FFI boundary with no process spawn, honoring V8's one-isolate-per-engine / non-`Send` constraint so the caller holds one engine per (worker thread, bundle) — observable: the Django backend imports the engine and calls `render(props)` in-process, and a prop still crosses as a parsed value, so the injection guarantee survives the FFI hop.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django dispatch (`backends/mizan-django/src/mizan/client`)
|
||
|
||
**Charter.** The Django HTTP/RPC dispatch surface: the executor that validates input, enforces auth, runs the function, and branches RPC-vs-view; the invalidation and merge resolvers; the context-bundle fetch; JWT/MWT request authentication. It owns per-request Django dispatch semantics; it does not own the registry, the IR, or the cache implementation (it calls them).
|
||
|
||
**Claims substantiated here.**
|
||
- RPC call dispatch returning `{result, invalidate}` and `merge` (README.md; MIZAN.md §4).
|
||
- Named-context bundle fetch — one GET returns all functions in the context, never N round-trips (INVARIANTS.md § Named Contexts; MIZAN.md §3).
|
||
- Mutation invalidation with three-tier auto-scoping; on failure nothing invalidates; developer writes no cache key (INVARIANTS.md § Mutation Invalidation).
|
||
- Invalidation travels two transports — the JSON body and the `X-Mizan-Invalidate` header — because a view-path response has no JSON body to carry it (INVARIANTS.md § Mutation Invalidation).
|
||
- Return-type branching: a data return takes the RPC path, an `HttpResponse` return takes the view path (backends/mizan-django/README.md § `@client` parameters).
|
||
- Auth enforced at dispatch, rejecting before the body runs, identically across transports (INVARIANTS.md § Auth; MWT_SPEC.md § Usage rule).
|
||
- Origin-side HMAC cache read/write on context fetch; `cache=False`/`rev` policy (docs/CACHE_KEYING.md; docs/PRODUCT_ARCHITECTURE.md § Spec surface).
|
||
- MWT/JWT server-side auth enforcement in the executor (`_check_auth_requirement`) (docs/MWT_SPEC.md § Usage rule).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
|
||
Dispatch & validation:
|
||
- `execute_function` validates input against the function's Pydantic `Input` before invoking the body, and rejects private functions from RPC — observable: a missing required field returns `VALIDATION_ERROR` with per-field detail and the body never runs; a `private=True` function returns `FORBIDDEN` when called over `/call/`.
|
||
- output serialization walks `BaseModel`/`list`/`dict` recursively via `to_jsonable_python` so `list[BaseModel]` reaches the wire as a bare array — observable: a `-> list[Item]` function returns `[{…},{…}]`, not `{"result":[…]}`; an `Optional[Model]` returning `None` serializes to `null` not `{"result":null}`.
|
||
|
||
Named-context bundle fetch (single request, param-filtered):
|
||
- `execute_context` runs every function in the group in one request, passing each only the params it declares, and fails the whole bundle if any member fails auth/validation — observable: `GET /ctx/user/?user_id=5&page=3` returns `{user_profile:…, user_orders:…}` where `user_profile` never sees `page`; if one member requires auth and the request is anonymous, the whole fetch returns the auth error, not a partial bundle.
|
||
|
||
Three-tier invalidation (the invariant that separates the AFI from typed RPC):
|
||
- `_resolve_invalidation` auto-scopes by matching mutation args against context param names (Tier 1), falling back to the bare context (Tier 3), and resolves function-level `affects` to the function name — observable: `update_profile(user_id=5,…)` against a `user` context emits `[{context:"user", params:{user_id:5}}]`; a mutation whose args don't overlap emits `["user"]`; `affects="user_profile"` emits the function name as the key.
|
||
- invalidation is emitted on both transports and only on success — observable: a successful mutation carries both `response["invalidate"]` (JSON body) and `X-Mizan-Invalidate: user;user_id=5` (header, URL-encoded so `q=hello world`→`q=hello%20world` and semicolons survive a parse round-trip); a mutation that raises emits neither.
|
||
- `_resolve_merges` resolves the merge slot server-side by matching the mutation's Output type against context members' Output types (`types_match_for_merge`), emitting `{context, slot, value, params?}` only on a unique match — observable: with `morph_groups: list[MorphGroupMeta]` and `morph_layers: list[MorphLayer]` in one context, a mutation returning `MorphLayer` merges into `morph_layers` only; the kernel does no shape inference.
|
||
|
||
Auth enforced before the body:
|
||
- `_check_auth_requirement` runs before `view.call`, handling `required`/`staff`/`superuser`/callable and mapping to `UNAUTHORIZED`/`FORBIDDEN` — observable: an anonymous call to `@client(auth=True)` returns `UNAUTHORIZED` and the function body never executes; a callable raising `PermissionError` surfaces its message as `FORBIDDEN`.
|
||
- MWT is checked first (`X-Mizan-Token`), then JWT (`Authorization: Bearer`), then session+CSRF; a present-but-invalid token is rejected (never a silent fall-through to session) — observable: an invalid `X-Mizan-Token` returns 401 without trying session auth; a valid MWT sets `request.user = MWTUser` with no DB query; CSRF is enforced only on the session path.
|
||
|
||
Return-type branching + origin cache:
|
||
- a function returning an `HttpResponse` takes the view path (invalidation rides the header, `Cache-Control: no-store`), while a data return takes the RPC path — observable: a `-> HttpResponseRedirect` mutation returns the 302 with `X-Mizan-Invalidate` set; the same-decorated `-> Shape` mutation returns JSON with `invalidate` in the body.
|
||
- context fetch consults the origin cache keyed by the effective `rev` (max across members) and effective cache policy (`False` short-circuits), stores deterministic (sorted-key) JSON on miss, and purges scoped/broad on mutation — observable: two identical fetches return byte-identical bodies and the second carries `X-Mizan-Cache: HIT`; a scoped mutation for `user_id=5` purges only that entry and leaves `user_id=6` a HIT; a context with any `cache=False` member emits `no-store`.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django cache (`backends/mizan-django/src/mizan/cache`)
|
||
|
||
**Charter.** The Django-side origin cache facade over `mizan_core`'s backends and key derivation — the free, unit-testable local cache that implements the same HMAC key and purge semantics as the paid Edge. It owns cache lifecycle/config resolution and the scoped-vs-broad purge dispatch; it does not own key derivation (delegates to core).
|
||
|
||
**Claims substantiated here.**
|
||
- Free framework origin-side cache implementing the full cache protocol locally, same HMAC key and purge as Edge (docs/PRODUCT_ARCHITECTURE.md § Origin-side cache; docs/CACHE_KEYING.md § Cache architecture).
|
||
- Scoped purge recomputes the key and deletes directly; broad purge SCANs the `ctx:{context}:*` prefix (docs/CACHE_KEYING.md § Required operations).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `cache_purge` recomputes the exact HMAC key for a scoped purge (one DELETE) and prefix-scans for a broad purge, so scoped invalidation touches exactly one entry — observable: `cache_purge(ctx, {user_id:5}, secret)` deletes only user 5's entry (returns 1) and leaves user 6; `cache_purge(ctx)` with no params removes every entry under the prefix.
|
||
- cache enablement is gated on both `cache_secret` and `cache_redis_url` present, thread-safe and lazily initialized — observable: with only one configured, caching is disabled and logged; concurrent `get_cache()` calls initialize once.
|
||
- the operability obligations the "same protocol as Edge, security-critical" claim rests on hold under concurrency and across languages: purge atomicity, cross-language stringification for every value type rather than bool/None alone, per-param sub-index cleanup on broad purge, single-flight protection against a thundering herd, one argument shape shared by `cache_get`/`cache_put`, and RedisCache exercised by the same suite as MemoryCache — observable: an index read racing a delete cannot resurrect a purged key; a broad purge leaves no orphaned per-param sub-index behind; N simultaneous misses on one key issue one origin fetch; a float or nested value stringifies identically in Python and TypeScript.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django channels (`backends/mizan-django/src/mizan/channels`)
|
||
|
||
**Charter.** The WebSocket transport: the `Channel` base + registry, the multiplexed consumer that handles channel subscribe/message and RPC-over-WS, server push, and channel schema export. It owns real-time bidirectional messaging and WS-transported RPC; it does not own HTTP dispatch (reuses the executor).
|
||
|
||
**Claims substantiated here.**
|
||
- WebSocket support: `websocket=` dispatched over a persistent connection; server-initiated messages reach subscribed contexts; declaration and wire semantics uniform across adapters (INVARIANTS.md § WebSocket Support).
|
||
- A channel is typed in both directions — typed params, a typed client→server message, a typed server→client message — so real-time traffic carries the same type contract RPC does (INVARIANTS.md § WebSocket Support; backends/mizan-django/README.md § Channels).
|
||
- Channels compose into the IR channel section (docs/AFI_ARCHITECTURE.md — codegen channels target consumes the channel nodes).
|
||
- Auth/authorization checked before any channel or RPC body runs (INVARIANTS.md § Auth; consumer security).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- the consumer multiplexes many channel subscriptions and RPC calls over one socket, keyed by `(channel, params_json)`, and validates Pydantic params/messages before `authorize`/`receive` — observable: subscribing with a wrong-typed param returns an error before authorization; a duplicate subscription to the same `(channel, params)` is rejected; unsubscribe leaves zero lingering subscriptions after rapid subscribe/unsubscribe cycles.
|
||
- WS-RPC only dispatches functions explicitly marked `websocket=True`, running the same `execute_function` (so validation/auth are identical to HTTP) — observable: an RPC call to an HTTP-only function returns `FORBIDDEN` ("use POST /call/"); a WS call to a `websocket=True` fn returns the same envelope shape as HTTP; a missing `id`/`fn` returns a structured error.
|
||
- `authorize()` gates every subscription and exceptions in it are contained — observable: `authorize` returning `False` blocks the subscribe with "Not authorized"; an `authorize` that raises returns an error rather than crashing the socket; room-level authorization enforces per-param access (room 1 allowed, room 999 rejected).
|
||
- server push (`Channel.push`) broadcasts to the channel-layer group, converting Pydantic to JSON, so a server function can reach subscribers — observable: `ChatChannel.push(room="general", message=…)` sends to `chat_general` with the message body; push with no channel layer configured warns rather than raising.
|
||
- a channel declares its wire types under the backend-neutral slot names the `Channel` base reads — `Params`, `ClientMessage` (travels client → server), `ServerMessage` (travels server → client) — and any slot left undeclared makes that direction unavailable — observable: a model declared under any other attribute name is invisible to the registry extension and to the IR, so the channel is exported as though that direction were absent.
|
||
- channel schema is exported into the registry's `channels` extension carrying the `params` / `client_message` / `server_message` shapes the channel declares plus a `bidirectional` flag — observable: a channel declaring a `ClientMessage` reports `bidirectional: true`; a push-only channel reports `false` and omits `client_message` while still carrying `server_message`; the KDL `channel` node names the same three slots as `params` / `client-message` / `server-message`, and the codegen channels target emits the matching typed envelopes and `useXChannel` hook.
|
||
- the OpenAPI channel document names each slot type through `mizan_core.ir.wire_to_pascal`, the same derivation the IR uses, and tabulates the per-channel slots under `x-mizan-channels` — observable: the `paramsType` / `clientMessageType` / `serverMessageType` entries and the `hasParams` / `hasClientMessage` / `hasServerMessage` flags name exactly the types the KDL `channel` node refers to, so the OpenAPI view and the IR view of one channel cannot disagree.
|
||
- JWT auth over the WS handshake authenticates from the `?token=` query param without a DB query, taking precedence over session — observable: a valid access token sets `scope["user"]` to a `JWTUser` from claims; an invalid token falls back to session rather than rejecting the socket.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django forms (`backends/mizan-django/src/mizan/forms`)
|
||
|
||
**Charter.** The Forms composition: `mizanFormMixin`/`mizanFormMeta` turning a Django Form into the three role-tagged server functions (schema/validate/submit), plus formsets, and the field schema/validation projection. It owns Django-Form-to-server-function translation; it does not own generic RPC dispatch. Auth-provider (django-allauth) forms are **out of scope** — the docs place them in a dedicated external `mizan-allauth` repository built on this mixin; this unit owes only the primitive they build on.
|
||
|
||
**Claims substantiated here.**
|
||
- Forms are three role-tagged client functions (schema / validate / submit) plus field validation, composed from RPC + validation (INVARIANTS.md § Compositions — Forms).
|
||
- A formset is the same three-role composition applied to a collection, so it introduces no fourth role (backends/mizan-django/README.md § Forms).
|
||
- Auto-registers `{name}.schema` / `.validate` / `.submit`; frontend gets `useXForm()` (backends/mizan-django/README.md § Forms).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `mizanFormMixin.__init_subclass__` auto-registers exactly three role-tagged server functions per concrete form (and formset variants when enabled), carrying `form`/`form_name`/`form_role` meta — observable: defining a `ContactForm` with a `mizanFormMeta(name="contact")` registers `contact.schema`, `contact.validate`, `contact.submit`; a form without a `mizan` attribute registers nothing; enabling `enable_formset` adds `contact.formset.{schema,validate,submit}`.
|
||
- the schema function projects each Django field into a typed `FieldSchema` (mapping field classes to Python types, extracting choices from `ModelChoiceField` safely, serializing initial values) and carries the `mizanFormMeta` display/behavior settings — observable: a `CharField`/`EmailField`/`Textarea` form yields three typed fields with correct `type`/`widget`; a `ModelChoiceField` yields JSON-serializable `{value,label}` choices (no `ModelChoiceIteratorValue` leak).
|
||
- validate runs the real Django form validation and returns structured per-field errors; submit branches multipart-vs-JSON, calls the form's `on_submit_success`/`on_submit_failure`, and returns pass/fail with data — observable: submitting an invalid email returns field errors and `success: false`; a valid submit runs `on_submit_success` and returns its data; a multipart submit binds files.
|
||
- `create_form_instance` threads `request`/`user`/`instance` init kwargs into the Django form and gracefully drops any the form doesn't accept, so the mixin is a reusable primitive for forms that need request context (the base the external `mizan-allauth` repo builds on) — observable: a form declaring a `request` kwarg receives it; a form that doesn't accept `request` still instantiates rather than raising `TypeError`.
|
||
- a forms codegen target emits the form clients against `mizanCall` from the kernel, so the frontend form surface stands on no hand-written provider — observable: `useXForm()` is generated from the IR's `is-form`/`form-name`/`form-role` fields and reaches the server through the kernel, exactly as every other generated client does.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django shapes (`backends/mizan-django/src/mizan/shapes`)
|
||
|
||
**Charter.** The "API Shapes" primitive: Pydantic-typed queryset projection over django-readers, PK-keyed structural diffing (add/modify/delete) across nested relations. It owns ORM projection and diff derivation; it does not own dispatch.
|
||
|
||
**Claims substantiated here.**
|
||
- API Shapes to the fullest extent: ORM integration, auto-diffing by primary key (add/modify/delete, Django as reference), authorable near the used function (INVARIANTS.md § API Shapes).
|
||
- A Shape is one declaration serving as both the wire type and the query plan — the Pydantic field set compiles to a django-readers projection (INVARIANTS.md § API Shapes).
|
||
- Context classes send/receive with Shape diffing (INVARIANTS.md § Compositions — Context classes; MIZAN.md §5).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `Shape.query` compiles a django-readers projection from the Pydantic field set + nested Shapes, executing minimal queries (single query for flat, prefetch for nested) and validating each row — observable: a flat shape query runs one SQL query; a nested `AuthorCardShape` with `books` runs two (prefetch), not N+1; per-relation querysets filter nested rows (`books=lambda qs: qs.filter(is_published=True)`).
|
||
- diffing computes add/modify/delete by primary key across nested relations, using a single batched query for existing rows and strict access to nested diffs — observable: `diff_many` of mixed new+existing items runs one query for the existing set; a nested diff reports `created`/`updated`/`deleted` by child PK; accessing a mistyped nested name raises (KeyError/AttributeError) rather than silently returning empty.
|
||
- PK/type resolution handles integer, slug, and UUID primary keys, two FKs to the same model, self-referential and nullable FKs, and treats `False`/`0`/`""` as present values — observable: a UUID-PK `Section` shape diffs correctly; `is_published=False` is not treated as missing; a nullable editor FK returns `None` rather than erroring.
|
||
- the `ReactContext('name')` class form carries `send`/`receive` and a `POST /ctx/<name>/commit/` endpoint that routes committed shape data to `receive`, with auto-refetch-or-fresh-return after commit (INVARIANTS.md § Compositions; MIZAN.md §5) — observable: a class defining `send`/`receive` generates a read hook and a commit function; committing runs `receive` and either refetches the context or splices the Shape `receive` returned, so the client never holds a post-commit stale bundle.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django SSR (`backends/mizan-django/src/mizan/ssr`)
|
||
|
||
**Charter.** The SSR product's Django half: a Django template backend that renders `.tsx`/`.jsx` component files by resolving each to its built bundle and driving the PyO3-bound `SsrEngine` in-process, wrapping output with a hydration payload. It owns the Django-template-engine integration, component-to-bundle resolution, prop gathering, and the per-(worker-thread, bundle) engine lifecycle; it does not own the V8 render (that is `cores/mizan-rust-ssr`'s `SsrEngine`) or the bundling that produces the render bundle (that is `mizan-generate`).
|
||
|
||
**Claims substantiated here.**
|
||
- SSR is a Django template backend replacing the rendering engine; the template name IS a `.tsx`/`.jsx` file path; context dict becomes props; output wrapped in `<div id="mizan-root">` + `window.__MIZAN_SSR_DATA__` hydration (docs/SSR_ARCHITECTURE.md).
|
||
- The render engine is embedded-V8 inside the Mizan Rust binary, bound via PyO3 — in-process FFI, no external JS runtime serving requests, no subprocess, no JSON-RPC framing (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary).
|
||
- The component path resolves against `DIRS` to its built bundle in `OPTIONS['bundles']` (produced by mizan-generate's SSR bundling step); the backend gathers props and wraps output for hydration (docs/SSR_ARCHITECTURE.md § AFI boundary; the `TEMPLATES` config block).
|
||
- One engine per (worker thread, bundle) — V8's Locker constraint makes an engine non-`Send`, so the Django side never shares one across threads (docs/SSR_ARCHITECTURE.md § The engine).
|
||
- SSR is orthogonal to RPC and composable; first paint carries data (INVARIANTS.md § SSR).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `MizanTemplates` implements Django's template-backend interface: `get_template(name)` resolves `name` as a `.tsx`/`.jsx` file path under `DIRS` and returns a `MizanTemplate` for the resolved component; `render` strips `request`/`csrf_token` and passes the remaining context as props — observable: `render(request, 'components/Hello.tsx', ctx)` renders that component with `ctx` as props; `from_string` raises (it renders files, not strings); a missing file raises `TemplateDoesNotExist`.
|
||
- component-to-bundle resolution: the backend resolves each component file to its self-contained render bundle in `OPTIONS['bundles']` (the `mizan-generate`-produced bundle assigning `globalThis.renderApp`) and hands the bundle to the engine, rather than reading component source directly — observable: `render` of `components/Hello.tsx` loads that component's built bundle from the configured `bundles` directory; a component with no built bundle raises rather than rendering stale or empty HTML.
|
||
- rendered output is wrapped for client hydration — observable: output contains `<div id="mizan-root">…</div>` plus `<script>window.__MIZAN_SSR_DATA__={sorted-json}</script>`, so first paint carries the props the client hydrates from.
|
||
- engine lifecycle: the backend constructs one `SsrEngine` per (worker thread, bundle) pair and reuses it across requests, never sharing a single engine across threads (the engine is non-`Send`) — observable: concurrent renders on different worker threads each use their own thread-local engine and return correct results with no interleaving; the engine is built once per bundle, not per render.
|
||
- the Django backend binds the engine through PyO3 and renders in-process: no external JS runtime process, no newline-delimited JSON-RPC correlation, no ready-signal or auto-restart machinery, and `OPTIONS` carries `bundles` (a directory of built bundles) rather than `worker` (a JS entry file) — observable: a render spawns no subprocess and calls the PyO3-bound `SsrEngine.render(props)` directly, so a render failure is a Python exception rather than a lost correlation id.
|
||
- render-on-mutation orchestration (mutation → trigger local render → store HTML) is driven by the manifest's `render_strategy`, wiring the engine to the PSR path (docs/PSR_VS_EDGE.md) — observable: a mutation against a public context triggers a local re-render and stores the HTML, so the next request for that page is served pre-rendered rather than rendered on demand.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django JWT/MWT (`backends/mizan-django/src/mizan/jwt`)
|
||
|
||
**Charter.** The Django identity layer: JWT access/refresh tokens tied to sessions, the MWT-mint server functions, JWT settings/algorithm resolution, and the Ninja security class. It owns Django-session-bound token issuance and validation; the MWT format itself lives in `mizan_core.mwt`.
|
||
|
||
**Claims substantiated here.**
|
||
- JWT auth is session-bound: the access/refresh pair is auto-detected at dispatch, and CSRF is handled on the session path only (backends/mizan-django/README.md § Setup; INVARIANTS.md § Auth).
|
||
- MWT is issued from an authenticated identity; `create_mwt(user, secret, ttl, audience, kid)`; a separate JWT module carries user-auth tokens (docs/MWT_SPEC.md § Key decisions).
|
||
- MWT is the cache-keying identity, not a replacement for JWT auth (docs/MWT_SPEC.md).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- JWT tokens carry `sub`/`sid`/`staff`/`super`/`type`/`iat`/`exp` and are tied to a session key so logout revokes them — observable: a refresh whose underlying session was destroyed returns `None` (immediate revocation); an access `JWTUser` is built from claims with no DB query; `decode_token` enforces the expected token type.
|
||
- settings auto-detect algorithm from key shape (PEM→RS256 else HS256) and derive the public key from the private RSA key when absent — observable: an HS256 secret works with `public_key == private_key`; a PEM private key auto-selects RS256 and extracts the public key.
|
||
- `mwt_obtain` mints an MWT from the authenticated session via `create_mwt`, requiring `MIZAN_MWT_SECRET`, and `jwt_obtain`/`jwt_refresh` issue/rotate the JWT pair carrying user claims — observable: `mwt_obtain` on an anonymous request raises; with no secret configured it raises a clear config error; the JWT pair includes `is_staff`/`is_superuser` so downstream auth needs no DB query.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django registration & export (`backends/mizan-django/src/mizan/export`, `.../management`, `.../setup`, `.../__init__.py`, `.../urls.py`, `.../_vendor`)
|
||
|
||
**Charter.** The Django discovery/registration glue and the two protocol export surfaces: the Edge manifest generator and the KDL IR management command, plus URL wiring, session-init, and the ASGI/channels wrapper. It owns clients.py auto-discovery and the manifest/IR export commands; it delegates registry and IR shape to `mizan_core`.
|
||
|
||
**Claims substantiated here.**
|
||
- Codegen IR export (KDL) via `python manage.py export_mizan_ir` (backends/mizan-django/README.md § Generate the frontend; docs/AFI_ARCHITECTURE.md § KDL is the IR).
|
||
- The Edge manifest is a deterministic (sorted) derivation of the registry covering both RPC and view-path functions, and records each context's `render_strategy` (docs/PSR_VS_EDGE.md § PSR — Preemptive Static Rendering).
|
||
- Function discovery / registration via the clients.py convention (backends/mizan-django/README.md § Setup; MIZAN.md §6).
|
||
- Session / CSRF init endpoint; `wrap_asgi` WebSocket routing (backends/mizan-django/README.md § Setup).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `export_mizan_ir` populates the registry via discovery, then writes canonical KDL from `mizan_core.ir.build_ir` — observable: the Django-emitted KDL is byte-identical to the FastAPI and Rust emissions for the same fixture (`tests/afi/test_codegen_parity.py`); this is the "IR is the only contract, validated against multiple adapters" claim made checkable.
|
||
- `generate_edge_manifest` emits a deterministic (sorted contexts and mutations) JSON mapping contexts to endpoints/params/functions, distinguishing rpc vs view path, marking `user_scoped` and `render_strategy` (`dynamic_cached` for user-scoped, `psr` for public), and mutations with auto-scoped params + private/route — observable: two exports are byte-identical regardless of registration order; a context with `user_id` is `user_scoped`+`dynamic_cached`; a view-path function's `route` populates `page_routes`; a mutation whose args match context params lists them under `auto_scoped_params`.
|
||
- `mizan_clients` discovers `ServerFunction` subclasses under each app's `clients.py`/`clients/` layer and registers them idempotently — observable: re-running discovery does not double-register; a class already registered under a different name is skipped rather than clobbered.
|
||
- the session-init view sets the CSRF cookie and returns the token, and `wrap_asgi` routes `/ws/` to the channels consumer — observable: `GET /session/` returns `{csrfToken}` and a `Set-Cookie: csrftoken=…`, so SSR/clients can establish CSRF before an authenticated call; `wrap_asgi(get_asgi_application())` produces a ProtocolTypeRouter dispatching http vs websocket.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django protocol tests (`backends/mizan-django/src/mizan/tests` — test_core.py, test_auth.py, test_ssr.py, test_benchmarks.py)
|
||
|
||
**Charter.** The Django backend's protocol-and-integration verification: the executor/registry/invalidation/merge/cache/manifest/edge-compatibility/auth/SSR/throughput suites. It holds the evidence that the dispatch, invalidation, cache, auth, and SSR mechanisms above behave as claimed against the real HTTP stack; it authors no production mechanism.
|
||
|
||
**Claims substantiated here.**
|
||
- The dispatch, invalidation, merge, cache, auth, and SSR claims of the mizan-django dispatch/cache/ssr/jwt sub-units are *verified* here.
|
||
- Edge caching is provable without an Edge — deterministic JSON, correct Cache-Control, header round-trip, auth-differentiated responses (test_core.py § EdgeCompatibilityTests, a doc-shaped claim carried in the suite).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- the suite exercises the real HTTP stack (Django test client / LiveServer) — not just RequestFactory — for dispatch, three-tier invalidation, merge, view-path branching, and cache HIT/MISS/scoped-purge — observable: `HTTPIntegrationTests` and `CacheIntegrationTests` assert the JSON body and `X-Mizan-Invalidate` header agree, that a scoped mutation preserves other users' cached entries, and that a second identical fetch is a HIT.
|
||
- the auth suite covers every axis (JWT valid/invalid/expired, MWT, session, staff/superuser/callable/PermissionError) and asserts the body never runs on failure — observable: an invalid token returns 401 without session fall-through; an anonymous call to an auth-required function returns before the body; a callable's `PermissionError` message surfaces verbatim.
|
||
- the Edge-compatibility suite asserts the properties a CDN cares about (deterministic byte-identical bodies, sorted JSON keys, URL-encoded delimiter-safe headers, `no-store` on errors/mutations, header↔body invalidation agreement, auth-differentiated responses for the same URL) — observable: these tests go red if any of those properties regress, so "Edge caching is possible" is checkable without a CDN.
|
||
- the SSR suite verifies the engine-based render path — template-backend resolution, bundle-driven in-process render, the hydration wrapper, and concurrent renders — and asserts that no external JS runtime process is spawned during the suite (the PyO3-bound `SsrEngine` renders in-process) — observable: a render produces the `<div id="mizan-root">` + `__MIZAN_SSR_DATA__` wrapper from the resolved bundle, concurrent renders across worker threads each use their own engine and return correct results, and the suite spawns no `bun`/`node` subprocess.
|
||
- the benchmark suite measures HTTP-vs-executor overhead and throughput with correctness assertions on each path — observable: every benchmark also asserts the function's numeric output, so a green benchmark run is also a correctness run.
|
||
|
||
---
|
||
|
||
## Unit: mizan-django adversarial & feature tests (`backends/mizan-django/src/mizan/tests` — test_pentest.py, test_security.py, test_channels.py, test_shapes.py)
|
||
|
||
**Charter.** The Django backend's adversarial and feature-specific verification: the penetration/security suites (attacker-shaped defenses) and the channels/shapes suites (feature behavior). It holds the evidence that validation, authorization, channel subscription, and shape diffing behave as claimed against hostile and edge inputs; it authors no production mechanism.
|
||
|
||
**Claims substantiated here.**
|
||
- The auth-guard, input-validation, and no-info-disclosure claims (INVARIANTS.md § Auth; executor validation) are verified adversarially here.
|
||
- The WebSocket channel authorization/subscription and API Shapes diff/query claims (INVARIANTS.md § WebSocket Support, § API Shapes) are verified here.
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- the pentest and security suites assert the properties an attacker probes: validation-runs-before-execution, private/internal functions unreachable over RPC, no sensitive detail in production error messages, injection strings (SQL/command/template/prototype-pollution/unicode-lookalike/zero-width) treated as inert data, and no function-existence timing leak — observable: these tests go red if the executor ever runs a body before validation, leaks a secret in a 500, or executes an injection payload.
|
||
- the channels suite verifies subscription lifecycle and authorization: param validation before `authorize`, `authorize`-false and `authorize`-raise both blocking cleanly, duplicate-subscription rejection, room-level per-param authorization, and WS-RPC gated to `websocket=True` functions — observable: subscribing to a room the user cannot access is rejected; an RPC to an HTTP-only function returns FORBIDDEN over the socket.
|
||
- every channel fixture these suites register subclasses the `Channel` base and declares its wire models under the `Params` / `ClientMessage` / `ServerMessage` slot names that base reads — observable: a fixture's declared message model reaches the exported schema and the IR; a model declared under any other attribute name is invisible to both, so the suite would be asserting against a channel the contract sees as slotless.
|
||
- the extension-schema tests pin both directions of the `bidirectional` flag — observable: a fixture declaring `ClientMessage` asserts `bidirectional` true with both `client_message` and `server_message` present, and a push-only fixture asserts `bidirectional` false with `client_message` absent, so neither the flag nor the slot keys can be renamed without turning the suite red.
|
||
- the shapes suite verifies query efficiency and diff correctness across the hard cases: single-query flat, prefetch nested (no N+1), UUID/slug/int PKs, two-FKs-to-same-model, self-referential and nullable FKs, `False`/`0`/`""` treated as present, batched `diff_many`, and strict nested-diff access raising on typos — observable: a nested query asserts exactly the prefetch count; a mistyped nested-diff name raises rather than silently returning empty.
|
||
|
||
---
|
||
|
||
## Unit: mizan-fastapi (`backends/mizan-fastapi/src/mizan_fastapi`)
|
||
|
||
**Charter.** The FastAPI adapter targeting the AFI-common subset: RPC dispatch, context bundling, JSON-body invalidation + merge, auth gating, the error envelope, the channel registry extension and socket handler, and the KDL IR CLI. It owns the FastAPI transport surface over `mizan_core`; Forms/Shapes/SSR are explicitly out of scope.
|
||
|
||
**Claims substantiated here.**
|
||
- RPC call dispatch, named-context bundle fetch, JSON-body invalidation, three-tier auto-scoping, function registration, KDL IR export (README.md § Adapters; backends/mizan-fastapi/README.md § Scope).
|
||
- Auth-guard enforcement (`auth=` rejects) (backends/mizan-fastapi/README.md § Auth integration).
|
||
- The same core primitives as Django, proving the protocol is not Django-specific; IR-shape parity with Django and Rust (README.md § Conformance; docs/AFI_ARCHITECTURE.md).
|
||
- Every error path renders through the Mizan envelope; `GET /session/` returns a null CSRF token for wire parity (backends/mizan-fastapi/README.md § Setup; README.md § Adapters note 7).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `execute_function` looks up the registered function, enforces `auth` before running (matching Django's semantics: `True`/`required`/`staff`/`superuser`/callable), validates input against the Pydantic `Input`, awaits `view.acall` (async handlers on the loop, sync in a threadpool), and serializes via `jsonable_encoder` — observable: an anonymous call to `@client(auth=True)` returns 401 before the body; an `async def` handler runs on the loop (a real `await` inside completes); `list[BaseModel]`/`Optional[BaseModel]` reach the wire bare.
|
||
- `compute_invalidation` auto-scopes by matching args against the context's declared Input fields, emitting a bare context or a `{context, params}` object — observable: a mutation with a matching arg emits the scoped form, a non-matching arg the bare context string; identical to the Django resolver's output.
|
||
- `compute_merges` resolves the slot by unique return-type match (`types_match_for_merge`) and emits `{context, slot, value, params?}`, dropping ambiguous — observable: the `morph_groups`/`morph_layers` fixture routes a `MorphLayer` mutation to `morph_layers` only; a merge-only mutation emits `merge` with empty `invalidate`.
|
||
- the router exposes `POST /call/`, `GET /ctx/{name}/`, `GET /session/` and both exception handlers render every failure through `{"error":{code,message,details?}}` with `Cache-Control: no-store` — observable: an unknown function returns 404 in the envelope; a malformed body returns `BAD_REQUEST`; a validation failure returns 422; `/session/` returns `{csrfToken: null}` (parity, since CSRF is Django-only).
|
||
- the `Channel` base declares the same three backend-neutral slots as the Django base (`Params`, `ClientMessage`, `ServerMessage`) and its `channels` registry extension exports the same key set (`name`, `type`, `bidirectional`, plus the declared `params`/`client_message`/`server_message` schemas) — observable: a channel declaring `ClientMessage` reports `bidirectional: true` and a `client_message` schema whose entry is comparable key-for-key with Django's entry for an identically-declared channel, so the slot names carry on a backend with no Django in it.
|
||
- the socket handler fans a message out to exactly the subscribers whose params key the group, dropping a socket that fails to take a frame — observable: two subscribers with the same params receive one push; a departed socket is discarded from the group with the failure surfaced rather than swallowed, so one dead client cannot wedge the broadcast.
|
||
- `python -m mizan_fastapi.ir <module>` imports the module (triggering registration) and writes canonical KDL — observable: its output equals the Django management command's output for the same fixture (three-way parity).
|
||
|
||
---
|
||
|
||
## Unit: mizan-rust-axum (`backends/mizan-rust-axum`)
|
||
|
||
**Charter.** The Rust/Axum HTTP adapter: the `/call/`, `/ctx/:name/`, `/session/` handlers, the error envelope, and app-state threading, dispatching through `mizan-core`'s `FUNCTIONS` registry. It owns the Axum wire surface; dispatch/invalidation/merge logic is `mizan-core`.
|
||
|
||
**Claims substantiated here.**
|
||
- RPC call dispatch, named-context bundle fetch, JSON-body invalidation, three-tier auto-scoping, KDL IR export (README.md § Adapters; note 6).
|
||
- Axum error envelope mirrors FastAPI's with `Cache-Control: no-store` (backends/mizan-rust-axum/src/errors.rs).
|
||
- Query params are coerced to typed JSON via the per-function input params (handlers.rs).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `function_call` dispatches through `lookup_function` + `FunctionSpec::dispatch`, then attaches `compute_invalidation` and `compute_merges` output, mirroring the FastAPI response shape `{result, invalidate, merge?}` — observable: the wire-parity drivers (`tests/rust/drive_kernel.rs`, `drive_emitted.rs`) run the same probes against the Axum server and FastAPI and require the same JSON shapes and invalidate/merge semantics.
|
||
- `context_fetch` bundles every registered member of the context and coerces string query params to typed JSON via each function's `input_params` primitive table — observable: `GET /ctx/user/?user_id=5` returns the flat bundle with `user_id` coerced to an integer before dispatch; an unknown context returns the envelope 404.
|
||
- app state is type-erased into the handle and downcast in user functions — observable: a handler downcasts `RequestHandle` to the concrete state type; the stateless router variant threads a unit handle.
|
||
- the adapter honors every declaration it accepts, so no function reaches the wire with a declared property the transport ignores (README.md § Caveat, notes 2/3/5; INVARIANTS.md § Auth): a `Transport::Websocket` function is routed through a WebSocket handler, an `is_form`/`form_role` function is reachable through validate/submit endpoints, and `auth=` is enforced in the dispatch path — observable: an `auth=True` function is rejected for an anonymous caller on this adapter exactly as it is on Django and FastAPI, which is what makes "auth enforced on every adapter" a single claim rather than a per-adapter one.
|
||
|
||
---
|
||
|
||
## Unit: mizan-tauri (`backends/mizan-tauri`)
|
||
|
||
**Charter.** The Tauri adapter: a plugin exposing a single `mizan_invoke` command that routes op-tagged call/fetch envelopes through the shared `mizan-core` registry over Tauri IPC. It owns the IPC wire surface; dispatch/invalidation/merge are `mizan-core`.
|
||
|
||
**Claims substantiated here.**
|
||
- RPC call dispatch, named-context bundle fetch, invalidation (JSON body only), three-tier auto-scoping (README.md § Adapters; note 1).
|
||
- Transport is Tauri IPC (a single `#[tauri::command]` envelope), not HTTP; invalidation rides the response body; no header channel (README.md note 1; backends/mizan-tauri/README.md § Wire protocol).
|
||
- `RequestHandle` wraps `AppHandle` so functions can access managed state; `Result<T, MizanError>` supported (backends/mizan-tauri/README.md § App-state access).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- the plugin registers exactly one command (`plugin:mizan|mizan_invoke`) that deserializes the op-tagged envelope and dispatches `call`/`fetch` through the same `FUNCTIONS`/`CONTEXTS` slices the HTTP adapter uses — observable: `{op:"call", fn, args}` returns `{result, invalidate, merge?}` and `{op:"fetch", context, params}` returns the flat bundle, identical shapes to the axum adapter minus the header channel; there is no per-function `#[tauri::command]`.
|
||
- errors flow through Tauri's reject path re-wrapped into the `{code, message, details?}` shape — observable: a `MizanError::ValidationFailed` reaches the JS transport as the same envelope an HTTP 422 would carry, so consumer error handling is transport-agnostic.
|
||
- `RequestHandle::new(app)` lets a function downcast to `tauri::AppHandle` for managed state / event emission — observable: a function calling `req.downcast::<tauri::AppHandle>()` reaches Tauri state; stateless functions ignore the handle.
|
||
- the `auth`/`private` fields Tauri's `FunctionSpec` carries are enforced in the dispatch path (README.md § Caveat, note 5; INVARIANTS.md § Auth) — observable: an `auth=`-declared function is rejected for an unauthorized caller over IPC, and a `private=True` function is unreachable through `mizan_invoke`, so a desktop build cannot be the one transport where a declared guard is decorative.
|
||
|
||
---
|
||
|
||
## Unit: mizan-rust client kernel (`frontends/mizan-rust`)
|
||
|
||
**Charter.** The Rust port of the shared client kernel: the reconciled cache (context registry + state), transport (HTTP with retry, CSRF), merge splicing, the debounced invalidation queue, error-envelope parsing, and the PyO3 bridge that exposes the kernel to Python. It owns the client-side reconciled view; framework rendering lives in adapters.
|
||
|
||
**Claims substantiated here.**
|
||
- The client kernel owns the reconciled cache — context state, status, error, server-driven merge and invalidate, session init — reached through a pluggable transport; no adapter keeps its own copy of the truth (INVARIANTS.md § Client Kernel; docs/AFI_ARCHITECTURE.md § Kernel model).
|
||
- Mutation invalidation auto-refetches affected contexts; on failure nothing invalidates (INVARIANTS.md § Mutation Invalidation).
|
||
- Merge splices the return value into the cached entry rather than refetching (the `merge=` path; MIZAN.md §5 fresh-return optimization generalized).
|
||
- Transports are pluggable (HTTP, Tauri IPC, webview) via `configure` (docs/AFI_ARCHITECTURE.md § Kernel model; frontends/mizan-tauri-transport/README.md).
|
||
- The Python client is a typed facade over this kernel via PyO3 (protocol/mizan-codegen python target; baselines/python/client.py).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- the context registry keys entries by context name + `stable_key(params)`, holds one `ContextState {data, status, error}` per entry, and notifies subscribers via a watch channel that coalesces to the latest state — observable: `stable_key({b,a})` == `stable_key({a,b})` (byte-identical to `JSON.stringify` with sorted keys), so the same params hit the same cache entry regardless of key order; a refetch advances the entry through Loading→Success visible to subscribers.
|
||
- `mizan_call` applies the response's `merge` entries first, then queues `invalidate` entries, then returns `result` — observable: a mutation response `{result, merge, invalidate}` splices the merged slot into the cached bundle AND schedules refetch; a failed call (4xx) surfaces the error and invalidates nothing.
|
||
- `splice_slot` upserts by `id` into an array slot, replaces an array slot with a new array, replaces a scalar, and no-ops a merge into a slot absent from the bundle — observable: merging `{id:1,name:"A"}` into `[{id:1,…},{id:2,…}]` replaces entry 1 in place; merging into a missing slot leaves the bundle untouched (no fabricated slot on a stale cache).
|
||
- the invalidation queue debounces within one async tick, and broad invalidations subsume scoped ones for the same context — observable: two invalidations queued in the same tick flush once; a broad invalidate refetches every param variant while a scoped invalidate refetches only the matching entry.
|
||
- transport is HTTP-with-retry (3 attempts, linear backoff, retry on 5xx/network, surface 4xx immediately), reads the CSRF cookie into the configured header per call, and is swappable — observable: a 5xx retries then errors; a 4xx returns immediately; swapping the transport (Tauri/webview) leaves the generated call/fetch code unchanged (transport read from config).
|
||
- the error envelope parses both the FastAPI nested shape and the Django flat shape, falling back to `HTTP_<status>` — observable: `{"error":{"code":…}}` and `{"error":true,"code":…}` both yield the correct `code`; an unparseable body yields `HTTP_500` with the raw body.
|
||
- the PyO3 bridge exposes `call`/`fetch_context`/`subscribe_context`/`invalidate` with the GIL released across the network round-trip, and fires the Python subscription callback on each watch change with a `{data,status,error}` dict — observable: `py.allow_threads` wraps the blocking call; a subscription callback fires with `status: "success"` and the decoded data; cancelling ends the watcher.
|
||
|
||
---
|
||
|
||
## Unit: mizan-base and framework adapters (`frontends/mizan-base`, `frontends/mizan-react`, `frontends/mizan-vue`, `frontends/mizan-svelte`)
|
||
|
||
**Charter.** The TypeScript client kernel (`@mizan/base`) and the per-framework idiomatic adapters (React hooks, Vue composables, Svelte stores) that subscribe to it. `@mizan/base` is the authoritative kernel the `frontends/mizan-rust` unit ports. The `mizan-ts` cross-language HMAC pin (`deriveCacheKey`) also lives on the TS side.
|
||
|
||
**Claims substantiated here.**
|
||
- Every frontend adapter is a thin idiomatic wrapper over one shared kernel; the kernel owns `ContextState<T> = {data,status,error}`, `registerContext`, `mizanCall`/`mizanFetch`, server-driven merge/invalidate, `initSession`, and a pluggable `MizanTransport` (HTTP default, Tauri/webview swap via `configure`) (INVARIANTS.md § Client Kernel; docs/AFI_ARCHITECTURE.md § Kernel model).
|
||
- Shared parameters elevate to required provider props; non-shared params elevate to optional props with per-function override (INVARIANTS.md § Named Contexts; MIZAN.md §2 param elevation / `specify` resolution order).
|
||
- Codegen targets the adapter surface, never the raw kernel; React devs get hooks, Vue composables, Svelte stores, same kernel underneath (docs/AFI_ARCHITECTURE.md § Kernel model).
|
||
- Vue and Svelte are co-equal codegen targets over the same kernel, not React-derived (docs/AFI_ARCHITECTURE.md § Authoring surface).
|
||
- Cross-language HMAC pin: `deriveCacheKey` in `mizan-ts` matches the Python key byte-for-byte (docs/CACHE_KEYING.md; README.md § Adapters — TypeScript is the protocol-reference adapter).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `@mizan/base` owns the single reconciled view: `ContextState`, the context registry, `mizanCall`/`mizanFetch`, server-driven `merge`/`invalidate`, `initSession`, over a `MizanTransport` interface — observable: the same behaviors the `mizan-rust` port pins (stable-key cache identity, merge-splice, scoped-vs-broad refetch, retry, dual-envelope error parse) hold in TS; the Rust port mirrors this file behavior-for-behavior.
|
||
- the generated provider elevates a context's shared params to required props and its non-shared params to optional props, and resolves each member's effective params by overlaying per-function overrides onto the provider props at fetch time (INVARIANTS.md § Named Contexts; MIZAN.md §2 resolution order) — observable: a two-function `user` context where both take `user_id` and only one takes `page` generates a provider with required `user_id` and optional `page`; a per-function override supplies a different `page` for that one member while the shared `user_id` still covers both, and a member still missing a required param at fetch time is a runtime error, not a silent undefined.
|
||
- `deriveCacheKey` (mizan-ts) reproduces the Python HMAC key byte-for-byte — observable: the pinned vectors in `cores/mizan-python/tests/test_keys.py::test_cross_language_pin` (`ctx:user:605a1ca5…`, `ctx:user:30fc08eb…`) are asserted against the TS output; any normalization drift (bool/None stringification, key ordering) breaks the pin, which the doc marks a security vulnerability.
|
||
- adapters subscribe to the kernel and render in their own idiom without keeping a parallel copy of the truth — observable: a React hook and a Vue composable over the same context read the same kernel entry; mutating in one path updates both because the truth lives once in the kernel.
|
||
- the channel subscription surface (`useChannel` and the `ChannelSubscription<Params, Server, Client>` type) orders its type parameters by direction, so a hook's inbound and outbound message types cannot be transposed — observable: a push-only channel's generated hook types its outbound message as `never`, making a client-side send on that channel a compile error rather than a runtime drop.
|
||
- `frontends/mizan-vue` and `frontends/mizan-svelte` are runtime kernel-adapter packages, not codegen output alone — observable: a Vue composable and a Svelte store each subscribe to `@mizan/base` and refresh on invalidation against a live backend, so co-equality is checkable at the runtime layer and not only at the byte-parity layer.
|
||
- the Svelte codegen target emits Svelte 5 `$state`/`$derived` runes (INVARIANTS.md § Client Kernel — "Svelte runes") — observable: the emitted Svelte client reads context state through runes rather than through `readable` stores.
|
||
- every React provider that ships subscribes to the kernel, so no adapter module holds context state of its own (INVARIANTS.md § Client Kernel) — observable: the desktop example imports the codegen-emitted kernel-subscribing `MizanContext`, and `mizan-react` exposes no second provider keeping a parallel copy of the truth.
|
||
- the generated channel client in the example harness names the channel slots from the client's side — observable: `examples/django-react-site/harness/src/api/channels.ts` declares `<Pascal>ClientMessage`/`<Pascal>ServerMessage` types with `hasClientMessage`/`hasServerMessage` flags and `clientMessageType`/`serverMessageType` keys, matching what `protocol/mizan-codegen`'s channels target emits from the IR, so the harness is regenerable rather than divergent.
|
||
|
||
---
|
||
|
||
## Unit: mizan-codegen (`protocol/mizan-codegen`)
|
||
|
||
**Charter.** The single Rust codegen binary that reads KDL IR and emits typed clients for every target (stage1, react, vue, svelte, channels, python, rust), plus the source-fetching that spawns each backend's IR-export command and the Pydantic-pre-step, plus the SSR bundling step that compiles each SSR entry component into a self-contained render bundle. It owns the IR→client transform and the SSR bundle production; it does not emit IR (backends do) or run the render engine (that is `cores/mizan-rust-ssr`).
|
||
|
||
**Claims substantiated here.**
|
||
- Codegen reads KDL directly — no OpenAPI envelope, no `openapi-typescript`, no per-backend converter (docs/AFI_ARCHITECTURE.md § KDL is the IR).
|
||
- Every frontend client is generated from the IR; each target is byte-parity-tested (INVARIANTS.md § Canonical IR & Codegen).
|
||
- The codegen drives the backend's IR-export command as a subprocess and parses the KDL it writes (docs/AFI_ARCHITECTURE.md; backends/*/README.md § Generate the frontend).
|
||
- Stage 1 (typed `callXxx`/`fetchXxx`) + Stage 2 (`<MizanContext>` provider, per-context providers, `use{Hook}()`) emission (backends/*/README.md § Generate the frontend).
|
||
- Pydantic + Rust DX: a decoru pre-step authors Rust types from Pydantic before the cargo IR bin runs; a generic `[source.script]` source spawns any command emitting KDL (backends/mizan-tauri/README.md § Generate the frontend; config.rs).
|
||
- mizan-generate's SSR bundling step compiles each SSR entry component together with `react-dom/server.browser` into a self-contained bundle assigning `globalThis.renderApp`, written to the `bundles` directory — the only place node/bun run in the SSR path (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `fetch.rs` spawns the configured source's export command (FastAPI `-m mizan_fastapi.ir`, Django `manage.py export_mizan_ir`, Rust `cargo run --bin`, or a generic script) and parses stdout as KDL — no OpenAPI/converter anywhere in the path — observable: a codegen run against a live FastAPI backend consumes only the KDL the CLI writes; the Rust source runs the cargo bin and the optional decoru pre-step first.
|
||
- the KDL parser reconstructs the full typed IR (types with struct/list/enum/alias shapes, functions with input/output/nullable/context/affects/merge/form, contexts with param elevation, channels) — observable: `ir_deserialization.rs` reads the AFI fixture back into typed structs and asserts the function set, per-function fields, param elevation, and named-type presence.
|
||
- the channel half of the parser reads the `params` / `client-message` / `server-message` slots in that order and the emitter renders them as `paramsType` / `clientMessageType` / `serverMessageType` — observable: a KDL `channel` node carrying only `server-message` yields a channel view whose client slot is absent, and the emitted `use{Pascal}Channel` hook types its outbound message as `never`.
|
||
- each target emits deterministically and is byte-parity-tested against a committed baseline — observable: `stage1_parity.rs`, `react_parity.rs`, `rust_parity.rs`, `python_parity.rs`, `vue_svelte_parity.rs`, and `channels_smoke.rs` diff emitter output against baselines and fail on any byte drift; two different runs produce identical output.
|
||
- the emitters produce genuinely different, correct artifacts per target — not one shape behind distinct labels — observable: the react target emits `<MizanContext>` + per-context providers + `use{Hook}()` reading React context (Stage 2), Stage 1 emits the framework-agnostic typed `callXxx`/`fetchXxx`; vue emits composables; svelte emits stores; python emits a Pydantic-typed facade over the PyO3 kernel; rust emits a full crate depending on `mizan-rust` — each byte-checked against its own baseline, and stage1 is auto-included whenever a framework target is requested.
|
||
- the codegen tree-shakes and canonicalizes types to match the backend emitters, and hoists inline enums into named Rust/TS types — observable: an unreferenced type is not emitted; an inline `field { enum … }` becomes a top-level Rust enum the struct field references; the channels target emits zero files when the IR carries no channels.
|
||
- an SSR bundling step compiles each SSR entry component together with `react-dom/server.browser` into a self-contained bundle that assigns `globalThis.renderApp`, emitted into the configured `bundles` directory — the only place node/bun run in the SSR path — observable: a `mizan-generate` run produces one render bundle per SSR entry component, each evaluable standalone by the embedded-V8 engine (assigning `renderApp` at eval time and rendering from a JSON-parsed props argument).
|
||
|
||
---
|
||
|
||
## Unit: AFI conformance (`tests/afi`)
|
||
|
||
**Charter.** The cross-adapter conformance gate: one fixture registered identically in Django, FastAPI, and a Rust app, asserting all three emit byte-identical KDL. It is the executable form of "the IR is the only contract"; it authors no production mechanism.
|
||
|
||
**Claims substantiated here.**
|
||
- Adapter parity is gated by the AFI conformance suite asserting IR-shape parity — the same fixture through Django, FastAPI, and Rust emits byte-identical KDL (README.md § Conformance; docs/AFI_ARCHITECTURE.md § KDL is the IR — "divergence between adapters is what the IR exists to prevent").
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- one shared fixture (`fixture.py` and its Rust twin `rust_app`) registers the same 7 functions / 5 types / context+affects+merge graph across all three backends, and the parity test diffs the three KDL emissions requiring exact three-way equality — observable: `test_codegen_parity.py` fails (naming the divergent pair) the instant any adapter's type introspection, ordering, or param elevation drifts; the fixture spans the AFI axes (plain fn, no-input fn, shared-param context, affects mutation, optional return, merge mutation) so the gate is not a degenerate single-shape check.
|
||
- the fixture spans the channel axis as well as the function axes, so the client-named slots are gated by the same three-way equality — observable: a channel declared once per backend emits an identical `channel` node with identical `<Pascal>Params` / `<Pascal>ClientMessage` / `<Pascal>ServerMessage` refs, so a backend-shaped slot name cannot re-enter the IR through one adapter without turning this gate red.
|
||
|
||
---
|
||
|
||
## Unit: wire-parity drivers (`tests/rust`, `tests/rust/fixture_client`)
|
||
|
||
**Charter.** The runtime wire-contract gate: Rust drivers (`drive_kernel`, `drive_emitted`) that hit a live FastAPI fixture and a live Rust/Axum fixture and assert the same JSON shapes and invalidate/merge semantics, plus the codegen-emitted `fixture_client` crate they exercise. It proves the runtime wire equivalence the static IR parity does not, and authors no production mechanism.
|
||
|
||
**Claims substantiated here.**
|
||
- The Rust adapters honor the same wire contract as FastAPI beyond static IR equivalence — same JSON shapes, same invalidate/merge semantics (README.md § Adapters; the "IR prevents divergence" claim taken to the runtime).
|
||
- The codegen-emitted typed client round-trips cleanly through the kernel (protocol/mizan-codegen rust target).
|
||
|
||
**Owed behavioral mechanisms.**
|
||
- `run_wire_parity.py` boots each backend, probes the readiness surface `/api/mizan/session/` (Mizan-protocol-shaped, so the harness reads the same surface across backends), then runs both the raw-kernel and emitted-typed drivers against each, propagating any non-zero exit — observable: the drivers hit every fixture endpoint (plain functions, the two-function context, the optional-return path, the merge mutation) against both FastAPI and Rust/Axum and require the same responses; a wire drift on either backend turns the harness red.
|
||
- `drive_emitted` exercises the codegen-emitted `fixture_client` typed functions (`call_echo`, `fetch_user_context`, `call_update_profile`, the optional `call_find_user`, the merge `call_rename_user`) so the generated crate is proven to round-trip, not merely to compile — observable: `call_find_user(99999)` returns `None`, `fetch_user_context(5)` returns the bundled `user_profile`+`user_orders`, and any deserialization mismatch fails the driver.
|