Skip to content

fix(local_calculate_verdicts): retire the border-mismatch join-key veto in favour of a demoted vote - #684

Closed
WilfordGrimley wants to merge 1 commit into
masterfrom
retire-border-veto-tagging
Closed

fix(local_calculate_verdicts): retire the border-mismatch join-key veto in favour of a demoted vote#684
WilfordGrimley wants to merge 1 commit into
masterfrom
retire-border-veto-tagging

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Summary

  • Retires the border-mismatch join-key veto (local_calculate_verdicts._apply_agreement_checks): a border disagreement between ImageEvidence.layout_class and the matched printing's CanonicalPrintingMetadata.border_color no 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."
  • The vote is now cast at a new demoted-confidence tier, JOIN_KEY_CONFIDENCE_BORDER_DISAGREEMENT = 0.70, placed (reasoned, not calibrated) between JOIN_KEY_CONFIDENCE_ARTIST_DISAGREEMENT (0.65) and JOIN_KEY_CONFIDENCE_COLLECTOR_ONLY/JOIN_KEY_CONFIDENCE_SYMBOL_TIEBREAK (0.75). When a card also carries an artist-OCR disagreement, _apply_agreement_checks now takes the LOWER of the two tiers via min(...) rather than whichever check runs last silently overwriting the other's demotion.
  • The frame half of local_identify_printing_tags.printing_attribute_disagreement is unchanged and still a hard veto (JOIN_KEY_FRAME_MISMATCH_SKIP_REASON) - only the border half changes, per spec.
  • JOIN_KEY_BORDER_MISMATCH_SKIP_REASON is retired-not-removed, matching the JOIN_KEY_PROXY_MARKER_VETO_SKIP_REASON precedent this file already established (2026-07-21): declared, still a member of JOIN_KEY_NO_HIT_SKIP_REASONS, so historical rows continue routing to human review via question_feed/the slow-path calculator/the funnel "withheld" count in views.py. No code path writes it as a NEW value going forward.
  • docs/reference/skip-reasons.md's border-mismatch row updated in place to reflect the retirement (mirroring the proxy-marker-veto row's own shape), with an explicit note that a stale pre-2026-08-04 row needs no dedicated retraction command - unlike proxy-marker-veto, a plain re-run of run_join_key_calculator under a fresh run_id (2026-07-29's run-scoping) revisits and casts the demoted vote through the ordinary calculator path.
  • Module docstring updated (the "Geometry/border agreement + frame agreement" bullet, the "TWO FURTHER CHEAP ADDITIONS" slow-path routing paragraph, the _apply_agreement_checks ordering 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:

  • The observed border is already cast as its own attribute-chip tag by 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.
  • Investigated illustration tagging for the border-mismatch cohort (12,442/15,626 cards carry artist_ocr_name, per the brief's own measurement). Finding: local_illustration._eligible_illustration_cards_queryset scopes its population to cards the join-key calculator concluded have "no confident hit" (join_key_voted_card_ids = is_no_match=True votes, OR join_key_scanned_card_ids = a JOIN_KEY_NO_HIT_SKIP_REASONS scan-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 at JOIN_KEY_CONFIDENCE_BORDER_DISAGREEMENT instead 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 real run_join_key_calculator batch runner casts a CardPrintingTag at the demoted confidence and writes no CardScanLog row for it; a same-run_id re-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's artist_ocr requirement is scoped to the frame check alone; border still demotes independent of it.
  • cardpicker/tests/test_local_calculate_verdicts.py run 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.py run 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.py run individually together (no funnel_counts): 447 passed - these files construct border-mismatch CardScanLog rows manually to test historical-row handling and needed no changes, confirming the retired-not-removed treatment is correct.
  • pre-commit run on the touched files (ruff, isort, black, mypy, prettier, readme-parity) - green, prettier auto-reformatted the docs/reference/skip-reasons.md table (re-committed).
  • test_funnel_counts.py deferred - 24/25 tests in this file fail with a pre-existing, unrelated ValueError: Missing staticfiles manifest entry for 'cardpicker/favicon.ico' in this environment. Verified this is not caused by this change: identical failure reproduces on unmodified master (git stash + re-run). Not investigated further as out of scope for this task.
  • CI - not watched by this session per dispatch convention.

Task-end checks (CLAUDE.md)

  • Docs convention: edited docs/reference/skip-reasons.md in place (no dated section appended).
  • Policy text dates: N/A, no policy page touched.
  • Wiki maintenance: N/A - this changes internal calculator behaviour only, no user-visible or admin-visible surface.
  • Extractable-primitives ledger: N/A - this touches Stage D's join-key calculator, which already depends on vote-consensus/CanonicalPrinting-consensus machinery; not a new dependency-free primitive.
  • Constant-rename-equivalence script: N/A - no constant was renamed, extracted, or moved. JOIN_KEY_BORDER_MISMATCH_SKIP_REASON keeps its exact name and value; only which code paths reference it changed. JOIN_KEY_CONFIDENCE_BORDER_DISAGREEMENT is a brand-new constant, not an extraction of an existing one.

…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.
@WilfordGrimley

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant