What to build: The pack manifests can no longer lie. A parity test introspects the live registries and enforces the three checks decided in the design session: (a) every card code registered in any hook registry is marked implemented in a pack manifest; (b) every implemented card either registers hooks or carries an explicit vanilla: true flag — explicit vanilla flags get added to the manifests as part of this; (c) every implemented non-vanilla card has a card_<code>_* test, except cards on a shrink-only grandfather list seeded from today's untested cards.
Blocked by: 19 — Contract: retire the port scaffolding.
Status: fixed
vanilla: true fails CI; manifests carry explicit vanilla flagscard_<code>_* test fails CI unless grandfathered
tests/test_card_manifest_honesty.py (16 tests) is the parity test. It reads
three inputs: the pack manifests, netrunner_engine.registry_inventory() (04's
bridge), and a scan of rust/**/*.rs for #[test] fn card_<code>_*. Python
side per ADR-0001 — Rust reports what it registers, Python compares.
Each check is a pure function of (entries, registered, tested) returning the
offending codes, so the "…fails CI" half of every bullet is exercised against
synthetic entries in the same file rather than only asserted to pass against
today's manifests. Six real-data checks, seven synthetic negatives, three
list-hygiene checks.
(b) and vanilla. No card in sg/elev/vp is vanilla — all 72 implemented
cards register hooks, and none of the 225 manifest entries has ability-free
NRDB text — so there were no vanilla: true flags to add. The flag is
supported and enforced in both directions (implemented-without-hooks needs it;
a card carrying it may not register hooks), and only an explicit true counts:
absent and false both read as non-vanilla. The first genuinely vanilla card
will be the first to carry it.
(c) and the grandfather list.
tests/fixtures/untested_cards_grandfathered.json holds the 71 implemented
cards that have no card_<code>_* test today (everything except Seamless
Launch, 01's pilot). Three properties hold it:
list, which is the mechanism by which it shrinks;
seeded_count: 71 is frozen metadata and the list may notexceed it.
Honest about the limit: nothing can stop someone editing the list file itself, and once it has shrunk the count cap has that much slack. What is machine- checked is that a new untested card never passes silently — it takes a deliberate, reviewable edit to a file whose header says shrink-only.
has_tests was lying. 30030 (Sure Gamble) and 30075 (Hedge Fund) were
has_tests: true, pointing at tests/cards/sg/test_*.py — Python card tests
deleted at the port cutover (982beb5); 30040, the one card that does have
tests, was false. Since the flag is displayed in the card browser and is now
derivable, a fourth check pins it to the source scan and the three flags are
corrected. Not in the ticket's three checks, but "the manifests can no longer
lie" does not stop at the three.
Doc pointers: docs/adding-a-card.md's honesty-check section names the test
file and the grandfather list and spells out the four things landing a card
touches; cards/registry.rs and test_registry_inventory.py stop forward-
referencing "issue 05" and name the test.
ruff check tests/ clean.cargo fmt --all -- --check, `cargo clippy --workspace --all-targets -- -D
warnings, cargo test --workspace` (53 core + 5 AI invariants): all clean.
afterwards):
implemented: false: check (a) fails.implemented: true (no hooks, no test): checks (b) and (c) fail.vanilla: true: the vanilla-with-hooks check fails.30040, which has tests: stale-entry and growth checks fail.has_tests: true on Sure Gamble: the has_tests check fails.card_<code>_* scan has its own non-vacuity guard in the test file
(30040 must be found), so a regex that silently matches nothing cannot make
check (c) pass by default.