Status: fixed
What to fix: The cards page shows "N/A" in the Tests and Manual columns for every identity, even though all 7 implemented sg identities have real card_<code>_* Rust tests and has_tests: true in game/cards_impl/sg.json. The manifest is honest; the page hard-codes the lie in static/cards.js:94-97:
const isIdentity = c.type === 'identity';
const impl = isIdentity && !c.implemented ? null : c.implemented;
const tests = isIdentity ? null : c.has_tests;
const manual = isIdentity ? null : c.tested_manually;
This is a leftover from before identities had ability hooks.
Fix (agreed):
has_tests / tested_manually values for identities, exactly like every other card type.vanilla: true (e.g. The Catalyst 30076, The Syndicate 30077), whose definition of done exempts tests (docs/adding-a-card.md). If the vanilla flag isn't in the API payload the page consumes, pass it through.Definition of done: on the cards page, sg identities show Tests: Yes / Manual: No matching the manifest; vanilla cards show N/A; unimplemented identities show Implemented: No.
Fixed in 73ba1ea ("fix: report real identity test status on the cards page"), which landed the code before this ticket file was committed. Verified against the running app's payload rather than the diff alone:
/api/cards as implemented/has_tests/tested_manually = true/true/false and render() now passes all three to badge() unfiltered → Tests: Yes, Manual: No.vanilla: true through the endpoint; testStatus() keys the N/A on that flag (c.vanilla === true, never implicit), so only they blank the two test columns.implemented: false and render "No" — the Implemented column has no N/A path left, and their titles mute like any other unimplemented card.tests/test_api.py (payload keeps the manifest fields, vanilla passes through, never implicit) and tests/test_static_pages.py (render() no longer mentions identity, only the two test columns go through testStatus, .badge-identity renamed .badge-na).
Both suites green at baseline: cargo test --workspace 241 passed, pytest -q 199 passed.
Left alone deliberately, out of this ticket's scope — two older identity special cases survive in static/cards.js and are not part of the columns this ticket covers:
applyFilters(): status === 'not-implemented' still filters with !c.implemented && c.type !== 'identity', so the 17 unimplemented identities never appear under the "Not implemented" filter even though the table now reports them honestly.buildSummary(): the implemented/total chips are computed over c.type !== 'identity', so identities are missing from both the numerator and the denominator.
Both date to the original cards page (314dc7b), same vintage as the column lie, and both are a product call about what the filter and the coverage counters should mean now that identities are implementable. Worth a follow-up ticket.
Review follow-up — a 3-model panel read 73ba1ea and found the fix real but under-covered and undeliverable. All four findings addressed:
tests/test_static_pages.py asserted that cards.js contains certain substrings, so transposing the two testStatus(c, ...) calls, or restoring c.type === 'identity' inside testStatus(), kept every assertion green — the exact regression this ticket fixed was uncovered. With no JS runtime in this repo (no node, no engine in the venv), the column decision moved to where Python can run it: status_columns() in server/routers/cards.py returns the three cells in table order and /api/cards ships them as status_columns. statusCells() in cards.js now maps that list to badges and decides nothing. tests/test_api.py drives the function with API-shaped cards (implemented sg identity, vanilla identity, unimplemented identity, ordinary program) and asserts the triples. Both mutations above now fail 3 and 4 tests respectively./cards versioned only style.css, so a browser that had been there before kept the old cards.js — and its N/A columns — against the new HTML and CSS. _versioned() in server/routers/pages.py now stamps every /static/*.css and /static/*.js a page pulls in, and all four page routes go through it. Covered per page in tests/test_static_pages.py."identity" not in body (which a comment or a future label would trip) is replaced by comment-stripped checks that the cells never branch on c.type and never recompute has_tests/tested_manually/vanilla. The three copies of re.search(rf"function {name}\(.*?\n}}") — which only worked while every closing brace sat at column 0 — are one _js_fn() that matches braces, skipping comments, strings and template literals, and it has its own tests. Verified it returns byte-identical source to the old regex at all 13 existing call sites.
Suites after the follow-up: cargo test --workspace 241 passed (untouched), pytest -q 213 passed.
Review round 2 — the panel re-read c80a0c1, confirmed all four fixes above hold, and found five gaps in the machinery guarding them. All five addressed (one round-2 finding, that status_columns is missing from /api/cards/sg|pool|startup, was rejected: no consumer — the page fetches /api/cards exclusively).
_js_code() was still the brittleness _js_fn had fixed.* Its comment stripper was a regex that knew nothing of strings, so a const url = "https://…" in cards.js would delete the rest of the line from the analyzed text — and with it a restored c.type === 'identity' sitting on it. It is now the same scanner _matching() uses (strings, templates, ${} holes and nested blocks all respected) and has its own tests, including the fixture the _js_fn tests already used and a URL in each of the three quote styles. Mutation-checked: an identity branch hidden behind a URL string now fails two tests.<th> were pinned but statusCells() could drop its <td> wrapper and render three badges in zero cells — headers and body silently one column apart, every test green. test_a_body_row_holds_exactly_one_cell_per_header derives the count from status_columns()'s own length: fixed cells + status cells must equal the headers, and each cell must be opened and closed.len(unimplemented) == 17 was a snapshot, not an invariant.* Implementing any identity — the point of the project — turned it red for nothing, and a dropped pack file read the same. The expected set is now derived from the pack manifests (with an explicit assert that each manifest is still there), and the assertions are that the endpoint's identity set and its unimplemented set match the manifests', naming which codes drifted which way. Verified by flipping an elev identity to implemented: the old assert would fail, the new one passes.int(st_mtime) repeats across two deploys in the same second, and any mtime repeats under a timestamp-preserving copy — either way the browser keeps the stale cards.js. _version() is now a blake2b digest of the file's bytes; test_two_deploys_within_the_same_second_still_get_different_urls writes two same-length scripts with identical st_mtime_ns and asserts the URLs differ, which kills mtime, st_mtime_ns and size+mtime alike.status_columns.* statusCells() maps the field on every row, so attaching it only on an identity path would throw undefined.map on the first program. test_every_card_the_endpoint_returns_carries_its_status_cells asserts a well-formed three-cell list equal to status_columns(card) for every card /api/cards returns.
Suites after round 2: cargo test 241 passed (untouched), pytest -q 218 passed.
Three review rounds (glm-5.2, gpt-5.6-terra, gpt-5.6-luna) over commits
73ba1ea, f0d97e2, c80a0c1, e266f68. Rounds 1 and 2 produced nine
findings, all fixed (see c80a0c1 and e266f68); the panel confirmed each fix
on re-inspection. Round 3 produced nine more, all P3 test-hygiene issues in the
guard machinery rather than in shipped behaviour, and the round cap stopped the
work here. They are recorded for a future hardening pass — none of them affects
what a user sees today. (Update 2026-08-04: the nine findings are filed as
ticket 66, 66-cards-page-guard-machinery-hardening.md, ready-for-agent; the
applyFilters()/buildSummary() product call recorded above is filed as
ticket 67, 67-identities-in-filter-and-summary-product-call.md, needs-triage.)
1. _game_fn was never consolidated (glm) — tests/test_static_pages.py:661
still uses re.search(rf"function {name}\(.*?\n}}"), the column-0
closing-brace regex the round-1 fix replaced everywhere else. _js_fn is a
drop-in replacement.
2. Naive // stripper survives at tests/test_static_pages.py:469 (glm) —
re.sub(r"//[^\n]*", "", _editor_fn("makeDeckRow")), the exact pattern
round 2 replaced in _js_code.
3. Identity invariant is pinned only over hardcoded sets (glm, terra) —
tests/test_api.py:244-272 checks the 7 sg codes and 2 vanilla codes; the one
implemented vp identity, 36017 Hiram "0mission" Svensson, falls through every
bucket, and no assertion pins the endpoint's raw manifest passthrough for the
complete identity set (test_every_card_..._carries_its_status_cells
recomputes both sides from the same dict, so it is a tautology).
4. VANILLA_IDENTITIES = ["30076", "30077"] (glm) — the same snapshot
anti-pattern round 2 removed from the unimplemented count; adding a third
vanilla card turns the suite red for correct behaviour.
5. Unimplemented identities forced to has_tests: false (terra) —
tests/test_api.py:268-269 rejects a truthful manifest that gains
card_35001_* tests before implemented flips to true.
6. Browser cell order is unpinned (terra) — appending .reverse() to
statusCells()'s map renders the columns backwards under the right headers
and every current assertion still passes.
7. Source scanners are not regex-literal aware (terra) — a literal such as
/[//]/ or /[}]/ in a scanned function misleads _matching()/_js_code().
8. _js_fn function discovery can select a commented-out example (terra) —
the re.search that locates the function is not syntax-aware, so
// function statusCells(c) { ... } placed above the real one wins.
9. _LOCAL_ASSET only versions double-quoted, query-free, flat paths (terra,
luna) — src='/static/cards.js', src="/static/cards.js?module=1" or
static/js/cards.js would silently ship unversioned, recreating the stale-JS
failure this ticket fixed.
Rejected by the orchestrator, recorded so they are not re-litigated: manifest
schema validation for malformed values (luna — manifests are generated and
covered by test_card_manifest_honesty.py); status_columns missing from
/api/cards/sg|pool|startup (glm — no consumer, the page fetches /api/cards
exclusively); content-hash/in-place-deploy race (luna — deployment practice, not
a code defect); and the applyFilters()/buildSummary() identity exclusions
(glm), which remain the out-of-scope follow-up recorded above.