Restore approved state (tree of 4effcc7 "Added LICENSE")
Roll the working tree back to the last approved shape, before the post-LICENSE span that false-greened the AFI parity matrix with symbol-presence probes and smuggled an unauthorized SQLAlchemy dependency into FastAPI's Shapes binding.
Forward commit, not a history rewrite — the six commits since 4effcc7 stay in the log as the record of what happened.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,7 @@ 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
|
||||
|
||||
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.
|
||||
No channels, no forms, no shapes — those aren't AFI-common.
|
||||
|
||||
`register_fixture()` registers the functions with mizan_core.registry.
|
||||
Backend test apps import this module and call register_fixture() during
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
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))
|
||||
@@ -1,388 +0,0 @@
|
||||
"""
|
||||
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:
|
||||
# Uniform, location-independent: the SSR subprocess bridge is single-sourced
|
||||
# (Python adapters ride `mizan_core.ssr.SSRBridge`); a capability "pass" means
|
||||
# the ADAPTER invokes it — references the bridge / its renderer — over its own
|
||||
# surface, not that a `bridge.py` lives at a fixed path. So the check is the
|
||||
# same for every adapter: an invocation of the SSR renderer in adapter source.
|
||||
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)
|
||||
303
tests/afi/rust_app/Cargo.lock
generated
303
tests/afi/rust_app/Cargo.lock
generated
@@ -39,7 +39,6 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -51,7 +50,6 @@ dependencies = [
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"multer",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
@@ -59,10 +57,8 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -90,33 +86,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -129,51 +104,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -208,23 +138,6 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
@@ -238,49 +151,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -442,10 +323,7 @@ name = "mizan-axum"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
"futures-util",
|
||||
"mizan-core",
|
||||
"multer",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -458,13 +336,10 @@ name = "mizan-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"hmac",
|
||||
"linkme",
|
||||
"mizan-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -477,23 +352,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http",
|
||||
"httparse",
|
||||
"memchr",
|
||||
"mime",
|
||||
"spin",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -535,15 +393,6 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -562,36 +411,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -685,28 +504,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -739,18 +536,6 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -768,26 +553,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
@@ -816,18 +581,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
@@ -892,48 +645,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -955,26 +672,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
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."
|
||||
)
|
||||
Reference in New Issue
Block a user