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