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

@@ -0,0 +1,75 @@
//! Drive the codegen-emitted `fixture_client` crate against a live
//! FastAPI fixture. Validates not just the kernel wire (which
//! `drive_kernel.rs` already covers) but that the codegen actually
//! produces typed-functions that round-trip cleanly through the same
//! kernel.
use std::env;
use std::process::ExitCode;
use mizan_rust::{MizanClient, MizanConfig};
use fixture_client::contexts::user::{fetch_user_context, UserContextParams};
use fixture_client::functions::echo::call_echo;
use fixture_client::functions::find_user::call_find_user;
use fixture_client::functions::rename_user::call_rename_user;
use fixture_client::functions::whoami::call_whoami;
use fixture_client::mutations::update_profile::call_update_profile;
use fixture_client::types::{
EchoInput, FindUserInput, RenameUserInput, UpdateProfileInput,
};
#[tokio::main]
async fn main() -> ExitCode {
let base_url = env::args().nth(1)
.unwrap_or_else(|| "http://127.0.0.1:8765/api/mizan".to_string());
let client = MizanClient::new(MizanConfig {
base_url,
session: false,
..Default::default()
});
let mut failures = 0usize;
match call_echo(&client, &EchoInput { text: "hello".to_string() }).await {
Ok(out) => println!("call_echo -> message={:?}", out.message),
Err(e) => { eprintln!("call_echo ERR {e}"); failures += 1; }
}
match call_whoami(&client).await {
Ok(out) => println!("call_whoami -> authenticated={} email={:?}", out.authenticated, out.email),
Err(e) => { eprintln!("call_whoami ERR {e}"); failures += 1; }
}
match call_find_user(&client, &FindUserInput { user_id: 99999 }).await {
Ok(None) => println!("call_find_user(99999) -> None"),
Ok(Some(out)) => println!("call_find_user(99999) -> user_id={} name={:?}", out.user_id, out.name),
Err(e) => { eprintln!("call_find_user ERR {e}"); failures += 1; }
}
match call_update_profile(&client, &UpdateProfileInput { user_id: 5, name: "Ryth".to_string() }).await {
Ok(out) => println!("call_update_profile -> ok={}", out.ok),
Err(e) => { eprintln!("call_update_profile ERR {e}"); failures += 1; }
}
match call_rename_user(&client, &RenameUserInput { user_id: 5, name: "RythR".to_string() }).await {
Ok(out) => println!("call_rename_user -> user_id={} name={:?}", out.user_id, out.name),
Err(e) => { eprintln!("call_rename_user ERR {e}"); failures += 1; }
}
match fetch_user_context(&client, &UserContextParams { user_id: 5 }).await {
Ok(out) => println!(
"fetch_user_context(5) -> user_profile={{ user_id:{}, name:{:?} }} user_orders.len={}",
out.user_profile.user_id, out.user_profile.name, out.user_orders.0.len(),
),
Err(e) => { eprintln!("fetch_user_context ERR {e}"); failures += 1; }
}
if failures > 0 {
eprintln!("[drive_emitted] {failures} probe(s) failed");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}

View File

@@ -0,0 +1,74 @@
//! Drive the mizan-rust kernel against a live FastAPI fixture app and
//! print every response. Used by `run_wire_parity.sh` which:
//!
//! 1. Boots `tests/afi/fastapi_app.py` via uvicorn on port 8765.
//! 2. Polls `/openapi.json` until the server is up.
//! 3. Runs `cargo run --bin drive_kernel -- http://127.0.0.1:8765/api/mizan`.
//! 4. Diffs the stdout against a committed snapshot.
//!
//! The kernel exercises every endpoint the fixture declares: the two
//! plain functions (`echo`, `whoami`), the two-function `user` context,
//! the `update_profile` mutation, the `find_user` Optional path, and
//! the `rename_user` merge mutation.
use std::env;
use std::process::ExitCode;
use mizan_rust::{MizanClient, MizanConfig};
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> ExitCode {
let base_url = env::args().nth(1)
.unwrap_or_else(|| "http://127.0.0.1:8765/api/mizan".to_string());
let client = MizanClient::new(MizanConfig {
base_url,
session: false,
..Default::default()
});
let mut failures = 0usize;
failures += probe(&client, "echo", json!({"text": "hello"})).await;
failures += probe(&client, "whoami", json!({})).await;
failures += probe(&client, "find_user", json!({"user_id": 99999})).await;
failures += probe(&client, "update_profile", json!({"user_id": 5, "name": "Ryth"})).await;
failures += probe(&client, "rename_user", json!({"user_id": 5, "name": "RythR"})).await;
failures += probe_context(&client, "user", json!({"user_id": 5})).await;
if failures > 0 {
eprintln!("[drive_kernel] {failures} probe(s) failed");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
async fn probe(client: &MizanClient, fn_name: &str, args: Value) -> usize {
match client.call(fn_name, args.clone()).await {
Ok(result) => {
println!("call {fn_name} args={args} -> {result}");
0
}
Err(err) => {
eprintln!("call {fn_name} args={args} -> ERR {err}");
1
}
}
}
async fn probe_context(client: &MizanClient, name: &str, params: Value) -> usize {
match client.fetch_context(name, &params).await {
Ok(data) => {
println!("ctx {name} params={params} -> {data}");
0
}
Err(err) => {
eprintln!("ctx {name} params={params} -> ERR {err}");
1
}
}
}