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,121 @@
//! Wire error envelope. Mirrors `MizanError` in `frontends/mizan-base/src/index.ts`.
//!
//! Two envelope shapes are tolerated:
//!
//! - FastAPI: `{"error": {"code": "...", "message": "...", "details": ...}}`
//! - Django: `{"error": true, "code": "...", "message": "...", "details": ...}`
//!
//! When neither shape parses, `code` falls back to `HTTP_<status>` and the
//! raw response body is the message.
use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct MizanError {
pub status: u16,
pub code: String,
pub message: String,
pub details: Option<Value>,
pub raw_body: String,
}
impl MizanError {
pub fn from_response(status: u16, body: String) -> Self {
let parsed = serde_json::from_str::<Envelope>(&body).ok();
let (code, message, details) = match parsed {
Some(Envelope::Fastapi { error }) => (
error.code.unwrap_or_else(|| format!("HTTP_{status}")),
error.message.unwrap_or_else(|| format!("Mizan call failed ({status})")),
error.details,
),
Some(Envelope::Django { code, message, details, .. }) => (
code.unwrap_or_else(|| format!("HTTP_{status}")),
message.unwrap_or_else(|| format!("Mizan call failed ({status})")),
details,
),
None => (
format!("HTTP_{status}"),
format!("Mizan call failed ({status})"),
None,
),
};
Self { status, code, message, details, raw_body: body }
}
pub fn transport(message: impl Into<String>) -> Self {
Self {
status: 0,
code: "TRANSPORT".to_string(),
message: message.into(),
details: None,
raw_body: String::new(),
}
}
}
impl std::fmt::Display for MizanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Mizan {} ({}): {}", self.status, self.code, self.message)
}
}
impl std::error::Error for MizanError {}
#[derive(Deserialize)]
#[serde(untagged)]
enum Envelope {
Fastapi { error: NestedError },
Django {
// Django form is `{"error": true, "code": ..., "message": ..., "details": ...}`.
// `error` is a bool sentinel; the actual fields are siblings.
#[allow(dead_code)]
error: bool,
code: Option<String>,
message: Option<String>,
details: Option<Value>,
},
}
#[derive(Deserialize)]
struct NestedError {
code: Option<String>,
message: Option<String>,
details: Option<Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_fastapi_envelope() {
let body = r#"{"error":{"code":"BAD_REQUEST","message":"oops","details":{"k":1}}}"#;
let e = MizanError::from_response(400, body.to_string());
assert_eq!(e.code, "BAD_REQUEST");
assert_eq!(e.message, "oops");
assert_eq!(e.details, Some(serde_json::json!({"k": 1})));
}
#[test]
fn parses_django_envelope() {
let body = r#"{"error":true,"code":"NOT_FOUND","message":"missing","details":null}"#;
let e = MizanError::from_response(404, body.to_string());
assert_eq!(e.code, "NOT_FOUND");
assert_eq!(e.message, "missing");
}
#[test]
fn falls_back_on_unparseable_body() {
let e = MizanError::from_response(500, "Internal Server Error".to_string());
assert_eq!(e.code, "HTTP_500");
assert!(e.message.contains("500"));
}
}