AFI parity: generate the matrix from conformance probes, not prose

The per-adapter parity table was hand-maintained prose. An adapter that
never wired a capability (FastAPI SSR, Axum WebSocket) got its gap
relabelled "Django-only" or "out of scope — use native equivalents," and
nothing went red. The de-scope was crystallized in five mutually-ratifying
sites: the README §Stack-extensions table, the AFI fixture docstring
("channels/forms/shapes aren't AFI-common"), the core registry's
extension-hook framing, the mizan-fastapi __init__ docstring, and a
"CSRF is Django-only" comment in two adapters' session endpoints.

Replace prose-parity with conformance-generated parity:

- tests/afi/manifest.py declares the AFI-common surface as data — one list
  of capabilities, one of adapters. Applicability ("—") is derived from
  transport, never typed.
- tests/afi/probes.py independently inspects each backend's source for the
  artifact a capability requires (comment-stripped, backend-scoped). Green
  means wired; a cell can't be set by editing a word.
- tests/afi/test_capability_parity.py asserts every (capability × applicable
  adapter) pair is wired. 35 unwired gaps are now loud red TFDD tests, each
  naming an owed binding. No xfail/skip.
- tests/afi/parity_table.py generates the README table from the probes;
  `make parity-check` fails CI on any hand-edit, like the codegen byte-parity.

Purge the five de-scope sites. The IR byte-parity gate is unchanged and green.
`make test-afi` is now intentionally red on the 35 gaps — that board is the
owed parity work, itemized; a gap turns green by being wired, never described.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 12:58:03 -04:00
parent b41f469bbd
commit 58d2cb2848
11 changed files with 915 additions and 93 deletions

View File

@@ -2,9 +2,17 @@
mizan-fastapi — FastAPI backend adapter for the Mizan protocol.
HTTP RPC dispatch and context bundling on top of mizan-core's function
registry. Channels, Forms, Shapes, SSR are out of scope — FastAPI
projects use native equivalents (WebSocket, Pydantic, ORM-of-choice,
SSR frameworks).
registry, sharing the auth / invalidation / cache / upload core with the
Django adapter.
WebSocket, Forms, Shapes, and the SSR bridge are AFI-common capabilities
this adapter does not wire yet — open gaps on the capability-parity board
(`tests/afi/`), not out-of-scope. "Use FastAPI's native WebSocket / an ORM
of choice" is the non-goal: the AFI exists precisely to wire those to the
generated typed client through one decorator, so an adapter that defers to
the native primitive isn't yet a complete AFI adapter. The SSR bridge in
particular is framework-agnostic (`mizan.ssr.bridge.SSRBridge` has no Django
coupling) and is mountable here directly.
Usage:
from fastapi import FastAPI

View File

@@ -44,11 +44,12 @@ def _no_store(payload: Any, status_code: int = 200) -> JSONResponse:
@router.get("/session/")
async def session_init() -> JSONResponse:
"""Session-init probe. Parity with mizan-django's session endpoint.
"""Session-init endpoint. AFI-common; wired here at parity with mizan-django.
CSRF is a Django-only concern at the protocol level; FastAPI surfaces a
null token so the response shape stays uniform across backends. The
wire-parity harness uses this endpoint as its readiness probe.
The endpoint itself is the AFI-common surface. The CSRF *token* is a Django
session mechanism with no FastAPI equivalent, so this returns a null token —
the difference is in the token's backing mechanism, not in whether the
endpoint is owed. The wire-parity harness uses it as its readiness probe.
"""
return _no_store({"csrfToken": None})

View File

@@ -153,9 +153,11 @@ fn coerce_query_args(
out
}
/// GET /session/ — placeholder for the Mizan-protocol session-init endpoint.
/// CSRF is a Django-only concern; the Rust adapter returns a null token so
/// readiness-probe consumers see a well-formed response.
/// GET /session/ — the AFI-common session-init endpoint, wired at parity with
/// mizan-django and mizan-fastapi. The CSRF *token* is a Django session
/// mechanism with no Rust equivalent, so this returns a null token; the endpoint
/// itself is owed and present, and readiness-probe consumers get a well-formed
/// response.
pub async fn session_init() -> Response {
let body = serde_json::json!({ "csrfToken": null });
no_store(body)