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>
This commit is contained in:
2026-05-17 18:26:32 -04:00
parent c15c6f3e14
commit 43bcf3f26f
114 changed files with 11090 additions and 2342 deletions

View File

@@ -17,9 +17,10 @@ from typing import Any
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from pydantic import BaseModel, create_model
from pydantic import BaseModel, RootModel, create_model
from mizan_core.registry import get_all_functions, get_context_groups, get_function
from mizan_core.type_utils import extract_list_element, extract_optional
__all__ = ["build_schema", "snake_to_camel"]
@@ -62,12 +63,20 @@ def _function_metadata(name: str, fn_class: Any) -> dict[str, Any]:
input_cls = getattr(fn_class, "Input", None)
has_input = _has_input(input_cls)
output_cls = getattr(fn_class, "Output", None)
_, output_nullable = extract_optional(output_cls) if output_cls is not None else (None, False)
entry: dict[str, Any] = {
"name": name,
"camelName": camel,
"hasInput": has_input,
"inputType": f"{camel}Input" if has_input else None,
"outputType": f"{camel}Output",
# Nullability of the response model — Pydantic `T | None` returns. Carried
# on the function entry rather than the schema class because OpenAPI emits
# `anyOf: [{$ref}, {type:null}]` at the response level, which strict
# deserializers (Rust serde) won't decode as Option<T> without this hint.
"outputNullable": output_nullable,
"transport": "websocket" if meta.get("websocket") else "http",
"isContext": meta.get("context", False),
# Form metadata — always emitted so the schema shape matches Django's,
@@ -79,6 +88,8 @@ def _function_metadata(name: str, fn_class: Any) -> dict[str, Any]:
if meta.get("affects"):
entry["affects"] = meta["affects"]
if meta.get("merge"):
entry["merge"] = meta["merge"]
return entry
@@ -154,13 +165,28 @@ def build_schema() -> dict[str, Any]:
input_type_name = f"{camel}Input" if has_input else None
output_type_name = f"{camel}Output"
# Strip Optional so the rename gets a concrete base — nullability is
# carried on the response declaration, not the schema class itself.
output_inner, output_nullable = extract_optional(output_cls)
if has_input:
schema_classes[input_type_name] = create_model(
input_type_name, __base__=input_cls,
)
schema_classes[output_type_name] = create_model(
output_type_name, __base__=output_cls,
)
if extract_list_element(output_inner) is not None:
# list[T] — RootModel makes the rename emit `type: array` rather
# than wrapping the list in a property.
schema_classes[output_type_name] = type(
output_type_name, (RootModel[output_inner],), {},
)
else:
schema_classes[output_type_name] = create_model(
output_type_name, __base__=output_inner,
)
response_model = schema_classes[output_type_name]
if output_nullable:
response_model = response_model | None
# Stub endpoint — only exists so FastAPI walks Pydantic types into
# components.schemas. Never invoked. Annotations are set explicitly
@@ -177,7 +203,7 @@ def build_schema() -> dict[str, Any]:
schema_app.post(
f"/mizan/{name}",
response_model=schema_classes[output_type_name],
response_model=response_model,
operation_id=camel,
summary=fn_class.__doc__ or f"Call {name}",
)(stub)