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>
168 lines
7.3 KiB
Python
168 lines
7.3 KiB
Python
"""
|
|
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
|