A channel's message slots are named from the client, on every backend

The IR called them react-message and django-message, so a FastAPI channel had
to declare a DjangoMessage. They are client-message and server-message now,
and the direction words hold wherever a channel is declared: Params /
ClientMessage / ServerMessage, with mizan-core deriving <Pascal>Params and
friends so no backend names a type itself. Django's ReactChannel and
FastAPI's ReactChannel are both Channel.

mizan-fastapi never registered a channels extension, so build_ir() emitted no
channel at all and every payload type was invisible to codegen. It registers
one now. RegistryExtension is an ABC requiring all(), which is what the IR
reads — an extension that cannot enumerate its registrations no longer exists.

The gate that should have caught the rename could not: tests/afi registered no
channel because mizan-rust had no channel registry to register one in, so a
five-package rename of the wire contract passed byte-parity without a channel
byte crossing it. mizan-rust grows ChannelSlotKind, a CHANNELS slice, a
#[mizan::channel] macro, and KDL emission whose wire_to_pascal matches Python's
split; the AFI fixture now carries a channel with every slot and one with a
single slot, so all three backends prove the contract byte for byte.

MizanChannel held three Option<String> beside three has_*() predicates and
unwrapped them with defaults; it holds an ordered slot vector, so an absent
slot is absent rather than defaulted. The channels target emitted a React
hooks file that a stage1-only consumer could not compile — react emits that
now. The codegen's parity tests byte-compared emitted source against baselines
without ever compiling it: they compile the generated crate and run its tests,
import the generated Python package and call every method, and typecheck each
TypeScript target against a consumer.

Also fixed at source: app_visitor printed its import diagnostic to stdout, the
stream export_mizan_ir writes KDL to, so a failed import silently corrupted the
IR; the apps root was hardcoded to "apps"; _default_literal crashed build_ir on
any non-JSON-serializable field default; Django and mizan-core derived Pascal
names two different ways, disagreeing on every dotted channel name.

ir.py builds a document and renders templates/ir/document.kdl.j2 rather than
appending KDL strings with hand-tracked indentation, and named types resolve to
a fixed point — a model reachable only through a union branch was referenced by
a ref that no type block ever defined.

The rest is the write-gate's own classifiers run over the standing tree:
relative imports, silent swallows, Protocol contracts that should be ABCs,
emitters hand-rendering target source, catch-all arms over closed enums, and
comments narrating the project rather than the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 14:03:19 -04:00
parent 398c90fc8b
commit 3aafec6dd4
345 changed files with 11054 additions and 17359 deletions

View File

@@ -1,18 +1,9 @@
"""
Server functions and channels for integration tests.
Registers everything the React integration test suite expects:
- echo, add (HTTP + WebSocket RPC)
- login, signup, add_email forms
- chat, notifications, presence channels
"""
from django import forms
from django.http import HttpRequest
from pydantic import BaseModel
from mizan.client import ServerFunction, client
from mizan.channels import ReactChannel
from mizan.channels import Channel
from mizan.setup import register, register_form, register_as
from mizan.channels import register as register_channel
from mizan.forms import mizanFormMixin, mizanFormMeta
@@ -85,7 +76,7 @@ class LoginForm(forms.Form):
def handle_login(request, form):
"""Login form submit handler."""
"""Authenticate the credentials and open a session."""
from django.contrib.auth import authenticate, login
user = authenticate(
@@ -109,7 +100,7 @@ class SignupForm(forms.Form):
def handle_signup(request, form):
"""Signup form submit handler."""
"""Create the user, surfacing any creation failure as a form error."""
from django.contrib.auth import get_user_model
User = get_user_model()
@@ -118,11 +109,12 @@ def handle_signup(request, form):
email=form.cleaned_data["email"],
password=form.cleaned_data["password1"],
)
return {"success": True, "data": {"user_id": user.pk}}
except Exception as e:
form.add_error(None, str(e))
return None
return {"success": True, "data": {"user_id": user.pk}}
register_form(SignupForm, "signup", submit_handler=handle_signup)
@@ -139,36 +131,35 @@ register_form(AddEmailForm, "add_email")
# =============================================================================
class ChatChannel(ReactChannel):
class ChatChannel(Channel):
class Params(BaseModel):
room: str
class ReactMessage(BaseModel):
class ClientMessage(BaseModel):
text: str
class DjangoMessage(BaseModel):
class ServerMessage(BaseModel):
text: str
def authorize(self, params=None):
return True
return bool(params and params.room)
def group(self, params=None):
room = params.room if params else "default"
return f"chat_{room}"
return f"chat_{params.room}"
def receive(self, params, msg):
return self.DjangoMessage(text=msg.text)
return self.ServerMessage(text=msg.text)
register_channel(ChatChannel, "chat")
class NotificationsChannel(ReactChannel):
class DjangoMessage(BaseModel):
class NotificationsChannel(Channel):
class ServerMessage(BaseModel):
text: str
def authorize(self, params=None):
return True
return self.user.is_authenticated
def group(self, params=None):
return "notifications_global"
@@ -177,12 +168,12 @@ class NotificationsChannel(ReactChannel):
register_channel(NotificationsChannel, "notifications")
class PresenceChannel(ReactChannel):
class DjangoMessage(BaseModel):
class PresenceChannel(Channel):
class ServerMessage(BaseModel):
value: int
def authorize(self, params=None):
return True
return self.user.is_authenticated
def group(self, params=None):
return "presence_global"
@@ -290,24 +281,34 @@ class Multiply(ServerFunction):
# =============================================================================
# Error-producing Functions
# Functions carrying error branches
# =============================================================================
@client
def not_implemented_fn(request: HttpRequest) -> EchoOutput:
raise NotImplementedError("This feature is not yet implemented")
register(not_implemented_fn, "not_implemented_fn")
_ECHO_TRANSFORMS = {"upper": str.upper, "lower": str.lower}
@client
def buggy_fn(request: HttpRequest) -> EchoOutput:
raise RuntimeError("Unexpected internal failure")
def echo_transform(request: HttpRequest, text: str, mode: str) -> EchoOutput:
transform = _ECHO_TRANSFORMS.get(mode)
if transform is None:
raise NotImplementedError(f"echo mode {mode!r} has no transform")
return EchoOutput(message=transform(text))
register(buggy_fn, "buggy_fn")
register(echo_transform, "echo_transform")
class DivideOutput(BaseModel):
quotient: float
@client
def divide(request: HttpRequest, numerator: int, denominator: int) -> DivideOutput:
return DivideOutput(quotient=numerator / denominator)
register(divide, "divide")
@client
@@ -384,16 +385,16 @@ class ItemForm(mizanFormMixin, forms.Form):
# =============================================================================
# Auth-gated Channel
# Staff-gated Channel
# =============================================================================
class PrivateChannel(ReactChannel):
class DjangoMessage(BaseModel):
class PrivateChannel(Channel):
class ServerMessage(BaseModel):
text: str
def authorize(self, params=None):
return getattr(self.user, "is_authenticated", False)
return self.user.is_staff
def group(self, params=None):
return "private_global"

View File

@@ -1,14 +1,11 @@
"""
Django settings for the integration test backend.
Provides:
- HTTP server functions (echo, add)
- WebSocket channels (chat, notifications, presence)
- JWT authentication
- Form integration (login, signup, add_email)
"""
"""Django settings for the integration-test backend the E2E harness talks to."""
import os
from pathlib import Path
# The directory holding manage.py and the app packages; mizan's client
# discovery walks the installed apps beneath it.
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "integration-test-secret-key-not-for-production"
@@ -41,7 +38,7 @@ TEMPLATES = [
"OPTIONS": {
"worker": os.path.join(
os.path.dirname(__file__), "..", "..", "..", "..",
"packages", "mizan-ssr", "src", "worker.tsx",
"workers", "mizan-ssr", "src", "worker.tsx",
),
},
},