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

@@ -1,4 +1,4 @@
.PHONY: install test test-core test-django test-fastapi test-react test-afi test-integration docker-up docker-down clean
.PHONY: install test test-core test-django test-fastapi test-react test-afi parity-table parity-check test-integration docker-up docker-down clean
CORE = cores/mizan-python
DJANGO = backends/mizan-django
@@ -30,11 +30,24 @@ test-fastapi:
test-react:
cd $(REACT) && npm test
# AFI conformance — verifies mizan-django and mizan-fastapi emit equivalent
# schemas for the same @client fixture. Substrate-level gate, not e2e.
# AFI conformance — two gates, substrate-level, not e2e:
# test_codegen_parity.py — Django/FastAPI/Rust emit byte-identical KDL IR.
# test_capability_parity.py — every (capability, applicable adapter) pair is
# probed for its wiring. RED on every unwired gap
# by design: that board is the owed work, itemized.
test-afi:
cd $(AFI) && uv run pytest
# Regenerate the README parity table from the live conformance probes. The table
# is generated output — never hand-edited.
parity-table:
cd $(AFI) && uv run python parity_table.py --write
# CI gate: the committed README parity table matches what the probes report.
# Fails on any hand-edit, the same forcing function as the codegen byte-parity.
parity-check:
cd $(AFI) && uv run python parity_table.py --check
# ─── Integration Tests ──────────────────────────────────────────────────────
test-integration: docker-up

125
README.md
View File

@@ -33,107 +33,86 @@ reference implementation; per-adapter support is inventoried below.
## Backend adapters
Every adapter implements the same AFI wire protocol. The matrix below inventories
support per adapter, grouped to separate protocol guarantees from Django-specific
features (forms, ORM projection, auth providers, SSR). A cell counts as supported only
when that adapter wires the capability into its own dispatch surface, not merely that a
shared core primitive exists.
Every adapter implements the same AFI wire protocol. The matrix below is **generated**
from the conformance probes in [`tests/afi/`](tests/afi/) by `make parity-table` — it is
output, not prose. A cell goes `✅` only when that adapter wires the capability into its
own dispatch surface; it cannot be set to "supported" or "Django-only" by editing this
file (a hand-edit fails `python tests/afi/parity_table.py --check` in CI, the same
forcing function the codegen byte-parity tests use).
Legend: ✅ supported · ◑ partial · ❌ not implemented · — not applicable to this transport
Every capability in the matrix is **AFI-common** — each adapter owes a binding, and a
`❌` is a gap on the owed-work board, never a "this framework doesn't do that." The line
between AFI-common and genuinely backend-bound lives in
[`tests/afi/manifest.py`](tests/afi/manifest.py): what sits *outside* the matrix by
design is the `allauth` integration (a Django-ecosystem package) and the per-stack
*bindings* of common capabilities (`django-readers` is Django's Shapes binding; Django
Forms is Django's Forms binding) — the capability is common; the binding is not.
<!-- MIZAN:PARITY:START — generated by tests/afi/parity_table.py; do not edit by hand -->
Legend: ✅ wired · ◑ partial (declared/stubbed) · ❌ gap (AFI-common, owed) · — not applicable to this adapter's transport
Every capability below is **AFI-common**: each adapter owes a binding, and a ❌ is a gap on the owed-work board (`tests/afi/`), never a category. Backend-specific *bindings* of common capabilities (django-readers for Shapes, Django Forms for Forms) and genuinely Django-ecosystem features (allauth) are out of this matrix by design — see `tests/afi/manifest.py` for the line.
### Protocol core
The surface every Mizan adapter implements.
| Capability | Django | FastAPI | Rust / Axum | Tauri | TypeScript |
|---|:---:|:---:|:---:|:---:|:---:|
| RPC call dispatch (`{result, invalidate}`) | ✅ | ✅ | ✅ | ✅ ¹ | ✅ |
| RPC call dispatch (`{result, invalidate}`) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Named-context bundle fetch | ✅ | ✅ | ✅ | ✅ | ✅ |
| Invalidation — JSON body | ✅ | ✅ | ✅ | ✅ | ✅ |
| Invalidation — `X-Mizan-Invalidate` header | ✅ | ✅ | ❌ | — | ✅ |
| Invalidation auto-scoping (three-tier) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Function discovery / registration | ✅ | ✅ | ✅ | ✅ | ✅ |
| Codegen IR export (KDL) | ✅ | ✅ | ✅ | ✅ | ❌ |
| File uploads (multipart, `Upload` type) | ✅ | ✅ | ❌ | ❌ ⁹ | — ¹⁰ |
| Codegen IR export (KDL) | ✅ | ✅ | ✅ | ✅ | ❌ |
| File uploads (`Upload` type) | ✅ | ✅ | ❌ | ❌ | ❌ |
### Edge, cache & enforcement
Protocol transports and guarantees co-equal with the body channel in the spec.
| Capability | Django | FastAPI | Rust / Axum | Tauri | TypeScript |
|---|:---:|:---:|:---:|:---:|:---:|
| Invalidation — `X-Mizan-Invalidate` header | ✅ | ✅ | | — ¹ | ✅ |
| Auth-guard enforcement (`auth=…` rejects) | ✅ | ✅ | ❌ ⁵ | ◑ ⁵ | ✅ ¹¹ |
| Auth-guard enforcement (`auth=…` rejects) | ✅ | ✅ | | | ✅ |
| Origin-side HMAC cache | ✅ | ✅ | ❌ | ❌ | ✅ |
| Edge manifest export | ✅ | ❌ | ❌ | — | ✅ |
| PSR (`render_strategy` in manifest) | ✅ | ❌ | ❌ | — | ✅ |
| Session / CSRF init endpoint | ✅ | ◑ ⁷ | ◑ ⁷ | — | ❌ |
| Session / CSRF init endpoint | ✅ | ✅ | ✅ | — | ❌ |
> **Caveat:** Rust/Axum and Tauri accept `auth=` on a function but do not yet enforce
> it — do not rely on `auth=` for access control on those adapters.
>
> Django, FastAPI, and TypeScript share one auth/invalidation/cache implementation
> (`mizan_core` for the Python adapters; the same spec, pinned cross-language, for TS).
### Stack extensions (Django)
Django ecosystem features Mizan wraps. Other adapters provide these only where the
target stack calls for them.
### Extension points
| Capability | Django | FastAPI | Rust / Axum | Tauri | TypeScript |
|---|:---:|:---:|:---:|:---:|:---:|
| WebSocket channels (declared transport) | ✅ | ❌ | ◑ ² | ❌ | ❌ |
| Forms (schema / validate / submit) | ✅ | ❌ | ◑ ³ | ❌ | ❌ |
| Formsets | ✅ | ❌ | ❌ | ❌ | ❌ |
| API shapes (ORM query projection) ⁴ | ✅ | | | — | |
| JWT auth (access / refresh) ¹² | ✅ | ✅ | ❌ | ❌ | ◑ ¹³ |
| MWT (edge identity token) | ✅ | ✅ | ❌ | | ◑ ¹³ |
| SSR bridge | ✅ | ❌ | ❌ | — | ❌ |
| Auth-provider integration (allauth) | ✅ | ❌ | ❌ | ❌ | ❌ |
| WebSocket transport (`websocket=` declared) | ✅ | ❌ | ◑ | ❌ | ❌ |
| SSR bridge (subprocess renderer) | ✅ | ❌ | | ❌ | ❌ |
| JWT auth (access / refresh) | ✅ | ✅ | ❌ | ❌ | |
| MWT (edge identity token) | ✅ | | | — | |
| Typed query projection (Shapes) | ✅ | ❌ | ❌ | ❌ | ❌ |
| Forms (schema / validate / submit) | ✅ | ❌ | | ◑ | ❌ |
**Notes**
1. Tauri's transport is Tauri IPC (a single `#[tauri::command]` envelope), not HTTP.
Invalidation rides in the JSON response body; there is no header channel.
2. Rust/Axum declares `Transport::Websocket` in the IR/macro but routes no Axum
WebSocket handler yet.
3. Rust/Axum carries `is_form`/`form_role` trait stubs but no validate/submit endpoint.
4. "API shapes" is Django's django-readers queryset projection — ORM-coupled. Every
adapter carries typed input/output through the KDL IR; the projection primitive
itself is Django-only.
5. Tauri's `FunctionSpec` carries `auth`/`private` fields; the dispatch path does not
enforce them. Rust/Axum has no enforcement either.
6. Rust/Axum and Tauri are the IR authority via the `#[mizan::client]` macro + linkme
registry; the codegen links the crate directly (`build_ir()` / the `export-ir` bin)
rather than fetching over HTTP.
7. FastAPI and Rust/Axum expose `GET /session/` returning a null CSRF token for wire
parity; CSRF is Django-only.
8. `mizan-ts` emits the Edge manifest (JSON) but has no KDL IR emitter, so it can't yet
feed the codegen — an unbuilt gap. A TypeScript backend still needs the generated
client (types + `callXxx`/`fetchXxx` + framework hooks); same language doesn't remove
the need for it.
9. The `mizan-codegen` crate parses the `upload` KDL node and emits the field across
targets (the Rust target lowers it to `Vec<u8>`). Multipart dispatch binding is wired
for Django and FastAPI only; the Rust/Axum and Tauri *adapters* have no upload concept
at dispatch yet.
10. The TypeScript column is the `mizan-ts` backend adapter, which has no upload
dispatch. The matching client side lives in the kernel (`@mizan/base`): `mizanCall`
auto-switches to `multipart/form-data` when any argument is a `File`.
11. `mizan-ts` dispatch now enforces `auth=` (`true`/`'staff'`/`'superuser'`/predicate)
against a host-supplied `Identity`, byte-matching the Python guard's denial messages.
12. JWT/MWT token logic is single-sourced in `mizan_core.auth`; Django and FastAPI ride
it. Session-validation (immediate-logout revocation) is Django-only — FastAPI mints
from its own credential check.
13. `mizan-ts` ships an optional `decodeMwt`/`decodeJwtBearer`/`identityFromMwt` helper
(HS256 via Node `crypto`, cross-language pin-tested against a Python-minted MWT) so a
TS edge worker can derive `Identity` from a Python-issued token. Identity source stays
host-supplied; `mizan-ts` does not mint from a session.
- **Invalidation — `X-Mizan-Invalidate` header** — The header channel is co-equal with the body channel in the spec. IPC transports carry invalidation in the response envelope instead.
- **Edge manifest export** — The manifest configures an HTTP/CDN edge; a desktop IPC shell has no edge.
- **MWT (edge identity token)** — MWT exists to key an edge cache; without an edge there is nothing to key.
- **Typed query projection (Shapes)** — The capability is AFI-common; the binding is per-ORM (django-readers on Django, the project's ORM elsewhere).
- **Forms (schema / validate / submit)** — The capability is AFI-common; the binding is per-framework (Django Forms on Django, Pydantic-or-equivalent elsewhere).
<!-- MIZAN:PARITY:END -->
## Conformance
Adapter parity is gated by the AFI conformance suite in [`tests/afi/`](tests/afi/). It
currently asserts **IR-shape parity** — the same fixture through Django, FastAPI, and
the Rust adapter emits byte-identical KDL (`test_codegen_parity.py`). Per-capability
runtime assertions (header transport, `auth=` enforcement, cache behavior) are planned.
Adapter parity is gated by the AFI conformance suite in [`tests/afi/`](tests/afi/), at
two layers:
- **IR-shape parity** (`test_codegen_parity.py`) — Django, FastAPI, and the Rust adapter
emit byte-identical KDL for the same registered fixture. The IR is the contract; the
language that wrote the backend is irrelevant to the codegen-facing artifact.
- **Capability parity** (`test_capability_parity.py`) — every `(capability, applicable
adapter)` pair declared in `manifest.py` is probed for its actual wiring (`probes.py`).
A gap is a **red test that names the owed binding**, not a footnote. The suite is
intentionally red wherever a capability is unwired: that redness is the owed-work
board, itemized and loud, and a gap turns green by being *wired*, never by being
*described*. This is the per-capability gate the roadmap previously deferred.
The generated table above is rendered from the capability layer, and the `--check`
diff keeps the README honest to the probes on every CI run.
## License

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)

View File

@@ -1,12 +1,16 @@
"""
Mizan core registry — function and composition registration with an
extension hook for backend-specific registries (channels, forms, etc.)
to plug into.
extension hook for the AFI-common capabilities that need their own
sub-registry (channels/WebSocket, forms, shapes) to plug into.
This is the framework-agnostic registry. Backends own their own
type-specific registries (channels in Django Channels, forms in Django
Forms, websockets in FastAPI, etc.) and register them as extensions
here so the unified schema export can include them.
This is the framework-agnostic registry. The extension points
(channels, forms, websockets, shapes) are AFI-common: every adapter owes
a binding for each, and registers it here so the unified schema export
sees it. Django binds all of them today; the other adapters' unbound
extensions are gaps tracked by the capability-parity suite in
`tests/afi/`, not framework-specific features. The binding is per-stack
(Django Channels vs. native WebSocket, Django Forms vs. Pydantic); the
capability is common.
"""
from __future__ import annotations

View File

@@ -7,7 +7,11 @@ exercise the protocol axes both backends must agree on:
- two context functions sharing a param (proves bundling + param elevation)
- a mutation declaring `affects` on the context
No channels, no forms, no shapes — those aren't AFI-common.
This fixture is deliberately minimal: it exercises the axes the *IR-shape*
parity test (`test_codegen_parity.py`) needs to compare. It intentionally omits
channels / forms / shapes — NOT because those are outside the AFI (they are
AFI-common; see `manifest.py`), but because their per-adapter wiring is gated by
the *capability* parity suite (`test_capability_parity.py`), not by IR-shape.
`register_fixture()` registers the functions with mizan_core.registry.
Backend test apps import this module and call register_fixture() during

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

141
tests/afi/parity_table.py Normal file
View File

@@ -0,0 +1,141 @@
"""
Generate the README parity table from the conformance probes.
The table in the README is *output*, not *input*. It is computed by running
every probe over every adapter and rendering the result. An agent can no longer
type "Django-only" into a cell, because no one types cells — `make parity-table`
overwrites the block between the markers, and `--check` fails CI if the
committed block has drifted from what the probes actually report (the same
forcing function the codegen byte-parity tests already use).
Glyphs:
✅ wired the probe found the artifact
◑ partial declared or stubbed, not complete — counts as RED in the suite
❌ gap AFI-common, owed, not wired
— n/a the capability does not exist over this adapter's transport
(derived from `manifest.applies`, never asserted)
Usage:
python parity_table.py --write # regenerate the README block
python parity_table.py --check # exit 1 if README block is stale
python parity_table.py # print the block to stdout
"""
from __future__ import annotations
import sys
from pathlib import Path
from manifest import ADAPTERS, CAPABILITIES, Tier, applies
from probes import run_probe
README = Path(__file__).resolve().parents[2] / "README.md"
START = "<!-- MIZAN:PARITY:START — generated by tests/afi/parity_table.py; do not edit by hand -->"
END = "<!-- MIZAN:PARITY:END -->"
_GLYPH = {"pass": "", "partial": "", "fail": ""}
def _cell(cap, adapter) -> str:
if not applies(cap, adapter):
return ""
return _GLYPH[run_probe(cap.id, adapter).state]
def _tier_table(tier: Tier) -> str:
caps = [c for c in CAPABILITIES if c.tier is tier]
if not caps:
return ""
header = "| Capability | " + " | ".join(a.title for a in ADAPTERS) + " |"
sep = "|---|" + "|".join(":---:" for _ in ADAPTERS) + "|"
rows = []
for cap in caps:
cells = " | ".join(_cell(cap, a) for a in ADAPTERS)
rows.append(f"| {cap.title} | {cells} |")
return f"### {tier.value}\n\n{header}\n{sep}\n" + "\n".join(rows)
def _notes() -> str:
noted = [c for c in CAPABILITIES if c.note]
if not noted:
return ""
lines = ["**Notes**", ""]
for c in noted:
lines.append(f"- **{c.title}** — {c.note}")
return "\n".join(lines)
def generate_block() -> str:
"""The full generated parity block: legend, one table per tier, notes."""
legend = (
"Legend: ✅ wired · ◑ partial (declared/stubbed) · ❌ gap (AFI-common, owed) · "
"— not applicable to this adapter's transport\n\n"
"Every capability below is **AFI-common**: each adapter owes a binding, and a "
"❌ is a gap on the owed-work board (`tests/afi/`), never a category. "
"Backend-specific *bindings* of common capabilities (django-readers for Shapes, "
"Django Forms for Forms) and genuinely Django-ecosystem features (allauth) are "
"out of this matrix by design — see `tests/afi/manifest.py` for the line."
)
parts = [legend]
for tier in (Tier.PROTOCOL_CORE, Tier.EDGE_CACHE, Tier.EXTENSION):
table = _tier_table(tier)
if table:
parts.append(table)
notes = _notes()
if notes:
parts.append(notes)
return "\n\n".join(parts)
def _wrap(block: str) -> str:
return f"{START}\n{block}\n{END}"
def _splice(readme_text: str, block: str) -> str:
if START not in readme_text or END not in readme_text:
raise SystemExit(
f"README markers not found. Add a block bounded by:\n {START}\n {END}\n"
f"to {README} where the parity table should render."
)
pre = readme_text.split(START)[0]
post = readme_text.split(END)[1]
return pre + _wrap(block) + post
def main(argv: list[str]) -> int:
block = generate_block()
mode = argv[1] if len(argv) > 1 else "--print"
if mode == "--print":
print(block)
return 0
text = README.read_text(encoding="utf-8")
spliced = _splice(text, block)
if mode == "--write":
if spliced != text:
README.write_text(spliced, encoding="utf-8")
print(f"Wrote parity table to {README}")
else:
print("Parity table already current.")
return 0
if mode == "--check":
if spliced != text:
print(
"README parity table is STALE. The committed table does not match what "
"the conformance probes report.\nRun: make parity-table",
file=sys.stderr,
)
return 1
print("README parity table is current.")
return 0
print(f"Unknown mode: {mode}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv))

385
tests/afi/probes.py Normal file
View File

@@ -0,0 +1,385 @@
"""
Capability probes — the adversarial half of the conformance gate.
A probe answers one question about one (capability, adapter) pair: *is this
capability actually wired into this adapter?* It answers by inspecting the
backend's own source for the concrete artifact the capability requires — a
route registration, an exported renderer, a management command, a call into the
shared core. It never asks the adapter to self-report (a self-report is the
thing that drifts), and it never reads a parity table (that would be circular).
The single invariant: **a probe goes green only when the wiring exists.** You
cannot flip a cell by editing prose, a docstring, or a README — only by adding
the route / module / binding the probe looks for.
Two facts learned by building this and watching it lie, now load-bearing:
1. Comments are the false-positive engine. A doc-comment that says "the JS side
re-wraps it into a Promise.reject" made an auth-enforcement probe pass on the
word `reject`. So source is comment-stripped per language before matching,
and patterns target code identifiers (`MizanError::Unauthorized`, a route
macro, an exported function name), never prose-able words.
2. AFI-common logic lives in the shared core; the adapter rides it. FastAPI's
JWT/MWT support *is* `mizan_core.auth.authenticate` — there is no `jwt`
literal in the FastAPI adapter, because the adapter delegates. Axum's KDL IR
lives in `cores/mizan-rust`, not the axum crate. So capabilities that are
genuinely core-provided (`jwt`, `mwt`, `ir_export`) probe the BACKEND scope
(adapter + its language core); capabilities the adapter must wire to its own
transport (the header on the response, the route, the manifest command, the
WebSocket handler) probe the ADAPTER scope only.
Depth. These are *source-surface* probes: they confirm the artifact is present,
not that it behaves correctly under load. Runtime-behavior depth (mount, drive,
assert the wire bytes) is owed per adapter and lands with each gap's closure —
the runtime probe is written alongside the wiring it verifies. Surface-presence
is the floor that makes de-scope impossible; runtime is the ceiling each closure
raises, and the gap between them is named here, not hidden.
`partial` (◑) is a real state: Axum declares `Transport::Websocket` in the IR
but routes no handler; mizan-ts decodes a JWT but mints none. It renders ◑ — and
the conformance suite still treats it as RED, because the suite asserts
`state == "pass"`. ◑ is visible honesty, never a shippable-green hiding place.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Callable, Literal
from manifest import Adapter
REPO_ROOT = Path(__file__).resolve().parents[2]
BACKENDS = REPO_ROOT / "backends"
CORES = REPO_ROOT / "cores"
# Each adapter's own source root. Probes scoped "adapter" read only here.
ADAPTER_ROOTS: dict[str, Path] = {
"django": BACKENDS / "mizan-django" / "src" / "mizan",
"fastapi": BACKENDS / "mizan-fastapi" / "src" / "mizan_fastapi",
"rust_axum": BACKENDS / "mizan-rust-axum" / "src",
"tauri": BACKENDS / "mizan-tauri" / "src",
"typescript": BACKENDS / "mizan-ts" / "src",
}
# The shared core each language's adapters ride. Probes scoped "backend" read
# the adapter root PLUS these — that is where core-provided AFI-common logic
# (token minting, KDL IR) actually lives.
CORE_ROOTS: dict[str, list[Path]] = {
"python": [CORES / "mizan-python" / "src" / "mizan_core"],
"rust": [CORES / "mizan-rust" / "src", CORES / "mizan-rust-macros" / "src"],
"typescript": [], # mizan-ts is self-contained; no shared core
}
_SOURCE_EXTS = {".py", ".rs", ".ts", ".tsx"}
_SKIP_DIRS = {"node_modules", "target", "__pycache__", "dist", ".venv"}
State = Literal["pass", "partial", "fail"]
@dataclass(frozen=True)
class ProbeResult:
state: State
detail: str
# ─── Source collection + comment stripping ────────────────────────────────────
def _strip_comments(text: str, language: str) -> str:
"""Remove comments so prose can't satisfy a code-artifact pattern.
Imperfect by design (a `#` inside a string may be clipped); the patterns are
code identifiers that live outside strings, and the cost of a rare false
strip is far below the cost of a comment-driven false `pass`.
"""
if language in ("rust", "typescript"):
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL) # block /* */ (incl. /** */)
text = re.sub(r"(?<!:)//[^\n]*", "", text) # line // (keep URL `://`)
elif language == "python":
text = re.sub(r'""".*?"""', "", text, flags=re.DOTALL) # triple-quoted
text = re.sub(r"'''.*?'''", "", text, flags=re.DOTALL)
text = re.sub(r"#[^\n]*", "", text) # line #
return text
def _read_roots(roots: tuple[Path, ...], language: str) -> str:
chunks: list[str] = []
for root in roots:
if not root.exists():
continue
for path in sorted(root.rglob("*")):
if not (path.is_file() and path.suffix in _SOURCE_EXTS):
continue
if any(part in _SKIP_DIRS for part in path.parts):
continue
chunks.append(_strip_comments(path.read_text(encoding="utf-8", errors="replace"), language))
return "\n".join(chunks)
@lru_cache(maxsize=None)
def _adapter_text(adapter_id: str, language: str) -> str:
return _read_roots((ADAPTER_ROOTS[adapter_id],), language)
@lru_cache(maxsize=None)
def _backend_text(adapter_id: str, language: str) -> str:
roots = (ADAPTER_ROOTS[adapter_id], *CORE_ROOTS.get(language, []))
return _read_roots(tuple(roots), language)
def _adapter(a: Adapter) -> str:
return _adapter_text(a.id, a.language)
def _backend(a: Adapter) -> str:
return _backend_text(a.id, a.language)
def _has_path(adapter_id: str, *relparts: str) -> bool:
return ADAPTER_ROOTS[adapter_id].joinpath(*relparts).exists()
def _hit(text: str, pattern: str) -> bool:
return re.search(pattern, text) is not None
def _wired(found: bool, what: str) -> ProbeResult:
return ProbeResult("pass", f"wired: {what}") if found else ProbeResult("fail", f"missing: {what}")
# ─── Per-capability detection ─────────────────────────────────────────────────
#
# Patterns are grounded in artifacts read directly from each adapter:
# django urls.py `path("call/", ...)`, channels/ ssr/ shapes/ forms/ dirs
# fastapi router.py `@router.post("/call/")`, header insert, /session/
# axum handlers.rs `function_call`, `compute_invalidation`, no header/cache
# tauri lib.rs `mizan_invoke`, Envelope::Call/Fetch, no auth/cache/ws
# ts dispatch.ts `handleMutationCall`, checkAuth, token.ts decode-only
def _probe_rpc_call(a: Adapter) -> ProbeResult:
pat = {
"python": r'path\("call/|/call/|function_call',
"rust": r"function_call|mizan_invoke|Envelope::Call",
"typescript": r"handleMutationCall",
}[a.language]
return _wired(_hit(_adapter(a), pat), "RPC call dispatch")
def _probe_context_bundle(a: Adapter) -> ProbeResult:
pat = {
"python": r"context_fetch|/ctx/|ctx/<",
"rust": r"context_fetch|handle_fetch|Envelope::Fetch",
"typescript": r"handleContextFetch",
}[a.language]
return _wired(_hit(_adapter(a), pat), "named-context bundle fetch")
def _probe_invalidate_body(a: Adapter) -> ProbeResult:
pat = {
"python": r'"invalidate"|invalidate=|\binvalidate\b',
"rust": r"invalidate|InvalidationTarget",
"typescript": r"\.invalidate\b|invalidate:",
}[a.language]
return _wired(_hit(_adapter(a), pat), "`invalidate` key in response body")
def _probe_invalidate_header(a: Adapter) -> ProbeResult:
return _wired(_hit(_adapter(a), r"X-Mizan-Invalidate"), "`X-Mizan-Invalidate` header emission")
def _probe_invalidate_autoscope(a: Adapter) -> ProbeResult:
# The adapter wires auto-scoping by routing through the shared resolver.
pat = {
"python": r"resolve_invalidation|dispatch_call|dispatch_context",
"rust": r"compute_invalidation",
"typescript": r"resolveInvalidation",
}[a.language]
return _wired(_hit(_adapter(a), pat), "three-tier invalidation auto-scoping (shared resolver)")
def _probe_registration(a: Adapter) -> ProbeResult:
pat = {
"python": r"\bregister\b|get_function|registry",
"rust": r"FUNCTIONS|lookup_function|linkme",
"typescript": r"getFunction|\bregister\b|registry",
}[a.language]
return _wired(_hit(_adapter(a), pat), "function discovery / registration")
def _probe_ir_export(a: Adapter) -> ProbeResult:
# Core-provided: python build_ir/export cmd (adapter), rust kdl.rs/ir.rs (core).
if a.language == "python":
return _wired(_hit(_adapter(a), r"build_ir|export_mizan_ir"), "KDL IR export")
if a.language == "rust":
return _wired(_hit(_backend(a), r"to_kdl|emit_kdl|fn build_ir|kdl::"), "KDL IR export (rust core)")
# TypeScript: emits the edge manifest (JSON), but no codegen KDL IR (note 8).
if _hit(_adapter(a), r"to_kdl|emitKdl|buildIr|\bkdl\b"):
return ProbeResult("pass", "wired: KDL IR emitter")
return ProbeResult("fail", "missing: KDL IR emitter (manifest.ts emits edge JSON, not codegen IR)")
def _probe_upload(a: Adapter) -> ProbeResult:
pat = {
"python": r"bind_uploads|UploadedFile|multipart",
"rust": r"Multipart|multipart|bind_uploads",
"typescript": r"FormData|multipart",
}[a.language]
return _wired(_hit(_adapter(a), pat), "multipart / Upload-type dispatch binding")
def _probe_auth_enforcement(a: Adapter) -> ProbeResult:
if a.language == "python":
return _wired(_hit(_adapter(a), r"enforce_auth|authenticate\(|authguard"), "auth= enforcement")
if a.language == "typescript":
return _wired(_hit(_adapter(a), r"checkAuth|authDenial|AuthDenial"), "auth= enforcement")
# Rust adapters (axum, tauri): dispatch must reject on auth=.
if _hit(_adapter(a), r"Unauthorized|Forbidden|enforce_auth"):
return ProbeResult("pass", "wired: auth enforcement in dispatch")
if _hit(_backend(a), r"\bauth\b|is_private|\bprivate\b"):
return ProbeResult("partial", "auth/private carried in FunctionSpec; dispatch does not reject")
return ProbeResult("fail", "missing: auth= enforcement")
def _probe_origin_cache(a: Adapter) -> ProbeResult:
pat = {
"python": r"CacheOrchestrator|derive_cache_key|cfg\.cache|cache_get|cache_put",
"rust": r"derive_cache_key|CacheBackend|origin_cache",
"typescript": r"cacheGet|cachePut|deriveCacheKey",
}[a.language]
return _wired(_hit(_backend(a), pat), "origin-side HMAC cache")
def _probe_edge_manifest(a: Adapter) -> ProbeResult:
pat = {
"python": r"export_edge_manifest|generate_edge_manifest|edge_manifest",
"rust": r"edge_manifest|generate_manifest",
"typescript": r"generateManifest|EdgeManifest",
}[a.language]
return _wired(_hit(_adapter(a), pat), "edge manifest export")
def _probe_psr(a: Adapter) -> ProbeResult:
return _wired(_hit(_adapter(a), r"render_strategy|renderStrategy"), "PSR render_strategy")
def _probe_session_init(a: Adapter) -> ProbeResult:
pat = {
"python": r"session/|session_init",
"rust": r"session_init|/session/",
"typescript": r"sessionInit|/session/",
}[a.language]
return _wired(_hit(_adapter(a), pat), "session / CSRF init endpoint")
def _probe_websocket(a: Adapter) -> ProbeResult:
if a.id == "django":
return _wired(_has_path(a.id, "channels"), "Django Channels consumer")
if a.id == "fastapi":
return _wired(_hit(_adapter(a), r"WebSocket|@router\.websocket|websocket_route"), "FastAPI WebSocket route")
if a.id == "rust_axum":
if _hit(_adapter(a), r"WebSocketUpgrade|on_upgrade|ws_handler"):
return ProbeResult("pass", "wired: Axum WebSocket handler")
if _hit(_backend(a), r"Websocket|WebSocket"):
return ProbeResult("partial", "Transport::Websocket declared in IR; no Axum handler routed")
return ProbeResult("fail", "missing: WebSocket transport")
if a.id == "tauri":
return _wired(_hit(_adapter(a), r"subscription|Subscribe|emit_to|Channel<"), "IPC subscription channel")
return _wired(_hit(_adapter(a), r"WebSocket|websocket"), "WebSocket transport")
def _probe_ssr_bridge(a: Adapter) -> ProbeResult:
if a.id == "django":
return _wired(_has_path(a.id, "ssr", "bridge.py"), "Bun SSR subprocess bridge")
return _wired(_hit(_adapter(a), r"SSRBridge|renderToString|ssr_bridge"), "SSR bridge (subprocess renderer)")
def _probe_jwt(a: Adapter) -> ProbeResult:
# Core-provided for Python (mizan_core.auth); TS has decode-only helpers.
if a.language == "python":
return _wired(_hit(_backend(a), r"\bjwt\b|JWT|def authenticate"), "JWT auth (rides mizan_core.auth)")
if a.id == "typescript":
if _hit(_adapter(a), r"signJwt|mintJwt|createJwt|encodeJwt"):
return ProbeResult("pass", "wired: JWT mint + verify")
if _hit(_adapter(a), r"decodeJwt|decodeJwtBearer"):
return ProbeResult("partial", "decode-only helper; mints no tokens")
return ProbeResult("fail", "missing: JWT auth")
return _wired(_hit(_backend(a), r"\bjwt\b|JWT"), "JWT auth (access / refresh)")
def _probe_mwt(a: Adapter) -> ProbeResult:
if a.language == "python":
return _wired(_hit(_backend(a), r"\bmwt\b|MWT|X-Mizan-Token"), "MWT (edge identity token)")
if a.id == "typescript":
if _hit(_adapter(a), r"signMwt|mintMwt|createMwt"):
return ProbeResult("pass", "wired: MWT mint")
if _hit(_adapter(a), r"decodeMwt|identityFromMwt"):
return ProbeResult("partial", "decode-only helper; mints no tokens")
return ProbeResult("fail", "missing: MWT")
return _wired(_hit(_backend(a), r"\bmwt\b|MWT"), "MWT (edge identity token)")
def _probe_shapes(a: Adapter) -> ProbeResult:
if a.id == "django":
return _wired(_has_path(a.id, "shapes"), "django-readers Shapes binding")
pat = {
"python": r"django_readers|QuerysetProjection|shapes\.",
"rust": r"shapes::|QueryProjection",
"typescript": r"QueryProjection|shapesBinding",
}[a.language]
return _wired(_hit(_adapter(a), pat), "typed query-projection binding")
def _probe_forms(a: Adapter) -> ProbeResult:
if a.id == "django":
return _wired(_has_path(a.id, "forms"), "Django Forms binding (schema / validate / submit)")
if a.language == "rust":
if _hit(_adapter(a), r"form_submit|form_validate|FormRole::Submit"):
return ProbeResult("pass", "wired: form validate/submit endpoint")
if _hit(_backend(a), r"is_form|form_role|FormRole"):
return ProbeResult("partial", "is_form/form_role carried; no validate/submit endpoint")
return ProbeResult("fail", "missing: forms")
pat = {
"python": r"FormSchema|form_role|forms\.",
"typescript": r"FormSchema|formRole|formSubmit",
}[a.language]
return _wired(_hit(_adapter(a), pat), "forms (schema / validate / submit)")
PROBES: dict[str, Callable[[Adapter], ProbeResult]] = {
"rpc_call": _probe_rpc_call,
"context_bundle": _probe_context_bundle,
"invalidate_body": _probe_invalidate_body,
"invalidate_header": _probe_invalidate_header,
"invalidate_autoscope": _probe_invalidate_autoscope,
"registration": _probe_registration,
"ir_export": _probe_ir_export,
"upload": _probe_upload,
"auth_enforcement": _probe_auth_enforcement,
"origin_cache": _probe_origin_cache,
"edge_manifest": _probe_edge_manifest,
"psr": _probe_psr,
"session_init": _probe_session_init,
"websocket": _probe_websocket,
"ssr_bridge": _probe_ssr_bridge,
"jwt": _probe_jwt,
"mwt": _probe_mwt,
"shapes": _probe_shapes,
"forms": _probe_forms,
}
def run_probe(capability_id: str, adapter: Adapter) -> ProbeResult:
"""Run the probe for one (capability, adapter) pair."""
probe = PROBES.get(capability_id)
if probe is None:
raise KeyError(
f"No probe registered for capability '{capability_id}'. Every capability "
f"in manifest.CAPABILITIES owes a probe — add one to PROBES."
)
return probe(adapter)

View File

@@ -0,0 +1,118 @@
"""
AFI capability parity — the runtime/surface conformance gate.
`test_codegen_parity.py` gates that the three backends emit byte-identical KDL.
That is necessary but narrow: it proves the IR agrees, not that an adapter
actually *implements* the capabilities the IR describes. The vacuum left by
"IR-shape only" is exactly where parity drifted — an adapter that never wired
SSR or WebSocket got its gap relabelled "Django-only" or "out of scope," and
nothing in the suite objected.
This module closes that vacuum. It parametrizes over every (capability,
applicable-adapter) pair drawn from `manifest.py` and asserts the adapter
actually wires the capability (`probes.py`). It is designed to be RED wherever a
gap is real — each failure names one owed binding. That redness is not a broken
build; it is the board of owed work, itemized and loud, that the prior prose
table hid behind false-green. A gap turns green by being *wired*, never by being
*described*.
Applicability is derived in `manifest.applies()` from the adapter's declared
transport, so a capability that simply does not exist over a transport (header
invalidation over Tauri IPC) is not parametrized here at all — it is a "" in
the generated table, computed, not a verdict anyone typed.
"""
from __future__ import annotations
import pytest
from manifest import ADAPTERS, CAPABILITIES, CAPABILITIES_BY_ID, PROBES_REQUIRED, applies
from probes import PROBES, run_probe
def _applicable_pairs() -> list[tuple[str, str, str]]:
"""(capability_id, adapter_id, test_id) for every pair the protocol applies to."""
pairs: list[tuple[str, str, str]] = []
for cap in CAPABILITIES:
for adapter in ADAPTERS:
if applies(cap, adapter):
pairs.append((cap.id, adapter.id, f"{cap.id}::{adapter.id}"))
return pairs
_PAIRS = _applicable_pairs()
@pytest.mark.parametrize(
"capability_id,adapter_id",
[(c, a) for c, a, _ in _PAIRS],
ids=[tid for _, _, tid in _PAIRS],
)
def test_adapter_wires_capability(capability_id: str, adapter_id: str) -> None:
"""The adapter must wire the AFI-common capability the protocol declares.
A failure here is one owed binding, not a regression. The message names the
capability, the adapter, and what the probe could not find — that string is
the gap's specification until someone closes it by wiring the artifact.
"""
adapter = next(a for a in ADAPTERS if a.id == adapter_id)
cap = CAPABILITIES_BY_ID[capability_id]
result = run_probe(capability_id, adapter)
assert result.state == "pass", (
f"AFI parity gap — {adapter.title} does not wire '{cap.title}'.\n"
f" probe: {result.detail}\n"
f" state: {result.state}"
+ (" (◑ partial — declared/stubbed but not complete)" if result.state == "partial" else "")
+ f"\n This capability is AFI-common (manifest tier: {cap.tier.value}); every "
f"adapter owes a binding. Close it by wiring the artifact the probe looks for — "
f"not by editing a table."
)
# ─── Meta-conformance: the manifest and the probes must stay in lockstep ───────
def test_every_capability_has_a_probe() -> None:
"""No capability may be declared without a probe — else it is unverifiable
and silently 'passes' by never being checked, recreating the original hole."""
missing = [c.id for c in CAPABILITIES if c.id not in PROBES]
assert not missing, (
f"Capabilities declared in manifest.py with no probe in probes.py: {missing}. "
f"An unprobed capability is an un-gated parity claim — exactly the drift this "
f"suite exists to prevent."
)
def test_no_orphan_probes() -> None:
"""No probe may exist for a capability the manifest doesn't declare — that
would be dead detection code drifting from the surface it claims to check."""
orphans = [pid for pid in PROBES if pid not in CAPABILITIES_BY_ID]
assert not orphans, (
f"Probes in probes.py with no matching capability in manifest.py: {orphans}."
)
def test_probe_count_matches_required() -> None:
"""Sanity pin: the manifest's own count of required probes equals the probe set."""
assert len(PROBES) == PROBES_REQUIRED, (
f"probes.py defines {len(PROBES)} probes; manifest expects {PROBES_REQUIRED}."
)
def test_readme_parity_table_is_current() -> None:
"""The README parity table is generated output; a hand-edit must fail here.
This is the lock that makes the original lie inexpressible. The table can no
longer be edited to read 'Django-only' — it is spliced from the probe results
by `parity_table.py`, and this test asserts the committed block matches a fresh
regeneration. Drift → red → `make parity-table`.
"""
import parity_table
text = parity_table.README.read_text(encoding="utf-8")
regenerated = parity_table._splice(text, parity_table.generate_block())
assert regenerated == text, (
"README parity table is stale or hand-edited. It is generated from the "
"conformance probes — run `make parity-table` to regenerate it."
)