diff --git a/MPCAutofill/cardpicker/migrations/0103_canonicalprintingmetadata_layout.py b/MPCAutofill/cardpicker/migrations/0103_canonicalprintingmetadata_layout.py new file mode 100644 index 000000000..684ff4d0f --- /dev/null +++ b/MPCAutofill/cardpicker/migrations/0103_canonicalprintingmetadata_layout.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.30 on 2026-08-05 01:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cardpicker", "0102_artbox_phash_exemplar"), + ] + + operations = [ + migrations.AddField( + model_name="canonicalprintingmetadata", + name="layout", + field=models.CharField(blank=True, max_length=30), + ), + ] diff --git a/MPCAutofill/cardpicker/models.py b/MPCAutofill/cardpicker/models.py index f41447013..62daf29ee 100755 --- a/MPCAutofill/cardpicker/models.py +++ b/MPCAutofill/cardpicker/models.py @@ -202,6 +202,16 @@ class CanonicalPrintingMetadata(models.Model): border_color = models.CharField(max_length=20, blank=True) frame = models.CharField(max_length=10, blank=True) frame_effects = models.JSONField(default=list, blank=True) + # Scryfall's own layout tag verbatim (e.g. "normal", "transform", "planar", "scheme") - + # the same value `printing_metadata_import.PrintingMetadataRow.layout` already parses and + # was, until issue #693, discarded after being checked against `DOUBLE_FACED_LAYOUTS`. + # Persisted so downstream consumers can ask "is this printing physically sideways" + # (`planar`/`scheme`, plus some `split`/`battle` cards) without re-deriving it from image + # geometry - measured 2026-08-05: only 46 of 230,378 evidence rows are landscape + # (width > height), because sideways cards are overwhelmingly rendered into a portrait + # frame with rotated content, so aspect ratio cannot find them. Canonical metadata is the + # only reliable source. + layout = models.CharField(max_length=30, blank=True) promo_types = models.JSONField(default=list, blank=True) edhrec_rank = models.IntegerField(null=True, blank=True) # HOW MANY PRINTINGS OF THIS ORACLE CARD *WE* HAVE CATALOGUED - a COUNT over our own diff --git a/MPCAutofill/cardpicker/printing_metadata_import.py b/MPCAutofill/cardpicker/printing_metadata_import.py index 23a908853..339964cea 100644 --- a/MPCAutofill/cardpicker/printing_metadata_import.py +++ b/MPCAutofill/cardpicker/printing_metadata_import.py @@ -343,6 +343,7 @@ def is_back_face(name: str, default_cards_path: Path | None = None) -> bool: "border_color", "frame", "frame_effects", + "layout", "promo_types", "edhrec_rank", "catalogued_printings_count", @@ -501,6 +502,7 @@ def import_scryfall_printing_metadata(default_cards_path: Path | None = None) -> border_color=row.border_color, frame=row.frame, frame_effects=row.frame_effects, + layout=row.layout, promo_types=row.promo_types, edhrec_rank=row.edhrec_rank, catalogued_printings_count=catalogued_printings_count, diff --git a/docs/features/catalog-completion-plan.md b/docs/features/catalog-completion-plan.md index 2726060c0..1e95fa5e4 100644 --- a/docs/features/catalog-completion-plan.md +++ b/docs/features/catalog-completion-plan.md @@ -1705,6 +1705,80 @@ fit" conditional wording; full suite 1099 passed / 4 skipped (the same CI-documented named skips — nothing newly broken); `makemigrations --check` clean (no model change). +**`layout` persisted, sideways-cohort measured (issue #693, 2026-08-05)**: +`CanonicalPrintingMetadata.layout` (migration +`0102_canonicalprintingmetadata_layout`, single-leaf on `0101`) persists +Scryfall's own `layout` tag verbatim from +`printing_metadata_import.PrintingMetadataRow.layout` — the same value the +back-face lookup above already reads to test membership in +`DOUBLE_FACED_LAYOUTS`, but which was discarded once that check ran rather +than kept. `layout` now survives past that check as a plain +`CharField(blank=True)`, following `border_color`/`frame`'s own field +convention exactly: added to `_METADATA_SYNC_FIELDS` and wired into the +`CanonicalPrintingMetadata(...)` constructor call; `DOUBLE_FACED_LAYOUTS`'s +existing use of `row.layout` is unchanged. + +Persisting it was motivated by a live measurement, not spec-first — but +the first pass at that measurement (41 of 230,488 cards, 0.018%, +2.4% parse failure) was **retracted and redone** (2026-08-05, same day): +it counted only cards that already carry a `canonical_card` link, and +that link is acquired _by having been successfully identified_ — a +sideways card whose crop lands on rotated content and therefore fails +identification can never appear in a cohort defined by identification +success. That cohort was drawn from a population selected for not having +failed, so its low failure rate proved nothing about sideways cards +generally, and the 91.5% of the catalogue with no canonical linkage +(layout unknown, not normal) was invisible to it entirely. + +The redo defines the cohort by **expected** layout instead: every +`Card.name` (source metadata, independent of identification) that +name-matches a `CanonicalCard` whose Scryfall `layout` is `planar`/ +`scheme`, via the same `to_searchable` join +`deductive_backfill.CanonicalNameIndex` already uses for this exact +Card-to-CanonicalCard join. That cohort is **1,093 cards (0.474% of the +catalogue)** — 305 canonical printing names map unambiguously to +planar/scheme, plus 4 names that are ambiguous (also match a +non-sideways printing); the old 41-card cohort is fully contained inside +it. Only 43 of the 1,093 (3.9%) carry a `canonical_card` link, confirming +the bias directly. Its collector-line parse failure rate is **53.3% +(583/1,093)**, compared against the correct baseline — the _unlinked_ +population generally (40.4%, 85,217/210,832) — not the whole-catalogue +figure (38.4%), which mixes the unlinked population with the +already-successfully-identified linked population (17.1%) and so +understates how bad the unlinked baseline really is. The sideways cohort +fails **13 points above** its correct comparison group, a real elevated +signal the old measurement's biased denominator hid entirely. + +That elevated failure rate is what a `local_fallback.normalize_crop_box` +rotation path was built to test — and the rotation trial's result is +negative. A 30-card real-image sample (fetched live via +`image_cdn_fetch.fetch_card_image`, drawn from the 1,046-card pool of +this cohort that has never carried a `canonical_card` link) was run +through the existing collector-line and Illus.-anchor crops unmodified, +then again with the whole image rotated 90° clockwise and 90° +counter-clockwise (both directions, since no stored signal indicates +which way a given render is oriented). Collector-line OCR produced a +_plausible_-looking parse on 17/30 baseline images, 9/30 rotated +clockwise, 7/30 rotated counter-clockwise — but validated against the +card's own real candidates (`local_ocr.validate_against_candidates`, +the same check Stage D's calculator applies), **zero of those parses +were genuine matches, in any of the three orientations**. The +Illus.-anchor artist-line crop found zero matches in all three +orientations too. Rotation neither recovers a match baseline missed nor +loses one baseline found (the one case where a rotation "recovered" a +plausible parse baseline missed was itself non-genuine noise). The +failure is not a crop-box placement problem rotation can fix; it matches +issue #683's own finding for the (heavily overlapping — 24/30 of this +sample's `bleed_class` is `trimmed`) trimmed-cohort investigation: these +are disproportionately fan-made custom renders that plausibly never had +a machine-readable collector line to begin with, a content problem no +crop geometry recovers. Crop-box rotation was evaluated and deliberately +**not built** — not because the cohort is negligible (it isn't) or the +failure rate is unremarkable (it isn't), but because the rotation trial +itself found nothing to recover. `golden_set.py`'s 30 pinned cards +contain zero sideways-layout cards, so the golden set cannot catch a +regression in this path specifically. + **color_profile / quality_signals / fetch-health completion — Stage C manifest extractor group, built** (public issue #150's re-spec, 2026-07-20 — the phash half of the original issue is DROPPED per the owner's same-day re-spec comment on #150, superseded by diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 15ff9f5f8..c757b85e2 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2058,3 +2058,28 @@ image samples). `cardpicker/golden_set.py`'s `GOLDEN_CARD_IDS` DOES cover black-border proxy template above, so the golden set has zero coverage of a genuine bleed-trimmed real-scan case and cannot catch a regression in that path specifically. + +## `manage.py makemigrations` fails with `ValueError: Missing staticfiles manifest entry for 'cardpicker/favicon.ico'` in a fresh worktree + +**Symptom**: `python manage.py makemigrations cardpicker` (or any other +management command that touches `MPCAutofill.urls`, which resolves a +static URL at import time) crashes before it gets anywhere near +migrations, with a `ValueError` from +`django.contrib.staticfiles.storage.ManifestStaticFilesStorage` about a +missing manifest entry for `cardpicker/favicon.ico`. Seen 2026-08-05 in +a brand-new `git worktree` checkout that had never had `collectstatic` +run against it. + +**Cause**: `STATICFILES_STORAGE` is `ManifestStaticFilesStorage` +(settings.py), which requires `staticfiles.json` — built by +`collectstatic` — to resolve any `{% static %}`/`static()` reference. +`MPCAutofill/MPCAutofill/urls.py` calls `static("cardpicker/favicon.ico")` at +import time for the favicon redirect, so importing the URLconf at all +(which `makemigrations`' system checks do) fails in any checkout that +hasn't run `collectstatic` yet — this has nothing to do with +migrations themselves. + +**Fix**: run `python manage.py collectstatic --noinput` once per +worktree before the first `makemigrations`/`migrate`/any other +management command in it. Output lands in the gitignored `/static/` +directory at the repo root, harmless to leave in place.