Skip to content

Wire the attribute-chip casters into an engine; strip the vote from the uncalled wrapper - #654

Merged
WilfordGrimley merged 1 commit into
masterfrom
feat/attribute-chip-evidence-caster
Jul 30, 2026
Merged

Wire the attribute-chip casters into an engine; strip the vote from the uncalled wrapper#654
WilfordGrimley merged 1 commit into
masterfrom
feat/attribute-chip-evidence-caster

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Item 2 of the "essential major blockers" brief, sourced from PR #616 / docs/reports/2026-07-29-pipeline-coverage-composition-audit.md §1 Q1 and §6.

The defect

cast_border_attribute_vote, cast_frame_style_vote, cast_bleed_edge_vote in local_fallback.py are called from exactly two places:

  1. local_identify_printing_tags.run_pilot — the live-FETCH pilot, with one completed run in its entire history (2026-07-16).
  2. image_evidence.extract_card_evidence — which has ZERO production callers. Both engines call compute_card_evidence + persist_evidence directly and have since the 2026-07-20 fetch/compute decoupling.

So no engine could cast any attribute chip. Measured consequence: frame-style and bleed-edge chips at zero rows with no substitute. Border chips survived only because layout-class-cast-v1 independently computes the same thing — and that caster was reachable only from its own standalone management command, so the conveyor could not produce a border chip either.

The fix

New cardpicker/local_attribute_chip_cast.py — an evidence-reading caster, modelled on local_layout_class_cast (which reads evidence) and not on the pilot (which fetches). Zero image fetches, booby-trapped in test. Reuses classify_frame_style / FRAME_STYLE_TO_TAG / FRAME_VOTE_CONFIDENCE / BLEED_EDGE_TAG_NAME / BLEED_EDGE_VOTE_CONFIDENCE verbatim so it cannot drift from the pilot on what a frame class is or what it is worth.

Derivable populations (read-only, 2026-07-29): Modern Border 133,627 + Old Border 9,006 + appropriate-bleed 2,786 = 145,419 votes, none needing a fetch.

It does not cast border. That would be a third border channel, and the duplication is the audit's own §6 cull recommendation, not a pattern to extend. local_layout_class_cast remains the border caster; this PR makes it conveyor-reachable instead of duplicating it.

Two identities (frame-style-cast-v1, bleed-edge-cast-v1) written from one pass. The bleed chip is negative-only (~98.7% of cards get a frame vote and no bleed vote), so under one shared identity that frame vote would read as "handled" and permanently strand the bleed chip.

REQUIRED_EXTRACTOR_KEYS per chip family. The frame chip gates on artist_ocr as well as collector_line_ocr, because illus_anchor_fired is nullable and bool(None) is False — indistinguishable from "the extractor ran and found no anchor". Without the gate every card missing artist_ocr reads modern. That is item 4's bug class, applied correctly from the start.

Wired into the conveyor

stage_e_dispatch._run_stage_d now runs both casters after the four printing calculators. Zero fetches means no envelope or fetch-budget cost. Order is irrelevant here (neither reads another calculator's output) and the docstring says so.

A missing tag seed is caught and logged at ERROR rather than propagating: by that point the printing votes are already written, and letting a RuntimeError out would mark the whole dispatch FAILED over an advisory chip. Not a silent swallow — the counters staying at zero is the audit's own "this channel never ran" signal, and the log names the fix.

extract_card_evidence resolved

Renamed fetch_and_compute_card_evidence_for_tests and its cast_border_attribute_vote(...).save() deleted, per the brief's first option. The docstring now carries a DO NOT ADD A WRITE HERE note explaining why.

Its test class inverted with it. test_classified_border_casts_one_vote_per_card was a green test over a channel that had not existed for four months — a vote cast in an uncalled function looks like a wired channel to any grep, and a passing test looks like proof it works. That is how this stayed invisible.

Verification

mutation expected red result
drop the artist_ocr gate (read bool(None) as a real False) frame chip skips rather than reading False 1 failed, 18 passed
unwire the casters from _run_stage_d both conveyor-wiring tests 2 failed, 17 passed
restore the deleted vote cast test-only-wrapper test + the inverted test_image_evidence class 2 failed, 129 passed
collapse to one shared anonymous_id write / cross-suppression / idempotence 3 failed, 16 passed
let the seed RuntimeError escape unseeded-tag dispatch test 1 failed, 19 passed
restored 20 passed

The third mutation initially came back green — the test stubbed the fetch to None, so layout_class was "" and the deleted cast would have returned None anyway. The test was rewritten to force a confident black reading, and only then did the mutation go red. Noted because a vacuous test is the same class of problem this whole PR is about.

  • pytest cardpicker/tests/ — 3444 passed, 11 skipped (full suite)
  • docs_lint.py --strict — clean
  • pre-commit (ruff/isort/black/mypy/prettier) — clean
  • No migration.

Two existing tethers fired and were satisfied, not bypassed: the calculator roster tether (docs/pipeline-fidelity-gate.md) and test_skip_reason_roster.py. Both new identities and all four new skip-reason constants are registered.

Docs (living, per .github/wiki-publish-map.json — no dated report)

  • docs/features/printing-tags.md — new "Who actually casts the attribute chips" section with the caster/identity table.
  • docs/pipeline-fidelity-gate.md — both new identities in the calculator roster, marked 0 rows / never run.
  • docs/reference/skip-reasons.md — new section for the caster's four values.

🤖 Generated with Claude Code

https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN

…he uncalled wrapper

The 2026-07-29 composition audit measured `Old Border`, `Modern Border` and
`appropriate-bleed` at ZERO machine rows with no substitute. Cause: the only
code that ever cast them was `local_identify_printing_tags.run_pilot` (a
live-FETCH pilot, one completed run in its history) and
`image_evidence.extract_card_evidence`, which has ZERO production callers -
both engines call `compute_card_evidence` + `persist_evidence` directly. Border
colour survived the same purge only because `local_layout_class_cast`
independently re-derives it, and even that was reachable only from its own
standalone command.

NEW `local_attribute_chip_cast` (`frame-style-cast-v1`/`bleed-edge-cast-v1`):
reads stored `ImageEvidence`, casts frame-style and bleed-edge chips, fetches
NOTHING. Modelled on `local_layout_class_cast`, not on the pilot. Reuses
`classify_frame_style`/`FRAME_STYLE_TO_TAG`/`FRAME_VOTE_CONFIDENCE`/
`BLEED_EDGE_TAG_NAME`/`BLEED_EDGE_VOTE_CONFIDENCE` verbatim so it cannot drift
from the pilot on what a frame class is or what it is worth. Derivable
populations, measured read-only 2026-07-29: 133,627 + 9,006 + 2,786 = 145,419
votes, none of which needs an image fetch.

It does NOT cast border - that would be a THIRD border channel, and the
duplication is the audit's own cull recommendation, not a pattern to extend.

TWO IDENTITIES, not one. The bleed chip is negative-only, so under a shared
identity a card's frame vote would read as "handled" and permanently strand its
bleed chip.

REQUIRED_EXTRACTOR_KEYS PER CHIP FAMILY. The frame chip gates on `artist_ocr`
as well as `collector_line_ocr`: `illus_anchor_fired` is nullable and
`bool(None)` is False, which without the gate classifies every card with no
anchor evidence as `modern`. This is item 4's bug class, applied correctly from
the start.

WIRED INTO THE CONVEYOR. `stage_e_dispatch._run_stage_d` now runs both this
caster and `local_layout_class_cast`, so all three chip families are reachable
from an engine for the first time. Zero fetches means no envelope/fetch-budget
cost. A missing tag seed is caught and logged at ERROR rather than marking a
dispatch FAILED over an advisory chip after the printing votes already landed -
the counters staying at zero is the observable, not a silent swallow.

`extract_card_evidence` -> `fetch_and_compute_card_evidence_for_tests`, and its
`cast_border_attribute_vote(...).save()` is deleted. Its test class inverted
with it: `test_classified_border_casts_one_vote_per_card` was a green test over
a channel that had not existed since the 2026-07-20 fetch/compute decoupling,
which is precisely how the hole stayed invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant