Skip to content

Add illustration_id import and Stage D illustration deduction calculator - #509

Merged
WilfordGrimley merged 3 commits into
masterfrom
illustration-id-deduction
Jul 28, 2026
Merged

Add illustration_id import and Stage D illustration deduction calculator#509
WilfordGrimley merged 3 commits into
masterfrom
illustration-id-deduction

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Summary

  1. Phase 1 — Import Scryfall illustration_id (Import Scryfall illustration_id into CanonicalPrintingMetadata (illustration deduction, phase 1) #506): Add illustration_id UUIDField to PrintingMetadataRow and CanonicalPrintingMetadata. Single-faced cards carry it at top level; snip double-faced cards nest it in card_faces[0]. Migration 0089 adds the indexed field.

  2. Phase 2 — Stage D illustration deduction calculator (Stage D illustration deduction calculator (stage-d-illustration-v1) — phase 2 #507): New calculator module (stage-d-illustration-v1) that uses illustration_id relationships to deduce printing identity from artist-OCR hits. Uses in-memory index of (artist_pk, card_name) → illustration_id → [printing_pk]. 0→abstain, 1→vote at BASE_CONFIDENCE=0.85, N>1→union at BASE_CONFIDENCE/N. Wired between fallback and slow-path in the management command. DEDUCTION source with normal machine weight (no weight override).

Test plan

  • Tests written: test_local_illustration.py — IllustrationIndex construction, 0/1/N vote shapes, confidence division, dry-run, live gate, purgeability, multi-faced skip, no-evidence skip, no-artist-ocr skip
  • pre-commit clean on all touched files (ruff, isort, black, mypy, prettier — all passed)
  • Migration 0089 written manually (AddField, depends on 0088_printingtagvote)
  • pytest not run (Django statics/URL config issue outside Docker; snip container image at 0086 vs master 0088 — tests need CI or Docker with current master image)
  • CI — not watched by this session per dispatch convention

Scope note: confidence stays INFORMATIONAL per owner ratification — does not affect vote_consensus.py weight math. Zero-weight override scoped to deductive-backfill-v1 only. New calculator uses normal DEDUCTION weight. No vote pooling across illustration siblings.

Closes #506, Closes #507

Phase 1: Import Scryfall illustration_id into CanonicalPrintingMetadata
- Add illustration_id UUIDField to PrintingMetadataRow and model
- Add resolved_illustration_id property for single/double-face parsing
- Migration 0089 for the new field

Phase 2: Stage D illustration deduction calculator (issue #507)
- IllustrationIndex: in-memory (artist_pk, card_name) → illustration_id → printing_pks
- calculate_illustration_verdict: 0→abstain, 1→vote@0.85, N>1→union@0.85/N
- run_illustration_calculator: batch runner with eligibility, gate, purgeability
- Wired into management command between fallback and slow-path
- Tests for index, verdict logic, integration, dry-run, gate check, purgeability
Source bug:
- IllustrationIndex.artist_by_pk was keyed by artist_pk but match_artist
  expects card_pk keys; separated into a dedicated query over ALL
  CanonicalCards so artists are matchable even without illustration metadata

Test bugs:
- CanonicalArtistFactory.objects.get() — factory class, not a model
- candidates=candidate (bare) instead of candidates=[candidate]
- _join_key_no_hit_card set both printing and is_no_match=True, violating
  cardprintingtag_printing_xor_no_match constraint
- _eligible_card had content_phash=None, causing all cards to be skipped
- purge_machine_votes called with unsupported interactive kwarg
@WilfordGrimley
WilfordGrimley merged commit 7b02385 into master Jul 28, 2026
4 checks passed
WilfordGrimley added a commit that referenced this pull request Aug 4, 2026
modern_artist_credit.recognize_artist_credit lands in this PR as a
third artist fallback inside compute_card_evidence, but without a
version bump the fix reaches no existing row: Stage C's resume filter
(MANIFEST_EXTRACTOR_CURRENT_VERSIONS, issue #509) compares stored
extractor_versions against the manifest and skips any row whose value
already matches, and all 230,378 production rows currently carry
artist_ocr: artist-ocr-v2.

Bumps ARTIST_OCR_EXTRACTOR_VERSION and its MANIFEST_EXTRACTOR_CURRENT_VERSIONS
counterpart to artist-ocr-v3, with a dedicated version-history comment
on the constant (the shared v1->v2 OCR-engine-swap comment above it
now covers only collector_line_ocr/collector_line_tsv, which are not
bumped here). Updates the three test fixtures asserting the literal
"artist-ocr-v2" string to match.
WilfordGrimley added a commit that referenced this pull request Aug 5, 2026
… + CI totality (#690)

Inventory + declare EXTRACTOR_OWNERSHIP for every module-private helper in
image_evidence.py and every externally-imported callable it actually calls
from collector_line_artist.py/local_ocr.py/local_image_quality.py that helps
determine a stored ImageEvidence field but carries no *_EXTRACTOR_VERSION of
its own (issue #509's own follow-up). All 17 entries are components of an
already-versioned extractor - none are new extractors, so no manifest key,
golden-set expectation, or stored value changes.

- .github/scripts/check_extractor_ownership_totality.py: AST-derived
  totality check, sibling to check_extractor_manifest_sync.py. Derives the
  set of module-private helpers + called scoped-external imports reachable
  from compute_card_evidence's own call graph and fails CI if it disagrees
  with EXTRACTOR_OWNERSHIP in either direction. Cross-checks every declared
  owning key against the real MANIFEST_EXTRACTOR_KEYS (imported directly
  from check_extractor_manifest_sync.py). Excludes local_fallback.py and
  _collector_line_ocr_attempts (the OCR tier ladder) - both out of scope,
  worked on in parallel per this PR's own brief.
- .github/scripts/tests/test_check_extractor_ownership_totality.py: fixture
  + real-repo tests, including a case proving a newly-added undeclared
  contributor fails CI.
- .github/workflows/docs-lint.yml: wires the new tether in as its own job,
  mirroring extractor-manifest-sync exactly.
- docs/features/catalog-completion-plan.md: "Stage C extractor ownership"
  section - the rule, why the two rosters stay in sync, the two explicit
  exclusions, and a correction to this PR's own brief (local_art_edge.py's
  extended-art classifier, cited as an example, is not actually reachable
  from compute_card_evidence today - it's a declared-not-yet-live Stage D
  calculator, not a Stage C extractor).
- image_evidence.py: pointer paragraph in the module docstring; no
  behavioral change.
WilfordGrimley added a commit that referenced this pull request Aug 5, 2026
…on (#685)

* fix(image_evidence): wire the artist-crop fallback into live extraction

artist_ocr_name stays blank on 87,371 evidence rows that carry non-blank
artist_ocr_raw_text - the "Illus." anchor misses OCR-noisy reads (e.g.
"Soot Itus." never matches _ILLUS_RE), and the 2026-07-29 collector-line
recovery (recover_artist_from_card_text) only reads collector_line_raw_text/
legal_line_raw_text, so it is structurally blind to old-border proxies whose
only credit is a centred "Illus. <name>" line with no bottom print row at
all.

cardpicker.modern_artist_credit.recognize_artist_credit already exists for
exactly this shape of input (re-reading artist_ocr_raw_text) but was only
ever reachable through its own standalone backfill_modern_artist_names
command, never during real extraction. This wires it into
compute_card_evidence as a third fallback, tried only after both the anchor
and the collector/legal-line recovery find nothing storable - strictly
additive (new modern_artist_lexicon param, default None, no
extractor_versions bump), and threaded through both live Stage C entry
points (stage_e_dispatch.py, run_image_evidence_cohort.py).

Verified against real production evidence rows (ids 221241/221268/221274):
recognize_artist_credit recovers "Sebastian Giacobino"/"Aaron Miller"/
"Andrey Kuzinskiy" from their stored artist_ocr_raw_text at ratio 1.0
against the real ~2.5k-name lexicon. A fourth reproduction row (id 221289,
"David Rapoza") turned out to be a correct abstention, not a bug: the real
lexicon holds both "Dave Rapoza" and "David Rapoza" as distinct canonical
artists, so the reading is genuinely ambiguous under the 2026-07-29 ruling
(fuzzy matching yes, fuzzy storage no) - confirmed by direct measurement,
not assumed.

Full-population measurement of the 87,371-row split: 19,478 (22.3%)
genuinely recoverable via the artist-crop fallback, 329 (0.4%) genuinely
ambiguous (correctly abstained), 67,564 (77.3%) carry no artist credit
findable by any means - consistent with this catalog's known proxy
population rendered without standard bottom-row metadata.

* fix(tests): thread modern_artist_lexicon through remaining compute_card_evidence stubs

test_stage_e_shakedown.py and test_stream_full_catalog.py's _install_ok_stage_c_stub
helpers mirrored compute_card_evidence's pre-modern_artist_lexicon signature,
missed by the sibling repair in test_stage_e_dispatch.py and
test_run_image_evidence_cohort.py (bf17be1) because neither module was run
locally. Same fix, same shape: accept and ignore the new keyword-only param.

* feat(question-feed): expose per-candidate art-crop URL on PrintingCandidate

Adds PrintingCandidate.artCropUrl, sourced from the existing
CanonicalPrintingMetadata.art_crop_url sidecar field (no new fetch or
harvest - the data is already on disk), following the illustrationId
precedent in shape and style:

- CanonicalCard.serialise_as_printing_candidate populates it, null-
  tolerant for both the no-sidecar and empty-art_crop_url cases.
- PrintingCandidate in both schema_types.py and schema_types.ts gains
  the optional field (backend from_dict/to_dict, frontend interface +
  typeMap entry).
- docs/features/printing-tags.md's Known gaps section notes the field
  now exists but nothing consumes it yet - rendering it is future
  frontend work.

Query efficiency: every path that reaches serialise_as_printing_candidate
(get_ranked_printing_candidates's two branches, and the ai_vote lookup in
question_feed._confirm_suggestion_item) already select_related's
printing_metadata for illustrationId's sake, so no new prefetch wiring
was needed. A new test proves the candidate-grid serialisation issues
zero additional queries regardless of candidate count.

This closes the backend half of the gap PR #687 (question-feed-frontend-
fixes branch, not yet merged) identified: illustration-clustered
candidate tiles have no shared art crop to render instead of each
candidate's own full-card scan. Consuming this field in the frontend
grid remains out of scope here.

* feat(question-feed): render artCropUrl on illustration-clustered tiles; reconcile docs

Item 1: merged origin/master (PR #685 was CONFLICTING) - the reconciled
Known-gaps bullet takes the branch side (artCropUrl now exists, nothing
consumed it) since it is the informed successor to master's #687 bullet.

Item 2: illustration-clustered candidate tiles now render
PrintingCandidate.artCropUrl in place of mediumThumbnailUrl, falling back
to the printing scan when a candidate's metadata sidecar has none.
Ungrouped/identify_printing tiles are unaffected.

Item 4: the Known-gaps bullet this closes is removed; the shipped
behaviour is documented on the existing 'Illustration grouping'
architecture bullet instead.

* feat(image-evidence): bump artist_ocr extractor to v3 (PR #685)

modern_artist_credit.recognize_artist_credit lands in this PR as a
third artist fallback inside compute_card_evidence, but without a
version bump the fix reaches no existing row: Stage C's resume filter
(MANIFEST_EXTRACTOR_CURRENT_VERSIONS, issue #509) compares stored
extractor_versions against the manifest and skips any row whose value
already matches, and all 230,378 production rows currently carry
artist_ocr: artist-ocr-v2.

Bumps ARTIST_OCR_EXTRACTOR_VERSION and its MANIFEST_EXTRACTOR_CURRENT_VERSIONS
counterpart to artist-ocr-v3, with a dedicated version-history comment
on the constant (the shared v1->v2 OCR-engine-swap comment above it
now covers only collector_line_ocr/collector_line_tsv, which are not
bumped here). Updates the three test fixtures asserting the literal
"artist-ocr-v2" string to match.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant