AFI parity: close all 35 gaps — every adapter wires every AFI-common capability

The conformance board (tests/afi/test_capability_parity.py) is now fully green:
90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The
gaps the prose table used to launder as "Django-only" / "out of scope" are
wired, against the pinned-spec model (single-authored spec, byte-identical
conformance across languages) — never per-language reimplementation.

FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest),
WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic
SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes
(SQLAlchemy projection, same declaration surface as django-readers), Forms
(Pydantic schema/validate/submit).

Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth=
enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC
subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT
mint+verify and cache-key derivation byte-pinned to the Python reference
(cache_keys_pin, token_pin, invalidate_header_pin).

TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS
backend can feed the codegen — the largest gap), multipart upload, session-init,
WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms.

Verified in the merged tree: core 25, fastapi 74, django 353/21-skip,
mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 13:44:35 -04:00
parent 58d2cb2848
commit 6c5f6f1fba
81 changed files with 9893 additions and 463 deletions

View File

@@ -26,6 +26,15 @@ pub struct FunctionArgs {
pub merge: Vec<Path>,
pub websocket: bool,
pub private: bool,
/// `auth = "required" | "staff" | "superuser"` (or bare `auth` ⇒
/// "required") — the `@client(auth=...)` guard. Bare-true and the string
/// `"required"` both mean "must be authenticated".
pub auth: Option<String>,
/// `form_name = "..."` + `form_role = "schema"|"validate"|"submit"` — the
/// Forms binding's per-endpoint metadata, mirroring the Django form
/// `_meta` keys. Carried into the IR (`is-form`/`form-name`/`form-role`).
pub form_name: Option<String>,
pub form_role: Option<String>,
}
impl FunctionArgs {
@@ -45,10 +54,16 @@ impl FunctionArgs {
out.affects = collect_paths(&nv.value)?;
} else if nv.path.is_ident("merge") {
out.merge = collect_paths(&nv.value)?;
} else if nv.path.is_ident("auth") {
out.auth = Some(expect_str(&nv.value)?);
} else if nv.path.is_ident("form_name") {
out.form_name = Some(expect_str(&nv.value)?);
} else if nv.path.is_ident("form_role") {
out.form_role = Some(expect_str(&nv.value)?);
} else {
return Err(syn::Error::new_spanned(
nv.path,
"unknown attribute key; expected one of: context, affects, merge",
"unknown attribute key; expected one of: context, affects, merge, auth, form_name, form_role",
));
}
}
@@ -57,10 +72,12 @@ impl FunctionArgs {
out.websocket = true;
} else if p.is_ident("private") {
out.private = true;
} else if p.is_ident("auth") {
out.auth = Some("required".to_string());
} else {
return Err(syn::Error::new_spanned(
p,
"unknown flag; expected `websocket` or `private`",
"unknown flag; expected `websocket`, `private`, or `auth`",
));
}
}
@@ -99,6 +116,21 @@ fn expect_path(expr: &Expr) -> syn::Result<Path> {
}
}
fn expect_str(expr: &Expr) -> syn::Result<String> {
if let Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = expr
{
Ok(s.value())
} else {
Err(syn::Error::new_spanned(
expr,
"expected a string literal (e.g. `\"staff\"`)",
))
}
}
fn collect_paths(expr: &Expr) -> syn::Result<Vec<Path>> {
match expr {
Expr::Path(_) => Ok(vec![expect_path(expr)?]),
@@ -183,7 +215,11 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
});
}
quote! {
#[derive(::std::fmt::Debug, ::std::clone::Clone, ::serde::Serialize, ::serde::Deserialize)]
// The synthetic Input is only ever *deserialized* (from the call's
// JSON args by the dispatch wrapper); it is never serialized, so it
// derives `Deserialize` only. Dropping `Serialize` lets binary
// field types like `Upload` (deserialize-only) participate.
#[derive(::std::fmt::Debug, ::std::clone::Clone, ::serde::Deserialize)]
pub struct #input_type_ident {
#(#field_defs)*
}
@@ -353,6 +389,20 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
let output_nullable = analysis.nullable;
let private = args.private;
let auth_value = match &args.auth {
Some(a) => quote! { ::std::option::Option::Some(#a) },
None => quote! { ::std::option::Option::None },
};
let is_form = args.form_name.is_some() || args.form_role.is_some();
let form_name_value = match &args.form_name {
Some(n) => quote! { ::std::option::Option::Some(#n) },
None => quote! { ::std::option::Option::None },
};
let form_role_value = match &args.form_role {
Some(r) => quote! { ::std::option::Option::Some(#r) },
None => quote! { ::std::option::Option::None },
};
let dispatch_body = build_dispatch(
&item,
&input_args,
@@ -389,6 +439,10 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
fn merge(&self) -> &'static [&'static str] { #merge_static }
fn transport(&self) -> ::mizan_core::Transport { #transport_value }
fn private(&self) -> bool { #private }
fn auth(&self) -> ::std::option::Option<&'static str> { #auth_value }
fn is_form(&self) -> bool { #is_form }
fn form_name(&self) -> ::std::option::Option<&'static str> { #form_name_value }
fn form_role(&self) -> ::std::option::Option<&'static str> { #form_role_value }
fn input_params(&self) -> &'static [::mizan_core::InputParam] { #params_static }
fn dispatch<'a>(

View File

@@ -105,6 +105,15 @@ pub fn type_shape_expr(ty: &Type) -> TokenStream {
if let Some(p) = primitive_of(ty) {
return quote! { ::mizan_core::TypeShape::Primitive(#p) };
}
if is_upload(ty) {
// An `Upload`-typed field emits the IR `upload` type-child rather than
// a `ref`, matching the Python emitter. Constraints (`max-size`,
// `content-type`) aren't carried in this baseline — an unconstrained
// upload — but the wire/IR shape is the recognized `upload` node.
return quote! {
::mizan_core::TypeShape::Upload { max_size: ::std::option::Option::None, content_types: &[] }
};
}
// Fallback: assume a user-defined struct/enum implementing MizanType.
// The Ref name comes from `<T as MizanType>::TYPE_NAME` (associated const).
quote! { ::mizan_core::TypeShape::Ref(<#ty as ::mizan_core::MizanType>::TYPE_NAME) }
@@ -149,6 +158,19 @@ pub fn unwrap_btreemap_value(ty: &Type) -> Option<Type> {
type_args.next()
}
/// True if `ty` names the `mizan_core::Upload` marker (by its last path
/// segment) — the binary file-input type.
pub fn is_upload(ty: &Type) -> bool {
match ty {
Type::Path(TypePath { qself: None, path }) => path
.segments
.last()
.map(|s| s.ident == "Upload")
.unwrap_or(false),
_ => false,
}
}
/// Emit a `Primitive` const-expression for `ty`, or `None` if `ty` isn't a
/// known primitive scalar.
pub fn primitive_of(ty: &Type) -> Option<TokenStream> {