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

@@ -1,20 +1,20 @@
//! IR deserialization tests against the AFI fixture schema.
//! IR deserialization tests against the AFI fixture (KDL).
//!
//! The fixture is captured from the FastAPI backend's `build_schema()`
//! The fixture is captured from `cores/mizan-python/src/mizan_core/ir.py::build_ir()`
//! 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.
//! elevation, and named-type presence — to confirm the typed Rust structs
//! match the KDL shape the backend emits.
use std::path::PathBuf;
use mizan_codegen::fetch::parse_ir_from_str;
use mizan_codegen::ir::{AffectKind, IsContext, Transport};
use mizan_codegen::ir::{AffectKind, IsContext, NamedType, Primitive, Transport};
fn load_fixture() -> mizan_codegen::ir::MizanIR {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/afi_schema.json");
.join("tests/fixtures/afi_ir.kdl");
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}"))
@@ -26,7 +26,6 @@ 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 [
@@ -67,6 +66,10 @@ fn afi_fixture_function_field_decode() {
assert_eq!(update_profile.affects.len(), 1);
assert_eq!(update_profile.affects[0].kind, AffectKind::Context);
assert_eq!(update_profile.affects[0].name, "user");
// Mutation with `merge="user"`.
let rename_user = ir.functions.iter().find(|f| f.name == "rename_user").unwrap();
assert_eq!(rename_user.merge, vec!["user".to_string()]);
}
@@ -77,7 +80,7 @@ fn afi_fixture_context_param_elevation() {
// 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_eq!(user_id.ty, Primitive::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()));
@@ -85,19 +88,26 @@ fn afi_fixture_context_param_elevation() {
#[test]
fn afi_fixture_components_schemas_present() {
fn afi_fixture_named_types_present() {
let ir = load_fixture();
// Each fixture function pairs with an *Input/Output schema in components.
// Every IR function references its <camelName>Input / <camelName>Output
// type by name; the IR's `type` section must declare each as a named
// type (struct, alias to a list, etc.).
for expected in [
"echoInput", "echoOutput",
"whoamiOutput",
"userProfileInput", "userProfileOutput",
"userOrdersInput",
"updateProfileInput", "updateProfileOutput",
"findUserInput", "findUserOutput",
"renameUserInput", "renameUserOutput",
] {
assert!(
ir.components.schemas.contains_key(expected),
"missing schema {expected:?}",
);
let ty = ir.types.get(expected)
.unwrap_or_else(|| panic!("missing type {expected:?}"));
// Each named type is one of the four KDL shapes — sanity-check
// we round-tripped a non-trivial declaration.
match ty {
NamedType::Struct(_) | NamedType::List(_) | NamedType::Enum(_) | NamedType::Alias(_) => {}
}
}
}