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,103 @@
//! IR deserialization tests against the AFI fixture schema.
//!
//! The fixture is captured from the FastAPI backend's `build_schema()`
//! against `tests/afi/fixture.py`. Each test exercises a different facet
//! of the IR — function set, per-function field decoding, context-param
//! elevation, and components.schemas presence — to confirm the typed
//! Rust structs match the JSON shape the backends emit.
use std::path::PathBuf;
use mizan_codegen::fetch::parse_ir_from_str;
use mizan_codegen::ir::{AffectKind, IsContext, Transport};
fn load_fixture() -> mizan_codegen::ir::MizanIR {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/afi_schema.json");
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
parse_ir_from_str(&raw).unwrap_or_else(|e| panic!("parse IR: {e}"))
}
#[test]
fn afi_fixture_deserializes_function_set() {
let ir = load_fixture();
let names: Vec<&str> = ir.functions.iter().map(|f| f.name.as_str()).collect();
// Seven fixture functions per tests/afi/fixture.py.
assert_eq!(ir.functions.len(), 7, "expected 7 functions, got {}: {names:?}", ir.functions.len());
for expected in [
"echo", "whoami",
"user_profile", "user_orders",
"update_profile", "find_user", "rename_user",
] {
assert!(names.contains(&expected), "missing function {expected:?} in {names:?}");
}
}
#[test]
fn afi_fixture_function_field_decode() {
let ir = load_fixture();
let echo = ir.functions.iter().find(|f| f.name == "echo").unwrap();
assert_eq!(echo.camel_name, "echo");
assert!(echo.has_input);
assert_eq!(echo.input_type.as_deref(), Some("echoInput"));
assert_eq!(echo.output_type, "echoOutput");
assert!(!echo.output_nullable);
assert_eq!(echo.transport, Transport::Http);
assert_eq!(echo.is_context, IsContext::No);
let whoami = ir.functions.iter().find(|f| f.name == "whoami").unwrap();
assert!(!whoami.has_input);
// `find_user` returns `ProfileOutput | None` — outputNullable must be true.
let find_user = ir.functions.iter().find(|f| f.name == "find_user").unwrap();
assert!(find_user.output_nullable, "find_user must be outputNullable");
// Context-typed function picks up the context name.
let user_profile = ir.functions.iter().find(|f| f.name == "user_profile").unwrap();
assert_eq!(user_profile.is_context.as_str(), Some("user"));
// Mutation with `affects="user"` lands in `affects` as a context target.
let update_profile = ir.functions.iter().find(|f| f.name == "update_profile").unwrap();
assert_eq!(update_profile.affects.len(), 1);
assert_eq!(update_profile.affects[0].kind, AffectKind::Context);
assert_eq!(update_profile.affects[0].name, "user");
}
#[test]
fn afi_fixture_context_param_elevation() {
let ir = load_fixture();
let user = ir.contexts.get("user").expect("user context group");
// Both context functions share `user_id` as a required param.
let user_id = user.params.get("user_id").expect("user_id param");
assert_eq!(user_id.ty, "integer");
assert!(user_id.required, "user_id is required (declared by every fn in the group)");
assert!(user_id.shared_by.contains(&"user_profile".to_string()));
assert!(user_id.shared_by.contains(&"user_orders".to_string()));
}
#[test]
fn afi_fixture_components_schemas_present() {
let ir = load_fixture();
// Each fixture function pairs with an *Input/Output schema in components.
for expected in [
"echoInput", "echoOutput",
"whoamiOutput",
"userProfileInput", "userProfileOutput",
"updateProfileInput", "updateProfileOutput",
"findUserInput", "findUserOutput",
] {
assert!(
ir.components.schemas.contains_key(expected),
"missing schema {expected:?}",
);
}
}