Reworked the MVP code along the lines Ryth flagged. Same behavior
(11/11 tests still pass), tighter idiom.
executor.py:
- Replaced FunctionResult / FunctionError dataclasses with a MizanError
exception hierarchy (NotFound, BadRequest, ValidationFailed,
Unauthorized, Forbidden, NotImplementedYet, InternalError). Each
carries its own ErrorCode + HTTP status; the dispatcher path raises
rather than returning sentinel objects.
- Auth check uses match/case for the requirement (True / 'staff' /
'superuser' / callable / other) — single declarative dispatch instead
of an if/elif chain.
- Broke up the single 80-line execute_function into focused helpers:
_resolve_function, _enforce_auth, _validate_input, _serialize,
_invalidation_target. The execute_function body now reads as five
declarative steps.
- Input validation uses Pydantic's model_fields[name].is_required()
directly and a list comprehension for required-field reporting,
instead of round-tripping through model_json_schema().
router.py:
- POST /call/ now declares its body as a Pydantic CallBody model;
FastAPI handles parsing + envelope validation. No more manual
await request.json() + dict[get] dancing.
- Endpoint bodies shrink to 3-5 lines each. Context fetch uses a
dict comprehension over the function group.
- mizan_exception_handler renders MizanError to the protocol's
{error: {code, message, details}} envelope.
- mizan_validation_handler maps FastAPI's RequestValidationError to
the same envelope under BAD_REQUEST so the wire format is uniform
whether the failure is body-shape or business validation.
__init__.py: exposes the full exception hierarchy + both handlers
so consumers can wire them onto their FastAPI app declaratively:
app.add_exception_handler(MizanError, mizan_exception_handler)
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
Verified: mizan-core 15/15, mizan-django 348 pass, mizan-fastapi 11/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Blazr-critical surface for FastAPI. Forms, Channels, Shapes, SSR,
and MWT are out of scope (Ryth's call: defer until Blazr exercises
them; FastAPI projects use native equivalents anyway).
What ships:
- POST /api/mizan/call/ RPC dispatch with Pydantic input validation
- GET /api/mizan/ctx/{name}/ bundled context fetch (all functions in
the named context, parallel-evaluated, single
JSON response)
- JSON-body invalidation transport (the 'invalidate' field on mutation
responses, with auto-scoping when mutation arg names match context params)
- Auth check infrastructure expecting request.state.user populated by
FastAPI middleware/deps (matches FastAPI idioms)
- Cache-Control: no-store on all responses
Built on existing mizan-core: registry (function lookup, context groups,
invalidation metadata), client.function (the @client decorator + ServerFunction
+ _FunctionWrapper). No code copied or duplicated from mizan-django — the
shared substrate is genuinely shared.
Package layout:
backends/mizan-fastapi/
pyproject.toml distribution=mizan-fastapi, module=mizan_fastapi
src/mizan_fastapi/
executor.py dispatch + auth + invalidation
router.py FastAPI APIRouter with the two endpoints
tests/test_dispatch.py 11 e2e tests against TestClient
Test fixture establishes the registration pattern: explicit
register(fn_class, "name") after each @client. mizan-fastapi doesn't
ship discovery — apps register their functions explicitly. (mizan-django
keeps its DjangoAppVisitor discovery; FastAPI's lack of an app system
makes auto-discovery less natural.)
Makefile: install + test targets now include mizan-fastapi alongside
the other packages. New test-core / test-fastapi targets added for
symmetry.
Verified:
- mizan-core: 15/15
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-fastapi: 11/11
- mizan-ts edge-compat: 34/34
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>
The fe39fcb commit captured the file moves (git mv stages those automatically)
but didn't catch the content edits I made afterward — npm package rename
(@mizan/runtime → @mizan/base), path updates in Makefile/Dockerfile/examples,
and doc updates were all left unstaged at commit time.
This commit lands those:
- npm rename: 3 frontend package.jsons (base/vue/svelte) + mizan-base/src/index.ts + 4 codegen templates
- path updates: Makefile, Dockerfile.test, two Gitea workflows, four example/harness configs
- doc updates: CLAUDE.md, ROADMAP.md, ISSUES.md, docs/AFI_ARCHITECTURE.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
packages/ flattens into:
backends/ server protocol adapters (mizan-django, mizan-ts)
frontends/ client kernel + framework adapters (mizan-base, mizan-react, mizan-vue, mizan-svelte)
workers/ runtime workers (mizan-ssr)
cores/ shared language-level primitives (empty for now; mizan-python forthcoming)
The frontend kernel (was packages/mizan-runtime, now frontends/mizan-base) is
renamed to reflect its role — it's the shared base that frontend adapters
depend on directly. Reflects the substrate position that per-framework adapters
wrap a single shared kernel; codegen targets the adapter, not the raw kernel.
Path updates landed in: Makefile, two Gitea workflows, Dockerfile.test, four
example/harness config files, .claude/settings.local.json, four docs
(CLAUDE/ISSUES/ROADMAP/AFI_ARCHITECTURE), four codegen templates (stage1 +
react/vue/svelte adapters), and three package.jsons (the mizan-base rename
plus mizan-vue/svelte peerDeps).
Generated files under examples/django-react-site/harness/src/api/ still
reference @mizan/runtime — left as-is; they're regenerated artifacts and
the harness is non-functional pending the React wrapper-layer codegen.
Also folded in a pre-existing fix: the Gitea workflows had
working-directory: react / django pointing at a layout that predates
packages/, never updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>