From cfe2af4616a25b7b80150dd1e5676b64654da384 Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Wed, 5 Aug 2026 01:20:34 +0000 Subject: [PATCH 1/5] feat(printing-metadata): persist Scryfall layout, measure sideways cohort (issue #693) Step 1: add CanonicalPrintingMetadata.layout (migration 0102), wired from PrintingMetadataRow.layout (already parsed, previously discarded after the DOUBLE_FACED_LAYOUTS check). Follows the existing border_color/frame field conventions on that model exactly (blank-string sentinel, CharField, added to _METADATA_SYNC_FIELDS). Step 2: measured the sideways cohort (Scryfall layout planar/scheme) against the live catalogue via read-only docker exec queries. 41 of 230,488 catalogued cards (0.018%) resolve to a sideways layout; the cohort's own collector-line parse failure rate (2.4%, 1/41) is not elevated versus the whole-catalogue rate measured multiple ways (15.5%-38.4% depending on denominator). Gate result: cohort negligible, no elevated failure signal - stopping here per the brief's own explicit exit ramp. Step 3 (crop-box rotation) not built. None of golden_set.py's 30 pinned cards is a sideways layout (confirmed via live query) - the golden set cannot catch a regression in this path. Also documents a fresh-worktree makemigrations blocker (missing staticfiles manifest) in docs/troubleshooting.md, hit and resolved while verifying this migration. --- .../0102_canonicalprintingmetadata_layout.py | 18 +++++++++++++ MPCAutofill/cardpicker/models.py | 10 ++++++++ .../cardpicker/printing_metadata_import.py | 2 ++ docs/troubleshooting.md | 25 +++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 MPCAutofill/cardpicker/migrations/0102_canonicalprintingmetadata_layout.py diff --git a/MPCAutofill/cardpicker/migrations/0102_canonicalprintingmetadata_layout.py b/MPCAutofill/cardpicker/migrations/0102_canonicalprintingmetadata_layout.py new file mode 100644 index 000000000..76f1ddd9d --- /dev/null +++ b/MPCAutofill/cardpicker/migrations/0102_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", "0101_delete_printingtagvote"), + ] + + 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 d7413bbe6..4a2e46ffc 100755 --- a/MPCAutofill/cardpicker/models.py +++ b/MPCAutofill/cardpicker/models.py @@ -195,6 +195,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/troubleshooting.md b/docs/troubleshooting.md index c6a816289..641ff9683 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1997,3 +1997,28 @@ query once it notices its client is gone, deliberately not `statement_timeout` (BULK-mode eligibility scans legitimately run for minutes-hours with a live client and must not be killed on a timer). Parked on the board, not yet shipped. + +## `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/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. From 206462806638c3f086fe44466459818e266b89ca Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Wed, 5 Aug 2026 01:43:38 +0000 Subject: [PATCH 2/5] fix(docs): correct urls.py path in favicon.ico troubleshooting entry MPCAutofill/urls.py -> MPCAutofill/MPCAutofill/urls.py (Django project's own urls module, which contains the static() call for the favicon redirect). Fixes docs_lint.py path-reference failure on PR #696. --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index cae4e7d81..c757b85e2 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2073,7 +2073,7 @@ run against it. **Cause**: `STATICFILES_STORAGE` is `ManifestStaticFilesStorage` (settings.py), which requires `staticfiles.json` — built by `collectstatic` — to resolve any `{% static %}`/`static()` reference. -`MPCAutofill/urls.py` calls `static("cardpicker/favicon.ico")` at +`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 From 8c34e5cc22f25c92038446ff7136d67ecec02c27 Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:03:38 +0000 Subject: [PATCH 3/5] docs(catalog-completion-plan): document CanonicalPrintingMetadata.layout and the sideways-cohort measurement (issue #693) --- docs/features/catalog-completion-plan.md | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/features/catalog-completion-plan.md b/docs/features/catalog-completion-plan.md index 2726060c0..7a9dbfa79 100644 --- a/docs/features/catalog-completion-plan.md +++ b/docs/features/catalog-completion-plan.md @@ -1705,6 +1705,32 @@ 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: the +"sideways" cohort (Scryfall `layout` `planar`/`scheme` — cards printed in +landscape, collector line and all other text rotated 90° from the +portrait orientation every other extractor assumes) is **41 of 230,488 +catalogued cards (0.018%)**, and that cohort's own collector-line parse +failure rate is **2.4% (1/41)** — not elevated against any whole-catalogue +baseline measured. On that result, crop-box rotation for sideways cards +(a `local_fallback.normalize_crop_box` extension) was evaluated and +deliberately **not built**: the cohort is too small and the failure rate +too unremarkable to justify it. `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 From bcf44a11a441f20f06b249a54e709fd1d714c400 Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Wed, 5 Aug 2026 03:06:25 +0000 Subject: [PATCH 4/5] fix(catalog-completion-plan): retract biased sideways-cohort measurement, redo with rotation trial (issue #693) The 41-card/0.018% cohort was drawn only from cards already carrying a canonical_card link, which is acquired by successful identification - a sideways card whose crop lands on rotated content and fails identification could never appear in it. Redone by expected layout (Card.name join against CanonicalCard's Scryfall layout, independent of identification success): 1,093 cards (0.474%), 53.3% collector-line parse failure vs. 40.4% for the correct comparison group (unlinked population generally, not the whole-catalogue figure that mixes in the already-identified linked population). That elevated signal motivated a rotation trial: 30 real images sampled from the cohort's never-linked pool, existing collector-line and Illus.-anchor crops run unmodified then again with the image rotated 90 degrees both directions. Zero candidate-validated genuine matches in any orientation - a content problem (fan-made renders without a real collector line, overlapping issue #683's trimmed-cohort finding), not a crop-geometry problem. Rotation was evaluated and deliberately not built. PR #696 body patched to match via gh api PATCH (gh pr edit fails on this repo). --- docs/features/catalog-completion-plan.md | 72 ++++++++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/docs/features/catalog-completion-plan.md b/docs/features/catalog-completion-plan.md index 7a9dbfa79..1e95fa5e4 100644 --- a/docs/features/catalog-completion-plan.md +++ b/docs/features/catalog-completion-plan.md @@ -1718,18 +1718,66 @@ 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: the -"sideways" cohort (Scryfall `layout` `planar`/`scheme` — cards printed in -landscape, collector line and all other text rotated 90° from the -portrait orientation every other extractor assumes) is **41 of 230,488 -catalogued cards (0.018%)**, and that cohort's own collector-line parse -failure rate is **2.4% (1/41)** — not elevated against any whole-catalogue -baseline measured. On that result, crop-box rotation for sideways cards -(a `local_fallback.normalize_crop_box` extension) was evaluated and -deliberately **not built**: the cohort is too small and the failure rate -too unremarkable to justify it. `golden_set.py`'s 30 pinned cards contain -zero sideways-layout cards, so the golden set cannot catch a regression in -this path specifically. +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 From a4e7a6c982fcf401417497e83576ba9006214fff Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Wed, 5 Aug 2026 03:23:13 +0000 Subject: [PATCH 5/5] fix(migrations): renumber 0102_canonicalprintingmetadata_layout to 0103, repoint dependency at 0102_artbox_phash_exemplar (#696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged origin/master into this branch, which brought in 0102_artbox_phash_exemplar (#701) depending on 0101_delete_printingtagvote — the same parent this branch's migration used, producing two leaves. Renumbered this branch's migration to 0103 and repointed its dependency at 0102_artbox_phash_exemplar. Operation unchanged. --- ...adata_layout.py => 0103_canonicalprintingmetadata_layout.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename MPCAutofill/cardpicker/migrations/{0102_canonicalprintingmetadata_layout.py => 0103_canonicalprintingmetadata_layout.py} (86%) diff --git a/MPCAutofill/cardpicker/migrations/0102_canonicalprintingmetadata_layout.py b/MPCAutofill/cardpicker/migrations/0103_canonicalprintingmetadata_layout.py similarity index 86% rename from MPCAutofill/cardpicker/migrations/0102_canonicalprintingmetadata_layout.py rename to MPCAutofill/cardpicker/migrations/0103_canonicalprintingmetadata_layout.py index 76f1ddd9d..684ff4d0f 100644 --- a/MPCAutofill/cardpicker/migrations/0102_canonicalprintingmetadata_layout.py +++ b/MPCAutofill/cardpicker/migrations/0103_canonicalprintingmetadata_layout.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("cardpicker", "0101_delete_printingtagvote"), + ("cardpicker", "0102_artbox_phash_exemplar"), ] operations = [