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:
@@ -135,6 +135,75 @@ impl InvalidationTarget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Percent-encode for the `X-Mizan-Invalidate` header, matching Python's
|
||||
/// `urllib.parse.quote(str(v), safe='')`: the RFC 3986 unreserved set
|
||||
/// (`A-Za-z0-9_.-~`) passes through; every other byte (of the UTF-8 encoding)
|
||||
/// becomes `%XX` with **upper-case** hex.
|
||||
fn url_encode(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'-' | b'~' => {
|
||||
out.push(b as char);
|
||||
}
|
||||
_ => out.push_str(&format!("%{b:02X}")),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Render an invalidation value to a JSON-ish string for header param values.
|
||||
/// Mirrors Python's `str(v)`: a JSON string yields its raw text; numbers and
|
||||
/// booleans their literal spelling (`true`/`false`); other shapes their JSON.
|
||||
fn header_value_str(v: &Value) -> String {
|
||||
match v {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::Null => "None".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a list of targets to the `X-Mizan-Invalidate` header value —
|
||||
/// byte-for-byte with `cores/mizan-python`'s `format_invalidate_header`:
|
||||
/// comma-separated contexts, semicolon-separated URL-encoded params per
|
||||
/// context (params sorted by key).
|
||||
///
|
||||
/// `[Context("user")]` → `user`
|
||||
/// `[Context("user"), Context("notifications")]` → `user, notifications`
|
||||
/// `[ScopedContext{user, {user_id:5}}]` → `user;user_id=5`
|
||||
/// `[ScopedContext{search, {q:"hello world"}}]` → `search;q=hello%20world`
|
||||
pub fn format_invalidate_header(targets: &[InvalidationTarget]) -> String {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
for t in targets {
|
||||
match t {
|
||||
InvalidationTarget::Context(name) | InvalidationTarget::Function(name) => {
|
||||
parts.push(name.clone());
|
||||
}
|
||||
InvalidationTarget::ScopedContext { context, params } => {
|
||||
if params.is_empty() {
|
||||
parts.push(context.clone());
|
||||
} else {
|
||||
// BTreeMap-sort the keys to match Python's `sorted(params.items())`.
|
||||
let mut keys: Vec<&String> = params.keys().collect();
|
||||
keys.sort();
|
||||
let param_str = keys
|
||||
.iter()
|
||||
.map(|k| {
|
||||
let v = ¶ms[*k];
|
||||
format!("{}={}", url_encode(k), url_encode(&header_value_str(v)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
parts.push(format!("{context};{param_str}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.join(", ")
|
||||
}
|
||||
|
||||
/// One entry in the response's `merge` array. Server-resolved slot — the
|
||||
/// kernel writes the value into `bundle[slot]` directly.
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
Reference in New Issue
Block a user