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:
167
tests/afi/manifest.py
Normal file
167
tests/afi/manifest.py
Normal 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
|
||||
Reference in New Issue
Block a user