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

167
tests/afi/manifest.py Normal file
View File

@@ -0,0 +1,167 @@
"""
The AFI surface, as data — the single source of truth for what every Mizan
adapter owes.
This module exists because "what is AFI-common" used to live as prose: a
README table, a fixture docstring, an adapter `__init__` comment. Prose drifts.
An adapter that didn't wire a capability got its gap relabelled "Django-only"
or "out of scope — use native equivalents," and nothing went red. Here the
surface is a list of `Capability` objects and the implementors are a list of
`Adapter` objects; the parity table and the conformance suite are both
*generated* from these two lists. A capability cannot be de-scoped by editing
a word — only by deleting it from `CAPABILITIES`, which is a reviewable diff,
not a buried cell.
Applicability ("" in the table) is DERIVED, never asserted: a capability that
`requires={"transport": "http"}` is simply not applicable to an IPC adapter.
The header-invalidation channel does not exist over Tauri IPC because IPC has
no headers — that is a fact about the transport, computed by `applies()`, not a
parity decision an agent gets to make.
The line between AFI-common and genuinely-backend-bound:
AFI-common Every adapter owes a binding. The protocol core, plus every
`register_extension` point (WebSocket, SSR, JWT, MWT, Shapes,
Forms). A missing one is a GAP (❌), never a category.
Backend-bound Not in this manifest at all. `allauth` is a Django-ecosystem
package — legitimately Django-only. The *bindings* of common
capabilities are backend-specific (django-readers is Django's
Shapes binding; Django Forms is Django's Forms binding) — but
the *capability* is common, so it lives here and each adapter
owes its own binding, not an "N/A."
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
class Tier(str, Enum):
"""How the README groups capabilities — presentation only, not semantics."""
PROTOCOL_CORE = "Protocol core"
EDGE_CACHE = "Edge, cache & enforcement"
EXTENSION = "Extension points"
@dataclass(frozen=True)
class Capability:
"""One thing every applicable adapter owes a binding for.
`requires` declares preconditions an adapter must meet for this capability
to APPLY. The only key today is `transport` ("http" | "ipc"); a capability
that names one is inapplicable to adapters on the other, and `applies()`
renders that as a derived "". An empty `requires` means transport-agnostic:
every adapter owes it, full stop.
"""
id: str
title: str
tier: Tier
requires: dict[str, str] = field(default_factory=dict)
note: str = ""
@dataclass(frozen=True)
class Adapter:
"""A backend adapter and the declared properties parity is computed against.
`transport` is the load-bearing property: "http" adapters have a header
channel, an edge, a session endpoint; "ipc" adapters (Tauri) do not, and
those capabilities derive to "" rather than counting as gaps.
"""
id: str
title: str
language: str # "python" | "rust" | "typescript"
transport: str # "http" | "ipc"
# ─── The AFI-common surface ───────────────────────────────────────────────────
CAPABILITIES: list[Capability] = [
# Protocol core — the wire contract every adapter implements.
Capability("rpc_call", "RPC call dispatch (`{result, invalidate}`)", Tier.PROTOCOL_CORE),
Capability("context_bundle", "Named-context bundle fetch", Tier.PROTOCOL_CORE),
Capability("invalidate_body", "Invalidation — JSON body", Tier.PROTOCOL_CORE),
Capability(
"invalidate_header", "Invalidation — `X-Mizan-Invalidate` header",
Tier.PROTOCOL_CORE, requires={"transport": "http"},
note="The header channel is co-equal with the body channel in the spec. "
"IPC transports carry invalidation in the response envelope instead.",
),
Capability("invalidate_autoscope", "Invalidation auto-scoping (three-tier)", Tier.PROTOCOL_CORE),
Capability("registration", "Function discovery / registration", Tier.PROTOCOL_CORE),
Capability("ir_export", "Codegen IR export (KDL)", Tier.PROTOCOL_CORE),
Capability("upload", "File uploads (`Upload` type)", Tier.PROTOCOL_CORE),
# Edge, cache & enforcement.
Capability("auth_enforcement", "Auth-guard enforcement (`auth=…` rejects)", Tier.EDGE_CACHE),
Capability("origin_cache", "Origin-side HMAC cache", Tier.EDGE_CACHE),
Capability(
"edge_manifest", "Edge manifest export", Tier.EDGE_CACHE,
requires={"transport": "http"},
note="The manifest configures an HTTP/CDN edge; a desktop IPC shell has no edge.",
),
Capability(
"psr", "PSR (`render_strategy` in manifest)", Tier.EDGE_CACHE,
requires={"transport": "http"},
),
Capability(
"session_init", "Session / CSRF init endpoint", Tier.EDGE_CACHE,
requires={"transport": "http"},
),
# Extension points — the `register_extension` surface. AFI-common, every one.
Capability("websocket", "WebSocket transport (`websocket=` declared)", Tier.EXTENSION),
Capability("ssr_bridge", "SSR bridge (subprocess renderer)", Tier.EXTENSION),
Capability("jwt", "JWT auth (access / refresh)", Tier.EXTENSION),
Capability(
"mwt", "MWT (edge identity token)", Tier.EXTENSION,
requires={"transport": "http"},
note="MWT exists to key an edge cache; without an edge there is nothing to key.",
),
Capability(
"shapes", "Typed query projection (Shapes)", Tier.EXTENSION,
note="The capability is AFI-common; the binding is per-ORM "
"(django-readers on Django, the project's ORM elsewhere).",
),
Capability(
"forms", "Forms (schema / validate / submit)", Tier.EXTENSION,
note="The capability is AFI-common; the binding is per-framework "
"(Django Forms on Django, Pydantic-or-equivalent elsewhere).",
),
]
# ─── The implementors ─────────────────────────────────────────────────────────
ADAPTERS: list[Adapter] = [
Adapter("django", "Django", "python", "http"),
Adapter("fastapi", "FastAPI", "python", "http"),
Adapter("rust_axum", "Rust / Axum", "rust", "http"),
Adapter("tauri", "Tauri", "rust", "ipc"),
Adapter("typescript", "TypeScript", "typescript", "http"),
]
CAPABILITIES_BY_ID: dict[str, Capability] = {c.id: c for c in CAPABILITIES}
ADAPTERS_BY_ID: dict[str, Adapter] = {a.id: a for a in ADAPTERS}
# Every capability owes exactly one probe in probes.py. The meta-conformance
# test pins this so a capability can't be added without its gate.
PROBES_REQUIRED: int = len(CAPABILITIES)
def applies(capability: Capability, adapter: Adapter) -> bool:
"""Whether `capability` is applicable to `adapter`, from declared properties.
This is the ONLY source of "" in the parity table. A `False` here is a
transport fact (IPC has no HTTP header channel), not a parity verdict.
"""
required_transport = capability.requires.get("transport")
if required_transport is not None and adapter.transport != required_transport:
return False
return True