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>
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>
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>
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>
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>
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>