Commit Graph

17 Commits

Author SHA1 Message Date
6c5f6f1fba AFI parity: close all 35 gaps — every adapter wires every AFI-common capability
The conformance board (tests/afi/test_capability_parity.py) is now fully green:
90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The
gaps the prose table used to launder as "Django-only" / "out of scope" are
wired, against the pinned-spec model (single-authored spec, byte-identical
conformance across languages) — never per-language reimplementation.

FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest),
WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic
SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes
(SQLAlchemy projection, same declaration surface as django-readers), Forms
(Pydantic schema/validate/submit).

Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth=
enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC
subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT
mint+verify and cache-key derivation byte-pinned to the Python reference
(cache_keys_pin, token_pin, invalidate_header_pin).

TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS
backend can feed the codegen — the largest gap), multipart upload, session-init,
WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms.

Verified in the merged tree: core 25, fastapi 74, django 353/21-skip,
mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:44:35 -04:00
58d2cb2848 AFI parity: generate the matrix from conformance probes, not prose
The per-adapter parity table was hand-maintained prose. An adapter that
never wired a capability (FastAPI SSR, Axum WebSocket) got its gap
relabelled "Django-only" or "out of scope — use native equivalents," and
nothing went red. The de-scope was crystallized in five mutually-ratifying
sites: the README §Stack-extensions table, the AFI fixture docstring
("channels/forms/shapes aren't AFI-common"), the core registry's
extension-hook framing, the mizan-fastapi __init__ docstring, and a
"CSRF is Django-only" comment in two adapters' session endpoints.

Replace prose-parity with conformance-generated parity:

- tests/afi/manifest.py declares the AFI-common surface as data — one list
  of capabilities, one of adapters. Applicability ("—") is derived from
  transport, never typed.
- tests/afi/probes.py independently inspects each backend's source for the
  artifact a capability requires (comment-stripped, backend-scoped). Green
  means wired; a cell can't be set by editing a word.
- tests/afi/test_capability_parity.py asserts every (capability × applicable
  adapter) pair is wired. 35 unwired gaps are now loud red TFDD tests, each
  naming an owed binding. No xfail/skip.
- tests/afi/parity_table.py generates the README table from the probes;
  `make parity-check` fails CI on any hand-edit, like the codegen byte-parity.

Purge the five de-scope sites. The IR byte-parity gate is unchanged and green.
`make test-afi` is now intentionally red on the 35 gaps — that board is the
owed parity work, itemized; a gap turns green by being wired, never described.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:58:03 -04:00
66b2db81fb FastAPI and TypeScript improved 2026-06-04 05:14:29 -04:00
67ad91b673 Added file upload support 2026-06-04 04:20:05 -04:00
4effcc7597 Added LICENSE 2026-06-04 03:38:26 -04:00
ffdf9aa24d Cleaned dead code and updated documents 2026-06-04 02:42:13 -04:00
22dcf0e3c1 mizan-tauri + Pydantic-aware codegen: Tauri-as-Mizan-backend substrate
Tauri now joins FastAPI/Django/axum as a first-class Mizan backend. The
React frontend calls Mizan-registered functions through Tauri's IPC
with the same {result, invalidate, merge} envelope the HTTP path uses;
the schema flows Pydantic → decoru → Rust → KDL → TS in one
mizan-generate invocation.

New packages:
* backends/mizan-tauri — Tauri plugin exposing a single `mizan_invoke`
  command that routes through mizan-core's FUNCTIONS / CONTEXTS
  registries. No per-function tauri::command; the linkme slice IS the
  dispatch table.
* frontends/mizan-tauri-transport — TS package exporting
  tauriTransport() that wraps invoke('plugin:mizan|mizan_invoke', ...)
  and re-shapes errors into MizanError. Pairs with mizan-tauri.

@mizan/base — pluggable transport:
* Adds MizanTransport interface + transport config field.
* Existing fetch-based body factored into httpTransport() (default).
* mizanCall/mizanFetch delegate to config.transport; merge/invalidate
  side-effects stay in the kernel (transport-agnostic).
* Consumers swap via configure({ transport: tauriTransport() }).

mizan-codegen — Rust source + Pydantic pre-step:
* [source.rust] runs a Cargo bin (cargo run --bin <name>) and parses
  KDL from stdout. The bin uses mizan_core::build_ir() after
  force-linking the consumer's #[derive(Mizan)] / #[mizan::client]
  registrations.
* [source.rust.pydantic] is an optional pre-step that pipes an
  embedded Python bridge (scripts/run_decoru.py) to python and writes
  decoru-emitted Rust types into the consumer crate. The bridge
  auto-discovers BaseModel subclasses AND Enum subclasses
  (last-variant-is-default convention so decoru's impl Default keeps
  compiling against enum-typed fields without explicit Pydantic
  defaults).
* Pure-Rust usage stays intact — omit pydantic block and write Rust
  types by hand.

mizan-macros:
* #[mizan::client] now supports Result<T, MizanError> returns. The
  dispatch wrapper `?`-unwraps the user fn so server-side errors
  surface as the protocol's standard {code, message, details?}
  envelope; T-returning functions stay unchanged.
* #[derive(Mizan)] strips the r# raw-identifier prefix and honors
  field-level #[serde(rename = "...")] when emitting IR field names.
  Matches serde's wire shape — fixes IR-vs-JSON drift for Rust-keyword
  fields (e.g. `r#type` → `type`).

react.tsx template:
* Conditionally emits context-related imports / useContextSubscription
  helper based on has_global || !named_contexts.is_empty(). Consumers
  without contexts (mutation/RPC-only apps like claude-manage) no
  longer get dead imports that trip noUnusedLocals.

Verified end-to-end: cargo build clean across mizan-tauri,
mizan-codegen, AFI rust_app; AFI three-way KDL parity tests pass;
claude-manage migration drives the full stack (Pydantic schema →
generated TS api → Tauri-IPC transport → mizan-core dispatch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:01:45 -04:00
54f060c273 mizan: IR inline-substitution + reachability tree-shake + serde rename_all
Three substrate moves required by the Blazr-session port that surfaced
real cross-backend divergences:

1. Inline-substitution for primitive aliases and string enums. Named
   types whose body is `Alias(Primitive(_))` or `Enum(_)` collapse into
   their inline TypeShape at every `Ref` use site, and don't emit as
   their own `type "X" { ... }` entry. Matches Python's Pydantic Literal
   and `Foo = str` alias inlining — codegen consumers see the primitive
   directly rather than chasing a single-hop indirection.

2. Reachability tree-shake on the type registry. `#[derive(Mizan)]` now
   auto-registers every Mizan type into the TYPES slice; the emitter
   then transitively walks Refs from function inputs/outputs and emits
   only the reachable subset. Original-named entries from derive
   register only when something refs them; canonical-renamed entries
   from the function macro are reachable by definition. Mirrors
   Python's `_collect_named_types`.

3. `#[serde(rename_all = "...")]` + `#[serde(rename = "...")]`
   propagation in `#[derive(Mizan)]` for enums. IR enum variants now
   match the on-wire JSON casing (lowercase / snake_case / kebab-case /
   etc.), not the Rust variant idents. Supports all serde casings.

AFI codegen + wire parity stays green after these changes (the AFI
fixture's enum-free + Pydantic-shape types are unchanged by the three
substrate extensions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:01:28 -04:00
a1d1d6928f mizan-axum + macros: state threading, array/map lowering, merge shape semantics
Three substrate extensions surfaced by the Blazr session port:

1. **App-state threading.** mizan-axum::router() is now generic over a
   user-supplied state type and threads `Arc<dyn Any + Send + Sync>` into
   every dispatch via RequestHandle. Handlers downcast to their concrete
   AppState. The stateless AFI fixture uses `router_stateless()` (matches
   the prior signature). RequestHandle gains a `from_dyn()` constructor
   to wrap already-erased trait-object references.

2. **`[T; N]` and `BTreeMap<K, V>` lowering in #[derive(Mizan)].** Fixed
   arrays emit as `List<T>` (matches Python `tuple[float,...]` → JSON
   array). String-keyed maps emit as `List<V>` — closest approximation
   until KDL grows a `dict` shape. Also: vec-element registrations get a
   per-function scope suffix so two handlers returning `Vec<Same>` don't
   collide at the static-name layer.

3. **`types_match` for merge: upsert-into-list semantics.** Now matches
   Python `types_match_for_merge`: direct (T == T), upsert (slot is
   `Alias(List(T))`, value is T), and list-replace (both sides list).
   The AFI fixture only exercised the direct path; the Blazr port's
   `morph_set_value` returning a single `MorphLayer` into a context with
   `Vec<MorphLayer>` slot is what surfaced the gap.

AFI codegen + wire parity stays 12/12 green after these substrate changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 23:40:33 -04:00
45bde51166 Mizan-Rust backend adapter: server-side substrate + three-way parity
Adds first-class Rust-backed Mizan to sit alongside mizan-django and
mizan-fastapi. A Rust dev writes:

    #[derive(Mizan, Serialize, Deserialize)]
    pub struct ProfileOutput { pub user_id: i64, pub name: String }

    #[mizan::context("user")]
    pub struct UserCtx;

    #[mizan::client(context = UserCtx)]
    pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { ... }

…and gets byte-identical KDL to the Python emitters, served over the
same wire protocol the React / Rust / Vue / Svelte kernels speak.

New crates:
- cores/mizan-rust/         (Cargo: mizan-core)     — IR types, KDL emitter, traits, registry,
                                                       runtime (compute_invalidation / compute_merges
                                                       ported from mizan-fastapi), graph_check with
                                                       structural type-matching
- cores/mizan-rust-macros/  (Cargo: mizan-macros)   — #[derive(Mizan)], #[mizan::context],
                                                       #[mizan::client] proc macros
- backends/mizan-rust-axum/ (Cargo: mizan-axum)     — axum HTTP adapter: /session/, /call/, /ctx/:name/
- tests/afi/rust_app/                                — AFI fixture port + server / export-ir binaries

Substrate-shape moves required by cross-language equivalence:
- IR canonicalization: functions / contexts / context-members / shared-by
  now sort alphabetically in both Python and Rust emitters. The IR is a
  contract; linkme doesn't preserve declaration order, so canonical sort
  is the only stable mapping. afi_ir.kdl + per-target baselines regenerated.
- MizanType::TYPE_NAME is a const (with a default type_name() reader) so
  it's usable in linkme TypeEntry static initializers.
- Tree-shaken type registry: #[derive(Mizan)] only emits the trait impl;
  the #[mizan::client] macro registers canonical-named entries from
  fn signatures, including Vec<T> element types for ref resolution.
- Merge resolution is structural (NamedType shape comparison) rather than
  by name — matches the Python types_match_for_merge semantics.

Three-way forcing functions:
- tests/afi/test_codegen_parity.py — Django ≡ FastAPI ≡ Rust on KDL bytes (3 pass)
- tests/rust/run_wire_parity.py    — 12/12 probes against FastAPI + Rust (EXIT=0)

Incidental fixes surfaced by the new tests:
- Stale `from .registry import validate_registry` import removed from
  mizan-django/setup/discovery.py (referenced a function that no longer
  exists; was masking codegen-parity).
- BASE_DIR added to tests/afi/django_app/project/settings.py.
- /session/ endpoint added to mizan-fastapi for protocol-shaped readiness
  probe parity (wire-parity harness now polls /api/mizan/session/ on both
  backends rather than FastAPI's /openapi.json).
- Root .gitignore picks up Rust target/ across the tree so new crates
  don't need per-crate gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:31:26 -04:00
9900f8a36f Mizan IR: cut over to KDL, delete OpenAPI envelope
Replaces the transitional OpenAPI 3.0 + `x-mizan-*` extensions
substrate with the canonical Mizan IR as KDL, per docs/AFI_ARCHITECTURE.md:
"KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it."

End-to-end cutover. No transitional path left on main.

Forward direction:
  cores/mizan-python/src/mizan_core/ir.py
    build_ir() walks mizan_core.registry, introspects Pydantic
    models directly (no JSON-Schema indirection), and emits the
    Mizan IR document. The KDL grammar is locked in this file's
    module docstring.

Backends emit KDL:
  backends/mizan-fastapi/src/mizan_fastapi/ir.py
    `python -m mizan_fastapi.ir <module>` — CLI entry point.
  backends/mizan-django/.../management/commands/export_mizan_ir.py
    `manage.py export_mizan_ir` — Django mgmt command.

Codegen consumes KDL:
  protocol/mizan-codegen/Cargo.toml: + kdl = "6"
  protocol/mizan-codegen/src/ir.rs: NamedType { Struct/List/Enum/Alias }
    + TypeShape { Primitive/Ref/List/Optional/Enum/Union } sum types,
    replacing the JsonSchema sprawl. KDL parser walks the
    `kdl::KdlDocument` tree into typed Rust structs.
  protocol/mizan-codegen/src/fetch.rs: subprocess command switches
    to the new IR-export entry points.
  All emit modules (stage1 / react / python / rust / vue / svelte /
    channels) port their type-walkers from JsonSchema to the new
    sum types — case analysis collapses substantially.

Substrate-honesty wins beyond the moat closure:
  - `int | bool` multi-arm unions land as `TypeShape::Union` (was
    silently coerced to "string" before).
  - `<CamelName>Output = list[T]` returns emit as named alias
    types instead of struct-shaped wrappers, so consumer code
    `.map()` works directly on the type.
  - Pydantic field defaults flow through to `default` properties
    in KDL, then back to non-optional shape in every target.

Deleted:
  - backends/mizan-fastapi/src/mizan_fastapi/{cli,schema}.py
  - backends/mizan-django/.../export_mizan_schema.py
  - openapi-bearing half of mizan/export/__init__.py (edge
    manifest generator preserved — separate concern).
  - tests/afi/schema_normalizer.py
  - tests/fixtures/{afi_schema.json, channels_schema.json}
  - tests/fixtures/js_* baseline directories.

Verification:
  - 20 mizan-codegen unit tests green (IR deserialization,
    byte-equivalence parity across stage1/rust/python/react/vue/svelte
    against fresh KDL-driven baselines, channels structural).
  - tests/rust/run_wire_parity.py: 12/12 probes green driving
    the binary end-to-end through KDL.
  - Blazr studio-ui typechecks against the regenerated React client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:14:47 -04:00
7fb0c4a400 Mutation→context merge primitive across the stack
The @client(merge=[context, ...]) decorator lets a mutation patch its
return value directly into the cached context bundle by matching the
mutation's Output type against each context-function's Output type
to identify the slot, then splicing server-side. Kernel runs
splice_slot on the response to apply locally — no refetch, no
invalidate-cascade.

Lands H14, H15, H16, M19, M20 from ISSUES.md.

Backends (Django + FastAPI):
  _resolve_merges() in both executors walks @client(merge=...) targets,
  resolves the per-context slot via types_match_for_merge, and emits
  {context, slot, value, params?} entries on the response. Param
  auto-scoping mirrors _resolve_invalidation's tier-1 logic.

Frontend kernel (mizan-base):
  Response handler reads the merge[] array and applies splice_slot
  for each entry — locates the cached context bundle by name+params,
  overwrites the named slot with the new value, notifies subscribers.

Core (mizan-python):
  @client decorator extended with merge= parameter. Schema export
  threads merge metadata onto the OpenAPI x-mizan-functions entries.

Examples / fixtures:
  fastapi-react-site harness exercises merge + Playwright spec covers
  the end-to-end happy path (mutation → instant UI update without
  network refetch). AFI fixture's rename_user function is the
  canonical merge target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:29:06 -04:00
43bcf3f26f Mizan codegen substrate: Rust kernel + Rust codegen binary, JS generator deleted
The Mizan codegen substrate moves off JavaScript template-literal emission
onto a compiled Rust binary that consumes the same OpenAPI + x-mizan-* IR
the JS substrate consumed. Three structural wins fall out of one move:

1. Moat closes. The codegen logic (how `affects` becomes auto-invalidation,
   how named contexts collapse onto bundled fetches, how the registry-to-
   Provider mapping is shaped) ships compiled instead of as source bytes
   in every consumer's node_modules.

2. Pattern F (lines.push append-walls) becomes structurally unauthorable.
   The emit substrate is askama templates in templates/<target>/*.j2 —
   actual target-language files with {{ ... }} substitution markers,
   syntax-highlighted natively, type-checked against the render context
   structs at compile time. The Rust emit modules build typed render
   contexts and call .render(); no string-builder surface exists.

3. OpenAPI `default`-bearing fields now emit as non-optional in TS / Python
   / Rust — the server always populates them, so consumer code reads them
   without nullable checks. Surfaced by Blazr's typecheck on regeneration.

Layout:
  frontends/mizan-rust/        — Rust port of @mizan/base; #[cfg(feature="pyo3")]
                                 exposes PyMizanClient for the Python target.
  protocol/mizan-codegen/      — codegen binary source + askama templates.
  protocol/mizan-generate/     — npm-package shim. bin/launcher.mjs dispatches
                                 to the platform-appropriate prebuilt binary.
                                 Old generator/ JS tree deleted.
  tests/rust/                  — wire-parity drivers. drive_kernel exercises
                                 raw client.call() / fetch_context(); drive_emitted
                                 exercises the typed crate the codegen emits.
  tests/afi/afi_codegen_app.py — codegen entrypoint module (imports + registers).
  backends/mizan-fastapi/.../schema.py — adds outputNullable so the Rust
                                 codegen can wrap T | None responses in Option<T>.

Verification:
  - 20 mizan-codegen tests green (IR deserialization, byte-equivalent
    parity vs JS baseline for stage1/rust/python/react/vue/svelte,
    structural test for channels).
  - tests/rust/run_wire_parity.py — 12/12 probes green via the Rust binary
    driving the FastAPI fixture end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:26:32 -04:00
dd41f0c25f Extract client/function.py to mizan-core (Tier B)
The @client decorator + ServerFunction base + composition machinery is
mostly framework-agnostic. The only Django couplings were typing
(HttpRequest in __init__ and submit_handler signatures) and runtime
view-path detection (HttpResponseBase isinstance/issubclass checks).

Replaced both with backend-extension hooks:

- HttpRequest type hints → Any. Type Protocol can be tightened later.
- HttpResponseBase view-path detection → set_framework_response_base(cls)
  hook in mizan_core.client.function. Backends register their framework's
  response base at import time. is_framework_response(obj_or_cls) handles
  both instance and subclass checks via the registered base.

mizan-django registers HttpResponseBase via mizan/client/__init__.py
before any @client-decorated code is loaded. FastAPI would similarly
register starlette.responses.Response.

Direct consumers updated:
- mizan/setup/discovery.py: ServerFunction import path
- mizan/forms/__init__.py: ServerFunction + create_form_functions imports

mizan/client/__init__.py keeps its public re-export surface stable so
'from mizan.client import client, ServerFunction, …' continues to work
for downstream Django consumers.

Verified:
- mizan-core: 15/15
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-ts edge-compat: 34/34

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:39:48 -04:00
76fce2dc85 Split the registry — function/composition core + backend extensions
The original registry tangled function, channel, composition, and form
registration in a single file with polymorphic register() dispatch.
That predates the household discipline; it was the design that was
supposed to ship but didn't. Re-implementing the original intent.

cores/mizan-python/src/mizan_core/registry.py (new):
- _functions, _compositions dicts
- register() — ServerFunction-only, no polymorphic dispatch
- register_as(), register_compose()
- register_extension(name, extension) — hook interface
- get_function/get_compose/get_all_functions/get_all_compositions
- get_contexts, get_context_groups
- get_registry, get_schema — aggregate extension contributions
- validate_registry, clear_registry — cascade-clear extensions

RegistryExtension Protocol:
- schema() returns the extension's schema subdict (keyed under its name)
- clear() resets extension state (called by clear_registry)

mizan-django/src/mizan/channels/__init__.py:
- _ChannelsExtension wraps the channel _registry, plugs into core via
  register_extension('channels', ...). Schema output preserves the
  same shape codegen consumed before (snake_case keys, type+bidirectional).

mizan-django/src/mizan/forms/__init__.py:
- register_form() and get_forms() helpers moved here (were in setup/registry.py)
- Both use mizan_core.registry under the hood. Forms don't need a
  separate extension because form sub-functions register as regular
  ServerFunctions with meta.form set.

mizan-django/src/mizan/setup/registry.py: deleted.
mizan-django/src/mizan/setup/__init__.py: re-exports the registry helpers
from mizan_core.registry / mizan.channels / mizan.forms — the Django
adapter's curated public API surface stays stable for users.

Consumers updated: ~10 files imported `from mizan.setup.registry`;
all switched to direct imports from mizan_core.registry, mizan.channels,
or mizan.forms as appropriate. ChannelTests in test_core.py rewritten
to use mizan.channels.register directly (no more polymorphic
@register_as on ReactChannel subclasses).

Verified:
- mizan-core: 15/15
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-ts edge-compat: 34/34 (cross-language pin holds)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:21:16 -04:00
9150cdc5ee Extract cache/backend.py to mizan-core (Tier A)
cache/backend.py is pure framework-agnostic key-value abstraction —
CacheBackend Protocol, MemoryCache, RedisCache. No Django imports.
Moves to cores/mizan-python/src/mizan_core/cache/backend.py with no
content changes; mizan-django re-imports.

Verified: mizan-core 15/15, mizan-django 348 pass / 21 skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:23:01 -04:00
37e61c646b Extract Layer 1 to cores/mizan-python (mizan-core)
Pull cache/keys.py (HMAC cache key derivation) and mwt.py (Mizan Web Token)
out of backends/mizan-django and into a new cores/mizan-python package.
mizan-django re-imports them via the new mizan_core module.

Naming: directory cores/mizan-python/, distribution mizan-core, importable
module mizan_core. mizan-django keeps its existing 'mizan' distribution slot
on PyPI; the two coexist as distinct packages.

Wiring:
- backends/mizan-django/pyproject.toml gains a 'mizan-core' dep with a
  [tool.uv.sources] path entry (editable install from ../../cores/mizan-python).
- Makefile install target prepends 'cd cores/mizan-python && uv pip install -e .'
- 3 import sites in mizan-django updated: cache/__init__.py, jwt/functions.py,
  client/executor.py — all now import from mizan_core.

Test split:
- 3 unit-test classes (CacheKeyDerivationTests, MWTCreationTests,
  PermissionKeyTests) move to cores/mizan-python/tests/, rewritten against
  unittest.TestCase (no Django dep). The cross-language pin test (pinned
  HMAC hex digests against mizan-ts) moves with CacheKeyDerivationTests.
- Integration tests stay in mizan-django (CacheBackendTests, CachePurgeTests,
  CacheIntegrationTests, RevParameterTests, MWTAuthIntegrationTests) — they
  need the Django request flow.

Verified:
- mizan-core: 15/15 pass (incl. cross-language pin)
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-ts: edge-compat 34/34 pass — protocol invariant holds, the moved
  Python derive_cache_key still produces the exact hex digests TS pins against.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:32:54 -04:00