-
Notifications
You must be signed in to change notification settings - Fork 0
Identification Pipeline
Plain-language companion to theory.md — that file proves
the false-accept bound and soundness properties; this one explains the
pipeline as the code actually runs it today, for a reader who wants the
walkthrough rather than the formal composition. See
theory.md§7
for the stage-by-stage error-term treatment of the same chain, and
features/printing-tags.md /
features/catalog-completion-plan.md
for the backend and frontend this pipeline feeds into. The Stage D chain
this file walks through is currently gated on the pipeline-fidelity gate
(GitHub issue #154) before it's cleared to fire at full-catalog scale —
see pipeline-fidelity-gate.md for that
gate's current status; this file describes the mechanics, not the gate.
Reviewed and approved by the owner, 2026-07-21. Written for the pre-197k review.
- A Card row: name, source drive, and a content phash of the image.
- The reference set: every real printing of every card name (CanonicalCard / CanonicalExpansion, from Scryfall) — set code, collector number, denominator.
- No pixels stored, ever. Images are fetched transiently, read, discarded.
- Fetch the image from its source drive (transient, throttled).
- Crop fixed regions: the collector line, the legal-line band (bottom ~10% of the card), the set-symbol area; compute geometry/quality signals (border color class, bleed class, blur, entropy, truncation).
-
OCR, tiered: a fast first pass; preprocessing fallback tiers
(contrast/upscale/alternate modes) fire only when useful. NEW (#294): if
the first pass reads text with no digit-bearing structure, escalation is
skipped entirely — measured 99.7% of such cards never yield a collector line
at any tier (customs). A
short_circuitedcounter logs every skip so the 197k run itself validates this. Escape hatch:--no-shortcircuit. Set-code lexicon gate on acceptance (2026-07-23, issue #370): a tier's parse only terminates escalation if its set code is a REALCanonicalExpansioncode (or the pre-M15 collector-number-only case, no set code parsed at all) — a live structural finding (issue #370) traced 94% of a lexicon-invalid-no-match sample to the OLD "any parse" criterion accepting tier 1's own OCR noise before tiers 2/3 ever got a chance to run. Acollector_number-bearing parse whose set code ISN'T a real one no longer stops the loop; it's kept as the running best invalid candidate (first such parse, by tier order) while escalation continues, and only becomes the stored outcome if no later tier ever produces a lexicon-valid parse — the exact value pre-gate code already stored for that case, so this only changes the PATH there, never the result. Governs acceptance during escalation, not whether escalation starts (the digit-free short-circuit above is unaffected). The live-pilot OCR engine (local_identify_printing_tags.run_ocr_for_card) applies the same lexicon check at its own "parsed-but-no-match" outcome: a lexicon-invalid parse there abstains (unknown-set-code, non-rescannable) instead of casting the confidentis_no_matchvote it used to. -
Parse: set code + collector number from the collector line
(slash-format-aware since #260); the legal band is scanned for proxy
marking —
not for sale,proxy/proxies/proxied,playtestvariants (#280/#285) — settinglegal_line_proxy_marker_detected. - Persist one ImageEvidence row: raw OCR text, parses, phashes, classes, the marker flag. Keyed (card, content hash) — computed once, overwritten only by explicit re-extraction. Signals only; nothing that can rebuild the image.
Eligible cards: current evidence exists, no prior vote from this machine identity, and (safety) nothing already resolved. Then five stages per card:
- g1 — read the stored parse. No re-OCR, no re-parse; Stage C's fields are the input.
-
g2 — candidate constraint. Candidates are only the real printings of
this card's name. The parsed (set, number) must match: exactly one
candidate → match; none → parsed-but-no-match; no usable parse →
no-text skip; several → ambiguous. Set-code lexicon gate (2026-07-23):
a
parsed-but-no-matchoutcome only casts the confident no-match vote below when the parsed set code is a REALCanonicalExpansioncode — a live audit found 85.5% of this outcome's parsed set codes matched no real expansion at all (dominated by proxy/watermark text the collector-line crop also caught: "proxy", "mtg", "not", "card"), un-parsed noise, not validated evidence. A lexicon-invalid parse abstains instead (a named, non-rescannable skip, routed to the slow path below exactly likeno-text) - no confidence/ OCR-quality split was separable (checked directly: in-lexicon and out-of-lexicon parses have near-identical tesseract confidence distributions), so this is gated on lexicon membership alone; a genuinely-custom set code on a proxy of a non-existent printing is also abstained by this gate, a deliberate, documented tradeoff (seelocal_calculate_verdicts.py's own module docstring for the full reasoning and numbers). - g3 — tie-break. Ambiguity only: compare the set-symbol phash against each candidate's rendered symbol; accept only within distance threshold AND a margin over the runner-up; a near-tie stays unresolved. (Fired for 2 of 20,677 cards — the tie-break is almost never needed.)
- g4 — agreement checks. Cross-checks that can only narrow or withhold, never manufacture a match: border-color contradiction → withhold; frame-style contradiction → withhold; copyright year predating the matched set by >2 years → withhold; artist-OCR disagreement → match proceeds at lowered confidence. Proxy marking is identification-neutral (#294): catalog-required on every genuine upload, so presence proves nothing about which printing this is. (Until the #294 re-scan it wrongly vetoed 1,552 validated matches — the re-scan un-blocks them.)
-
g5 — the vote, never the verdict. A match casts one machine
CardPrintingTag vote: weight 0.5, with an ordinal confidence label
(0.85/0.75/0.65 — a pipeline-state rank, not a probability, and verified
to have zero effect on resolution). Resolution requires total weight ≥ 2
including at least one human-backed vote — structurally, the machine can
never resolve a card alone (verified 0 machine-only resolutions across
12,684 gate-checked cards, spanning both the printing- and tag-consensus
engines — see
docs/reports/2026-07-21-recovery-arc.md). Confident no-matches cast a no-match vote under the same rules. Sharpened by the owner-ratified 2026-07-22 vote-weight scenario matrix (implemented in PR #325; raw ruling atreference/vote-weight-matrix.md): it's no longer just "a human vote must be present" — machine weight (and two other non-human-backed classes added the same day: a low-weight, hard-cappedIMPLICITvote cast passively when someone picks a candidate under an active/editorfilter chip, and aFEDERATEDvote imported from a peer instance) is now excluded entirely from who wins and from the share math the moment there's a genuine human-vs-human disagreement, or the moment a human-backed winner has already cleared the resolution bar on its own. Practically: machine agreement can still help a lone human's vote resolve a previously undecided card (that's still allowed and intended — it's the whole point of the deductive backfill below), but machine or implicit disagreement can no longer do either of the two things it used to be able to do — tip an actual human-vs-human tie, or quietly flip an already-human-resolved card back to unresolved by diluting its share. That second failure mode was real and reachable at the scale this catalog now runs at (any 2-human-vote printing with 3+ contradicting machine votes), and is what the ratification specifically closed.
Everything unresolved routes to humans: skips and no-matches go to the slow path — durable review-queue markers carrying the raw signals — where the clustering backend (#265) groups them into batchable decisions, and the question feed collects the human votes that actually resolve cards.
- AI-art detector: generator names in the OCR text → "AI-Generated" tag votes (ordinary consensus since #292). Detect-and-tag only.
- "Marked as proxy" (#291, planned): marker presence → tag; absence → moderation flag, batched by source (the counterfeit-risk framing).
Candidates are name-constrained (a wrong match must be a real printing of the
same card name with a colliding set+number — the parse would have to be wrong
in a way that lands exactly on a sibling printing); the tie-break demands a
margin, not just a best score; every cross-check can only withhold; and no
machine vote resolves anything without a human. Measured so far: zero false
accepts observed at every gate that can be measured, with the error terms that
remain unmeasured named as such in
theory.md§7
rather than assumed.
Every machine action carries a run_id and identity: votes retract by identity+card set, evidence re-extracts by run, review routings clear by selector. Nothing the machine does is permanent against better information — the only permanent records are what humans decide.
Understanding the system
- Overview
- Documentation-Process
- Theory
- Identification-Pipeline
- Pipeline-Fidelity-Gate
- Federation-v1
- Vote-System
- Readiness-Audit
- License-Provenance
- Upstreaming-Conventions
- Drift-Log
- Upstream-Wiki-Drift
- Printing-Tags
- Catalog-Completion-Plan
- Moderation
- Card-DOM-API
- PDF-Generator
- Print-Export-Page
- Google-Drive-Connect
- Grid-Selector
- Image-CDN
- Local-File-Source
Using it
Operating it
Folded into other pages