Docs carry the shape of the system, not its status

MIZAN.md's phase-numbered implementation order, PRODUCT_ARCHITECTURE's
"Deferred until Render revenue funds it" and "Shipped in", PSR_VS_EDGE's
current-state section, and the READMEs' passing-test counts were all reporting
where the work stood rather than what the system is. OWED_SURFACE keeps its
subject — surface that is specified but unbuilt — stated as the shape each unit
owes.

The channel sections follow the renamed slots: Params / ClientMessage /
ServerMessage, and Channel as the base class on both backends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 14:03:29 -04:00
parent 3aafec6dd4
commit e00b3a177e
10 changed files with 243 additions and 213 deletions

View File

@@ -119,18 +119,24 @@ dedicated `mizan-allauth` repository, built on this mixin.
## Channels
WebSocket-native RPC via a flag flip:
WebSocket-native RPC via a flag flip. The message slots are named from the
client's point of view: `ClientMessage` travels client → server,
`ServerMessage` travels server → client. Declare only the directions the
channel uses.
```python
from pydantic import BaseModel
from mizan.channels import ReactChannel
from mizan.channels import Channel
class ChatChannel(ReactChannel):
class ChatChannel(Channel):
class Params(BaseModel):
room: str
class DjangoMessage(BaseModel):
class ClientMessage(BaseModel):
text: str
class ServerMessage(BaseModel):
text: str
user: str
@@ -139,10 +145,19 @@ class ChatChannel(ReactChannel):
def group(self, params):
return f"chat_{params.room}"
def receive(self, params, msg):
return self.ServerMessage(text=msg.text, user=self.user.email)
```
Frontend gets `useChatChannel({ room })`.
Server code outside a subscription broadcasts with `push()`:
```python
await ChatChannel.push(room="general", message=ChatChannel.ServerMessage(...))
```
## Generate the frontend
The codegen is the `mizan-generate` Rust binary (source at

View File

@@ -5,11 +5,10 @@ function. Typed React client generated. Invalidation automatic.
## Scope
mizan-fastapi targets the **AFI-common subset** RPC dispatch, context
bundling, JSON-body invalidation, and auth gating. Forms, Channels, Shapes,
and SSR are out of scope for the FastAPI adapter — FastAPI projects use
native equivalents (Pydantic, native WebSockets, ORM-of-choice, FastAPI's
own SSR ecosystem).
mizan-fastapi's surface is RPC dispatch, context bundling, JSON-body
invalidation, auth gating, and channels over a multiplexed WebSocket. Forms,
Shapes, and SSR sit outside that surface — a FastAPI project reaches for its
own native equivalents (Pydantic, ORM-of-choice, FastAPI's SSR ecosystem).
## Install
@@ -29,11 +28,13 @@ from mizan_fastapi import (
mizan_exception_handler,
mizan_validation_handler,
router as mizan_router,
ws_router,
)
app = FastAPI()
app.include_router(mizan_router, prefix="/api/mizan")
app.include_router(ws_router, prefix="/api/mizan")
app.add_exception_handler(MizanError, mizan_exception_handler)
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
```
@@ -82,9 +83,56 @@ a dedicated `clients.py` imported during startup.
@client(rev=2) # cache revision (busts on bump)
```
`websocket=True`, Forms, and Channels parameters are accepted by the
decorator (they're a `mizan-core` primitive) but ignored by mizan-fastapi —
those features only have effect when paired with mizan-django.
Forms parameters are accepted by the decorator (they're a `mizan-core`
primitive) and carry no meaning to this adapter.
## Channels
A channel is a named fan-out over the WebSocket `ws_router` serves. Subclass
`Channel`, declare whichever payload models the channel carries, and register
it. The three model names are read from the client's side: `Params` keys the
fan-out, `ClientMessage` travels up, `ServerMessage` travels down.
```python
from mizan_fastapi import Channel, register_channel
from pydantic import BaseModel
class Chat(Channel):
class Params(BaseModel):
room: str
class ClientMessage(BaseModel):
text: str
class ServerMessage(BaseModel):
user: str
text: str
def authorize(self, params: Params) -> bool:
return True
def receive(self, params: Params, msg: ClientMessage) -> ServerMessage:
return self.ServerMessage(user="anon", text=msg.text)
register_channel(Chat, "chat")
```
Server code pushes to a group from anywhere:
```python
await Chat.push(Chat.ServerMessage(user="system", text="hello"), room="general")
```
Registered channels contribute to the exported IR, so codegen emits the
`<Pascal>Params` / `<Pascal>ClientMessage` / `<Pascal>ServerMessage` types and
the matching frontend hook.
Group membership lives in the process that holds the socket, so a push reaches
only the subscribers attached to that process. Fan-out that spans processes is
a shared broker in front of `broadcast`.
## Auth integration
@@ -169,8 +217,6 @@ python -m mizan_fastapi.ir <module>
Imports the named module (which must register every `@client` function as
import-time side effects), then prints the Mizan KDL IR to stdout.
Mirrors mizan-django's `manage.py export_mizan_ir` so the codegen consumes
either backend the same subprocess way.
## Architecture
@@ -183,4 +229,4 @@ emit equivalent schemas for the same registered functions. See
A live e2e harness exercises this adapter end-to-end at
`examples/fastapi-react-site/` (real Chromium → React with generated hooks
→ FastAPI server, 14/14 Playwright tests).
→ FastAPI server, driven by Playwright).

View File

@@ -215,11 +215,11 @@ const greeting = await callGreet({ name: "world" });
console.log(greeting.message);
```
For framework hooks generated by Stage 2 (`useGreet()` etc., wrapping the
imperative `callGreet` with `isPending`/`error` state), wrap your tree
with `<MizanContext>` at the root — same as the HTTP-transport setup. The
generated provider is transport-agnostic; it reads from `config.transport`
the kernel is using.
For the framework hooks the `react` target generates (`useGreet()` etc.,
wrapping the imperative `callGreet` with `isPending`/`error` state), wrap
your tree with `<MizanContext>` at the root — same as the HTTP-transport
setup. The generated provider is transport-agnostic; it reads from
`config.transport` the kernel is using.
### tsconfig / vite preserve symlinks
@@ -273,12 +273,6 @@ Errors flow through Tauri's `Promise.reject` path; `@mizan/tauri-transport`
re-wraps them into the same `MizanError` shape the HTTP transport
produces, so consumer code is identical regardless of transport.
## Reference application
`claude-manage` is the production reference — Tauri + React + Pydantic
schema + Mizan RPC. See `~/dev/claude-manage/mizan.toml` and
`~/dev/claude-manage/src-tauri/src/commands.rs` for a full migrated app.
## Architecture
mizan-tauri shares `cores/mizan-rust` with `mizan-rust-axum`. Both