Mizan IR: cut over to KDL, delete OpenAPI envelope

Replaces the transitional OpenAPI 3.0 + `x-mizan-*` extensions
substrate with the canonical Mizan IR as KDL, per docs/AFI_ARCHITECTURE.md:
"KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it."

End-to-end cutover. No transitional path left on main.

Forward direction:
  cores/mizan-python/src/mizan_core/ir.py
    build_ir() walks mizan_core.registry, introspects Pydantic
    models directly (no JSON-Schema indirection), and emits the
    Mizan IR document. The KDL grammar is locked in this file's
    module docstring.

Backends emit KDL:
  backends/mizan-fastapi/src/mizan_fastapi/ir.py
    `python -m mizan_fastapi.ir <module>` — CLI entry point.
  backends/mizan-django/.../management/commands/export_mizan_ir.py
    `manage.py export_mizan_ir` — Django mgmt command.

Codegen consumes KDL:
  protocol/mizan-codegen/Cargo.toml: + kdl = "6"
  protocol/mizan-codegen/src/ir.rs: NamedType { Struct/List/Enum/Alias }
    + TypeShape { Primitive/Ref/List/Optional/Enum/Union } sum types,
    replacing the JsonSchema sprawl. KDL parser walks the
    `kdl::KdlDocument` tree into typed Rust structs.
  protocol/mizan-codegen/src/fetch.rs: subprocess command switches
    to the new IR-export entry points.
  All emit modules (stage1 / react / python / rust / vue / svelte /
    channels) port their type-walkers from JsonSchema to the new
    sum types — case analysis collapses substantially.

Substrate-honesty wins beyond the moat closure:
  - `int | bool` multi-arm unions land as `TypeShape::Union` (was
    silently coerced to "string" before).
  - `<CamelName>Output = list[T]` returns emit as named alias
    types instead of struct-shaped wrappers, so consumer code
    `.map()` works directly on the type.
  - Pydantic field defaults flow through to `default` properties
    in KDL, then back to non-optional shape in every target.

Deleted:
  - backends/mizan-fastapi/src/mizan_fastapi/{cli,schema}.py
  - backends/mizan-django/.../export_mizan_schema.py
  - openapi-bearing half of mizan/export/__init__.py (edge
    manifest generator preserved — separate concern).
  - tests/afi/schema_normalizer.py
  - tests/fixtures/{afi_schema.json, channels_schema.json}
  - tests/fixtures/js_* baseline directories.

Verification:
  - 20 mizan-codegen unit tests green (IR deserialization,
    byte-equivalence parity across stage1/rust/python/react/vue/svelte
    against fresh KDL-driven baselines, channels structural).
  - tests/rust/run_wire_parity.py: 12/12 probes green driving
    the binary end-to-end through KDL.
  - Blazr studio-ui typechecks against the regenerated React client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 19:14:47 -04:00
parent 7fb0c4a400
commit 9900f8a36f
86 changed files with 2231 additions and 2272 deletions

View File

@@ -10,7 +10,10 @@ use crate::config::{Config, RustKernelSpec};
use crate::emit::CodegenTarget;
use crate::emit::EmittedFile;
use crate::emit::casing::{pascal_case, rust_ident, rust_type_ident, snake_case};
use crate::ir::{IsContext, JsonSchema, MizanContext, MizanFunction, MizanIR};
use crate::ir::{
IsContext, MizanContext, MizanFunction, MizanIR, NamedType, Primitive,
StructField as IrStructField, TypeShape,
};
pub struct RustCrate;
@@ -35,7 +38,7 @@ impl CodegenTarget for RustCrate {
.render().expect("Cargo.toml renders"),
));
out.push(EmittedFile::new("src/types.rs", emit_types_rs(&ir.components.schemas)));
out.push(EmittedFile::new("src/types.rs", emit_types_rs(&ir.types)));
let mut context_modules: Vec<String> = Vec::new();
for (ctx_name, ctx_meta) in &ir.contexts {
@@ -119,8 +122,8 @@ struct ContextTemplate<'a> {
snake: String,
ctx_name: &'a str,
type_imports: Vec<String>,
data_fields: Vec<StructField>,
params: Vec<StructField>,
data_fields: Vec<RustField>,
params: Vec<RustField>,
}
@@ -144,7 +147,10 @@ struct TypesTemplate {
}
struct StructField {
/// Renderer-side view of a single Rust struct field. Distinct from
/// `ir::StructField` (the IR shape) because the renderer carries
/// already-rendered identifiers and rename flags.
struct RustField {
raw_name: String,
ident: String,
ty: String,
@@ -212,10 +218,10 @@ fn emit_context_file(
ctx_fns.iter().map(|f| rust_type_ident(&f.output_type)),
);
let data_fields: Vec<StructField> = ctx_fns.iter()
let data_fields: Vec<RustField> = ctx_fns.iter()
.map(|f| {
let ident = rust_ident(&f.name);
StructField {
RustField {
has_rename: ident != f.name,
raw_name: f.name.clone(),
ident,
@@ -224,12 +230,12 @@ fn emit_context_file(
})
.collect();
let params: Vec<StructField> = ctx_meta.params.iter()
let params: Vec<RustField> = ctx_meta.params.iter()
.map(|(p_name, p_meta)| {
let ident = rust_ident(p_name);
let base = param_rust_type(&p_meta.ty);
let base = param_rust_type(p_meta.ty);
let ty = if p_meta.required { base.to_string() } else { format!("Option<{base}>") };
StructField {
RustField {
has_rename: ident != *p_name,
raw_name: p_name.clone(),
ident,
@@ -249,12 +255,12 @@ fn emit_context_file(
}
fn param_rust_type(json_ty: &str) -> &'static str {
match json_ty {
"integer" => "i64",
"number" => "f64",
"boolean" => "bool",
_ => "String",
fn param_rust_type(p: Primitive) -> &'static str {
match p {
Primitive::Integer => "i64",
Primitive::Number => "f64",
Primitive::Boolean => "bool",
Primitive::String => "String",
}
}
@@ -303,33 +309,26 @@ fn emit_call_file(fn_meta: &MizanFunction) -> String {
// ─── types.rs ──────────────────────────────────────────────────────────────
/// Per-types-file context tracking enum names hoisted out of inline
/// `field { enum "a" "b" }` declarations into Rust top-level enum types.
struct EnumCtx {
hoisted: Vec<(String, Vec<serde_json::Value>)>,
depth: usize,
hoisted: Vec<(String, Vec<String>)>,
enum_name: Option<String>,
}
fn emit_types_rs(schemas: &IndexMap<String, JsonSchema>) -> String {
let mut ctx = EnumCtx { hoisted: Vec::new(), depth: 0, enum_name: None };
fn emit_types_rs(types: &IndexMap<String, NamedType>) -> String {
let mut ctx = EnumCtx { hoisted: Vec::new(), enum_name: None };
let schemas_block = schemas.iter()
.map(|(raw_name, schema)| {
let schemas_block = types.iter()
.map(|(raw_name, ty)| {
let name = rust_type_ident(raw_name);
if let Some(values) = &schema.r#enum {
if schema.ty.as_deref() == Some("string") {
return emit_string_enum(&name, values);
}
match ty {
NamedType::Struct(fields) => emit_struct_decl(&name, fields, &mut ctx),
NamedType::List(inner) => emit_transparent_array(&name, inner, &mut ctx),
NamedType::Enum(variants) => emit_string_enum(&name, variants),
NamedType::Alias(inner) => emit_type_alias(&name, inner, &mut ctx),
}
if schema.ty.as_deref() == Some("array") {
return emit_transparent_array(&name, schema, &mut ctx);
}
if schema.ty.as_deref() == Some("object") {
if let Some(props) = &schema.properties {
return emit_struct(&name, schema, props, &mut ctx);
}
}
emit_type_alias(&name, schema, &mut ctx)
})
.collect::<Vec<_>>()
.join("\n");
@@ -344,12 +343,11 @@ fn emit_types_rs(schemas: &IndexMap<String, JsonSchema>) -> String {
}
fn emit_string_enum(name: &str, variants: &[serde_json::Value]) -> String {
fn emit_string_enum(name: &str, variants: &[String]) -> String {
let body = variants.iter()
.filter_map(|v| v.as_str())
.map(|v| {
let ident = pascal_case(v);
let rename = if ident == v {
let rename = if ident == *v {
String::new()
} else {
format!(" #[serde(rename = {})]\n", json_str(v))
@@ -365,40 +363,33 @@ fn emit_string_enum(name: &str, variants: &[serde_json::Value]) -> String {
}
fn emit_transparent_array(name: &str, schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
ctx.depth = 1;
fn emit_transparent_array(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
ctx.enum_name = None;
let inner = rust_type_from_schema(schema.items.as_deref().unwrap_or(&JsonSchema::default()), ctx);
let inner_ty = rust_type_from_shape(inner, ctx);
format!(
"#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner}>);\n",
"#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner_ty}>);\n",
)
}
fn emit_struct(
fn emit_struct_decl(
name: &str,
schema: &JsonSchema,
properties: &IndexMap<String, JsonSchema>,
fields: &[IrStructField],
ctx: &mut EnumCtx,
) -> String {
let required: std::collections::HashSet<&str> =
schema.required.iter().map(String::as_str).collect();
let fields = properties.iter()
.map(|(field_raw, field_schema)| {
let field_name = rust_ident(field_raw);
ctx.depth = 1;
ctx.enum_name = Some(format!("{name}_{}", pascal_case(field_raw)));
let mut ty = rust_type_from_schema(field_schema, ctx);
let is_required = required.contains(field_raw.as_str())
|| field_schema.default.is_some();
let fields_body = fields.iter()
.map(|f| {
let field_name = rust_ident(&f.name);
ctx.enum_name = Some(format!("{name}_{}", pascal_case(&f.name)));
let mut ty = rust_type_from_shape(&f.shape, ctx);
let is_required = f.required || f.default.is_some();
if !is_required && !ty.starts_with("Option<") {
ty = format!("Option<{ty}>");
}
let rename = if field_name == *field_raw {
let rename = if field_name == f.name {
String::new()
} else {
format!(" #[serde(rename = \"{field_raw}\")]\n")
format!(" #[serde(rename = \"{raw}\")]\n", raw = f.name)
};
format!("{rename} pub {field_name}: {ty},")
})
@@ -406,69 +397,48 @@ fn emit_struct(
.join("\n");
format!(
"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields}\n}}\n",
"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields_body}\n}}\n",
)
}
fn emit_type_alias(name: &str, schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
ctx.depth = 0;
fn emit_type_alias(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
ctx.enum_name = Some(name.to_string());
let ty = rust_type_from_schema(schema, ctx);
let ty = rust_type_from_shape(inner, ctx);
format!("pub type {name} = {ty};\n")
}
fn rust_type_from_schema(schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
if let Some(r) = schema.ref_name() {
return rust_type_ident(r);
}
if let Some(any_of) = &schema.any_of {
let has_null = any_of.iter().any(|s| s.ty.as_deref() == Some("null"));
let non_null: Vec<&JsonSchema> = any_of
.iter()
.filter(|s| s.ty.as_deref() != Some("null"))
.collect();
if has_null && non_null.len() == 1 {
fn rust_type_from_shape(shape: &TypeShape, ctx: &mut EnumCtx) -> String {
match shape {
TypeShape::Ref(name) => rust_type_ident(name),
TypeShape::Primitive(Primitive::Integer) => "i64".to_string(),
TypeShape::Primitive(Primitive::Number) => "f64".to_string(),
TypeShape::Primitive(Primitive::Boolean) => "bool".to_string(),
TypeShape::Primitive(Primitive::String) => "String".to_string(),
TypeShape::List(inner) => {
ctx.enum_name = None;
return format!("Option<{}>", rust_type_from_schema(non_null[0], ctx));
format!("Vec<{}>", rust_type_from_shape(inner, ctx))
}
}
let nullable = schema.nullable;
let inner = inner_rust_type(schema, ctx);
if nullable {
format!("Option<{inner}>")
} else {
inner
}
}
fn inner_rust_type(schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
if let Some(values) = &schema.r#enum {
if schema.ty.as_deref() == Some("string") {
TypeShape::Optional(inner) => {
ctx.enum_name = None;
format!("Option<{}>", rust_type_from_shape(inner, ctx))
}
TypeShape::Enum(variants) => {
// Inline enums hoist out into top-level Rust enum types so the
// generated struct field can reference them by name.
let enum_name = ctx
.enum_name
.clone()
.unwrap_or_else(|| format!("Enum_{}", ctx.depth));
ctx.hoisted.push((enum_name.clone(), values.clone()));
return enum_name;
.unwrap_or_else(|| "Enum_inline".to_string());
ctx.hoisted.push((enum_name.clone(), variants.clone()));
enum_name
}
}
match schema.ty.as_deref() {
Some("integer") => "i64".to_string(),
Some("number") => "f64".to_string(),
Some("boolean") => "bool".to_string(),
Some("string") => "String".to_string(),
Some("array") => {
ctx.depth += 1;
ctx.enum_name = None;
let inner = rust_type_from_schema(schema.items.as_deref().unwrap_or(&JsonSchema::default()), ctx);
format!("Vec<{inner}>")
TypeShape::Union(_branches) => {
// Rust serde doesn't have a clean way to deserialize an untagged
// multi-arm union without losing type info; fall back to a JSON
// Value so the consumer can match on the runtime variant.
"serde_json::Value".to_string()
}
Some("object") => "serde_json::Value".to_string(),
_ => "serde_json::Value".to_string(),
}
}