fix(local_calculate_verdicts): retire the border-mismatch join-key veto in favour of a demoted vote - #684
fix(local_calculate_verdicts): retire the border-mismatch join-key veto in favour of a demoted vote#684WilfordGrimley wants to merge 1 commit into
Conversation
…to in favour of a demoted vote A border disagreement in _apply_agreement_checks no longer withholds an otherwise-exact join-key match. Owner ruling: this catalogue indexes proxy renders of a printing, not the printing itself, so a differently-bordered render still depicts the same printing - the border is a real, worth- recording distinction, not counter-evidence against the match. - Border disagreement now demotes confidence to the new JOIN_KEY_CONFIDENCE_BORDER_DISAGREEMENT tier (0.70) instead of returning a skip. When a card also carries an artist-OCR disagreement, the resulting confidence is the lower of the two tiers, not whichever check runs last. - The frame half of printing_attribute_disagreement is unchanged and still a hard veto - only the border half changes. - JOIN_KEY_BORDER_MISMATCH_SKIP_REASON is retired-not-removed, matching the JOIN_KEY_PROXY_MARKER_VETO_SKIP_REASON precedent: no code path writes it as a new value, but it stays declared and in JOIN_KEY_NO_HIT_SKIP_REASONS so historical rows keep routing to review. - The observed-border attribute-chip tag (local_layout_class_cast. run_layout_class_cast, wired into Stage E's _run_attribute_chip_casters) already casts independently of this calculator's own eligibility, so no new caster is needed to satisfy the "record the observed border as a tag" half of the ruling. - docs/reference/skip-reasons.md updated in place for the retirement. Investigated illustration tagging for the border-mismatch cohort (12,442/15,626 cards carry artist_ocr_name): once the veto is lifted these cards exit the illustration calculator's own eligibility population entirely, since they now resolve via a direct join-key printing vote instead of the join-key "no confident hit" outcome that population is scoped to - a negative finding, not a gap to fix.
|
Closing unmerged on owner ruling. A border disagreement means the card is not that printing - the same illustration published with a different border is a different printing - so casting a CardPrintingTag for it is a false positive rather than a weak positive. The demoted confidence tier does not mitigate this, because resolve_weighted_consensus weights votes by source alone and never reads the confidence field, so a 0.70 row carries identical weight to a full-confidence join-key hit. The change is also self-defeating. Casting the vote makes the card no longer a no-confident-hit outcome, which drops it out of the illustration calculator's eligible population and discards the one accurate record obtainable here. The existing border veto is correct behaviour and stays as-is. The real gap is downstream in the illustration calculator and is tracked separately. |
Summary
local_calculate_verdicts._apply_agreement_checks): a border disagreement betweenImageEvidence.layout_classand the matched printing'sCanonicalPrintingMetadata.border_colorno longer withholds the match (JOIN_KEY_BORDER_MISMATCH_SKIP_REASON). Owner ruling: this catalogue indexes proxy renders of a printing, not the printing itself, so a differently-bordered render still depicts the same printing - "a printing matches exactly, a border mismatch is an accurate distinction."JOIN_KEY_CONFIDENCE_BORDER_DISAGREEMENT = 0.70, placed (reasoned, not calibrated) betweenJOIN_KEY_CONFIDENCE_ARTIST_DISAGREEMENT(0.65) andJOIN_KEY_CONFIDENCE_COLLECTOR_ONLY/JOIN_KEY_CONFIDENCE_SYMBOL_TIEBREAK(0.75). When a card also carries an artist-OCR disagreement,_apply_agreement_checksnow takes the LOWER of the two tiers viamin(...)rather than whichever check runs last silently overwriting the other's demotion.local_identify_printing_tags.printing_attribute_disagreementis unchanged and still a hard veto (JOIN_KEY_FRAME_MISMATCH_SKIP_REASON) - only the border half changes, per spec.JOIN_KEY_BORDER_MISMATCH_SKIP_REASONis retired-not-removed, matching theJOIN_KEY_PROXY_MARKER_VETO_SKIP_REASONprecedent this file already established (2026-07-21): declared, still a member ofJOIN_KEY_NO_HIT_SKIP_REASONS, so historical rows continue routing to human review viaquestion_feed/the slow-path calculator/the funnel "withheld" count inviews.py. No code path writes it as a NEW value going forward.docs/reference/skip-reasons.md'sborder-mismatchrow updated in place to reflect the retirement (mirroring theproxy-marker-vetorow's own shape), with an explicit note that a stale pre-2026-08-04 row needs no dedicated retraction command - unlikeproxy-marker-veto, a plain re-run ofrun_join_key_calculatorunder a freshrun_id(2026-07-29's run-scoping) revisits and casts the demoted vote through the ordinary calculator path._apply_agreement_checksordering list) so the border/frame asymmetry is documented at every place it used to describe both as symmetric vetoes.Scope items 3 and 4 (border tagging / illustration tagging), investigated, no code change needed:
local_layout_class_cast.run_layout_class_cast, wired into Stage E's_run_attribute_chip_casters(2026-07-30) - its own eligibility query is independent of the join-key calculator entirely (it excludes only cards it has itself already voted on), so this was never gated behind the border veto and needs no new caster.artist_ocr_name, per the brief's own measurement). Finding:local_illustration._eligible_illustration_cards_querysetscopes its population to cards the join-key calculator concluded have "no confident hit" (join_key_voted_card_ids=is_no_match=Truevotes, ORjoin_key_scanned_card_ids= aJOIN_KEY_NO_HIT_SKIP_REASONSscan-log row). Once the border veto is lifted, a border-mismatched card gets a real, positive join-key vote instead - it is no longer part of that "no confident hit" population, and so it now falls OUT of the illustration calculator's eligibility rather than into it. This is a negative finding, not a gap: these cards are "already tagged" once the veto is lifted, just via the direct join-key printing vote from this PR's own item 1, not via a separate illustration vote. No further change made for this item, per the brief's own instruction to report an accurate negative finding rather than build a fix for a problem that isn't there.Test plan
TestAgreementChecks::test_border_mismatch_casts_the_vote_at_a_demoted_confidence(new) - a border disagreement now casts the join-key vote atJOIN_KEY_CONFIDENCE_BORDER_DISAGREEMENTinstead of withholding it.TestAgreementChecks::test_border_and_artist_disagreement_together_take_the_lower_confidence(new) - both disagreements present -> the lower (artist, 0.65) tier wins, not whichever check ran last.TestAgreementChecks::test_frame_mismatch_withholds_the_match(pre-existing, unmodified) - confirms the frame veto is unaffected.TestAgreementChecks::test_border_mismatch_writes_a_demoted_vote_via_the_full_runner(rewritten from the old scan-log-row assertion) - the realrun_join_key_calculatorbatch runner casts aCardPrintingTagat the demoted confidence and writes noCardScanLogrow for it; a same-run_idre-run does not reconsider the card (ordinary vote idempotence).TestFrameVetoRequiresArtistOcr::test_the_border_demotion_is_unaffected_by_the_frame_gate(rewritten from the old veto assertion) - the frame-gate'sartist_ocrrequirement is scoped to the frame check alone; border still demotes independent of it.cardpicker/tests/test_local_calculate_verdicts.pyrun individually: 187 passed.cardpicker/tests/test_local_calculate_verdicts.py+test_skip_reason_roster.py+test_run_scoped_eligibility.py+test_local_identify_printing_tags.py+test_local_illustration.py+test_local_layout_class_cast.py+test_question_feed.pyrun together (issue Test suite is order-dependent: leaked fetch-failure window trips the envelope across files (8 failures on master) #679 order-dependence check): 680 passed.test_skip_reason_roster.py,test_run_scoped_eligibility.py,test_local_identify_printing_tags.py,test_local_illustration.py,test_local_layout_class_cast.pyrun individually together (no funnel_counts): 447 passed - these files constructborder-mismatchCardScanLogrows manually to test historical-row handling and needed no changes, confirming the retired-not-removed treatment is correct.pre-commit runon the touched files (ruff, isort, black, mypy, prettier, readme-parity) - green, prettier auto-reformatted thedocs/reference/skip-reasons.mdtable (re-committed).test_funnel_counts.pydeferred - 24/25 tests in this file fail with a pre-existing, unrelatedValueError: Missing staticfiles manifest entry for 'cardpicker/favicon.ico'in this environment. Verified this is not caused by this change: identical failure reproduces on unmodifiedmaster(git stash+ re-run). Not investigated further as out of scope for this task.Task-end checks (CLAUDE.md)
docs/reference/skip-reasons.mdin place (no dated section appended).CanonicalPrinting-consensus machinery; not a new dependency-free primitive.JOIN_KEY_BORDER_MISMATCH_SKIP_REASONkeeps its exact name and value; only which code paths reference it changed.JOIN_KEY_CONFIDENCE_BORDER_DISAGREEMENTis a brand-new constant, not an extraction of an existing one.