diff --git a/MPCAutofill/cardpicker/management/commands/run_pipeline.py b/MPCAutofill/cardpicker/management/commands/run_pipeline.py index d74355581..8efd172d5 100644 --- a/MPCAutofill/cardpicker/management/commands/run_pipeline.py +++ b/MPCAutofill/cardpicker/management/commands/run_pipeline.py @@ -603,7 +603,8 @@ def _run_stage_d_bulk( """ self.stdout.write( "STAGE D: join-key -> fallback -> illustration -> slow-path, then the border / " - "frame-style / bleed-edge attribute chips" + "frame-style / bleed-edge attribute chips, then ai-art / residual-classify / " + "art-hash-artist / lands-artist-decomp" + ("" if cohort_ids is None else f" (scoped to {len(cohort_ids)} cards)") ) outcome = DispatchOutcome(status="monolith", run_id=run_id) @@ -636,6 +637,12 @@ def _run_stage_d_bulk( "border_chip_votes": outcome.stage_d_border_chip_votes, "frame_chip_votes": outcome.stage_d_frame_chip_votes, "bleed_chip_votes": outcome.stage_d_bleed_chip_votes, + "ai_art_votes": outcome.stage_d_ai_art_votes, + "art_hash_artist_votes": outcome.stage_d_art_hash_artist_votes, + "lands_votes": outcome.stage_d_lands_votes, + "lands_already_voted": outcome.stage_d_lands_already_voted, + "residual_artist_votes": outcome.stage_d_residual_artist_votes, + "residual_tag_votes": outcome.stage_d_residual_tag_votes, "verdict_transfer_votes": outcome.stage_d_verdict_transfer_votes, } self.stdout.write(f"STAGE D: {result}") @@ -656,7 +663,8 @@ def _run_streaming_stages( self.stdout.write(f"STREAMING C→D: {batch_decision.describe()}") self.stdout.write(f"STAGE C: run_stage_e_streaming (micro-batches of {batch_size})") self.stdout.write( - "STAGE D: join-key -> fallback -> illustration -> slow-path, then the border / frame / bleed chips" + "STAGE D: join-key -> fallback -> illustration -> slow-path, then the border / frame / bleed " + "chips, then ai-art / residual-classify / art-hash-artist / lands-artist-decomp" ) max_batches: Optional[int] = options.get("max_batches") @@ -691,6 +699,12 @@ def _run_streaming_stages( "stage_d_border_chip_votes": 0, "stage_d_frame_chip_votes": 0, "stage_d_bleed_chip_votes": 0, + "stage_d_ai_art_votes": 0, + "stage_d_art_hash_artist_votes": 0, + "stage_d_lands_votes": 0, + "stage_d_lands_already_voted": 0, + "stage_d_residual_artist_votes": 0, + "stage_d_residual_tag_votes": 0, "stage_d_verdict_transfer_votes": 0, } diff --git a/MPCAutofill/cardpicker/stage_e_dispatch.py b/MPCAutofill/cardpicker/stage_e_dispatch.py index 8c06fe5b4..14c4c361b 100644 --- a/MPCAutofill/cardpicker/stage_e_dispatch.py +++ b/MPCAutofill/cardpicker/stage_e_dispatch.py @@ -279,6 +279,17 @@ class DispatchOutcome: stage_d_border_chip_votes: int = 0 stage_d_frame_chip_votes: int = 0 stage_d_bleed_chip_votes: int = 0 + # EVIDENCE-ONLY CALCULATORS (2026-08-05, closing the "10 of ~28 channels" wiring audit - see + # `_run_evidence_only_calculators`' own docstring for the FREE/EXPENSIVE classification of + # every channel this closes and every one it deliberately leaves open). All four read only + # data this pass has already stored - `ImageEvidence`, `Card.content_phash`, resolved artist + # chains - and fetch nothing, so wiring them costs the conveyor no network or fetch budget. + stage_d_ai_art_votes: int = 0 + stage_d_art_hash_artist_votes: int = 0 + stage_d_lands_votes: int = 0 + stage_d_lands_already_voted: int = 0 + stage_d_residual_artist_votes: int = 0 + stage_d_residual_tag_votes: int = 0 # Stream B (md5 verdict-transfer gate): how many cards in this batch had their Stage D verdict # satisfied via propagation from a same-md5 sibling's existing CardPrintingTag row instead of # running through the four calculators and three chips. Zero when the gate found nothing to @@ -1230,6 +1241,98 @@ def _run_attribute_chip_casters( ) +def _run_evidence_only_calculators( + run_id: str, card_ids: Optional[list[int]], outcome: DispatchOutcome, dry_run: bool = False +) -> None: + """ + FOUR MORE CHANNELS WIRED INTO THE CONVEYOR (2026-08-05, closing the wiring audit that asked + "does a full-catalogue pass actually invoke every declared calculator identity, or only the + ones a name-match against this file happens to find"). `ai-art-detector-v1` + (`local_detect_ai_art.run_ai_art_detector`), `residual-classify-v1`/`art-hash-artist-v1` + (`local_residual_classify.run_frame_mismatch_recovery`/`run_d0_sibling_artist_propagation`) + and `lands-artist-decomp-v1` (`local_lands_identify.run_lands_identify`) had a real, tested + `card_ids`-scoped code path (issue #533's own batch-scoping prerequisite already applied to + each) but no caller reachable from this module or `stream_full_catalog.py` - a channel that + never runs and a channel that runs and casts zero votes are indistinguishable from the + outside, which is the whole defect this wiring closes. + + WHY THESE FOUR AND NOT THE OTHER SIX UNWIRED IDENTITIES the same audit found (`docs/ + pipeline-fidelity-gate.md`'s roster has the full accounting): every one of these four reads + ONLY data this pass has already stored - `ImageEvidence` OCR text, `Card.content_phash`, + already-resolved artist/printing chains - and fetches no image, runs no tesseract, calls no + external API. `run_frame_mismatch_recovery`/`run_lands_identify` each accept an OCR/fallback + refetch budget for the LIVE-FETCH portion of their own pipeline; both are called here with + every such budget forced to 0, which their own docstrings document as "the scoped, genuinely + free [...] path" - the live-fetch branches stay unreachable from this conveyor, exactly like + every other calculator's own fetch work stays inside Stage C, never Stage D. + `deductive-backfill-v1`/`local-name-frequency-v1` were the other two candidates that looked + fetch-free on the same reading; both were left OUT because neither has a `card_ids` parameter + at all - each rebuilds a whole-catalogue in-memory index (`CanonicalNameIndex`, + 113k+ `CanonicalCard` rows) from scratch on every call, so wiring either one here would mean + paying that full-catalogue rebuild on every single micro-batch rather than once per pass. See + `docs/pipeline-fidelity-gate.md` for that finding and the other four EXPENSIVE identities + (`art-edge-continuity-v1`, `local-ocr-v1`, `local-phash-v1`, `local-fallback-v1`) this same + audit left deliberately unwired, with a reason and a tracked issue for each. + + ORDER: `run_frame_mismatch_recovery` runs BEFORE `run_d0_sibling_artist_propagation` + deliberately - the former calls `resolve_and_persist_artist` on every card it recovers, and a + freshly-resolved artist from one card in this batch is exactly the kind of signal the latter's + sibling-propagation read (`Card.canonical_artist`/`canonical_card__artist`/ + `inferred_canonical_card__artist`/`inferred_canonical_artist`) can pick up for a d=0 twin + later in the SAME batch. Getting this backwards costs nothing this batch cannot recover next + time it revisits the same card, but costs one batch's worth of reach for free. Otherwise order + is irrelevant here, same reasoning as `_run_attribute_chip_casters`' own docstring: none of + these four reads any other NEW calculator's output, only stored evidence and pre-existing + resolved fields. + + A MISSING TAG SEED MUST NOT DESTROY A MICRO-BATCH, same discipline as + `_run_attribute_chip_casters`. Only `run_ai_art_detector` can raise for this (a missing + "AI-Generated" `Tag` row) - `run_frame_mismatch_recovery` already degrades gracefully when + "altered-frame" is unseeded (skips the tag vote, still casts the artist vote; see its own + docstring), and the other two calculators here have no `Tag` dependency at all. By the time + this runs, the four printing calculators and the three attribute chips above have already + written their votes, so letting a `RuntimeError` out would fail the WHOLE dispatch over an + operator setup gap in one advisory tag - caught, logged, `stage_d_ai_art_votes` stays 0. + """ + from cardpicker.local_detect_ai_art import run_ai_art_detector + from cardpicker.local_lands_identify import run_lands_identify + from cardpicker.local_residual_classify import ( + run_d0_sibling_artist_propagation, + run_frame_mismatch_recovery, + ) + + try: + ai_art_result = run_ai_art_detector(run_id=run_id, dry_run=dry_run, card_ids=card_ids) + outcome.stage_d_ai_art_votes = ai_art_result.votes_written + except RuntimeError as exc: + logger.error( + "AI-art detector skipped for run_id=%s: %s Stage D's printing votes for this batch " + "are unaffected and already written. Run `seed_default_tags` to close this - until " + "then stage_d_ai_art_votes stays at zero on every dispatch.", + run_id, + exc, + ) + + residual_result = run_frame_mismatch_recovery( + run_id=run_id, + dry_run=dry_run, + ocr_refetch_budget=0, + fallback_refetch_budget=0, + card_ids=card_ids, + ) + outcome.stage_d_residual_artist_votes = residual_result.artist_votes_written + outcome.stage_d_residual_tag_votes = residual_result.tag_votes_written + + art_hash_result = run_d0_sibling_artist_propagation(run_id=run_id, dry_run=dry_run, card_ids=card_ids) + outcome.stage_d_art_hash_artist_votes = art_hash_result.votes_written + + lands_result = run_lands_identify( + run_id=run_id, dry_run=dry_run, sample_size=None, fetch_budget=0, card_ids=card_ids + ) + outcome.stage_d_lands_votes = lands_result.votes_written + outcome.stage_d_lands_already_voted = lands_result.already_voted + + def _run_stage_d( batch_ids: Optional[list[int]], run_id: str, @@ -1346,6 +1449,9 @@ def seam(step: str) -> None: seam("stage-d:attribute-chips") _run_attribute_chip_casters(run_id=run_id, card_ids=batch_ids, outcome=outcome, dry_run=dry_run) + seam("stage-d:evidence-only") + _run_evidence_only_calculators(run_id=run_id, card_ids=batch_ids, outcome=outcome, dry_run=dry_run) + def _partition_by_md5_verdict( batch_ids: list[int], @@ -1676,6 +1782,15 @@ def dispatch_micro_batch( "stage_d_illustration_votes": outcome.stage_d_illustration_votes, "stage_d_illustration_already_voted": outcome.stage_d_illustration_already_voted, "stage_d_slow_path_routed": outcome.stage_d_slow_path_routed, + "stage_d_border_chip_votes": outcome.stage_d_border_chip_votes, + "stage_d_frame_chip_votes": outcome.stage_d_frame_chip_votes, + "stage_d_bleed_chip_votes": outcome.stage_d_bleed_chip_votes, + "stage_d_ai_art_votes": outcome.stage_d_ai_art_votes, + "stage_d_art_hash_artist_votes": outcome.stage_d_art_hash_artist_votes, + "stage_d_lands_votes": outcome.stage_d_lands_votes, + "stage_d_lands_already_voted": outcome.stage_d_lands_already_voted, + "stage_d_residual_artist_votes": outcome.stage_d_residual_artist_votes, + "stage_d_residual_tag_votes": outcome.stage_d_residual_tag_votes, "stage_d_verdict_transfer_votes": outcome.stage_d_verdict_transfer_votes, "peak_rss_mb": peak_rss_mb, "lockout_trip_id": lockout_trip.trip_id if lockout_trip is not None else None, diff --git a/MPCAutofill/cardpicker/tests/test_stage_e_dispatch.py b/MPCAutofill/cardpicker/tests/test_stage_e_dispatch.py index 273463a0c..957092b2f 100644 --- a/MPCAutofill/cardpicker/tests/test_stage_e_dispatch.py +++ b/MPCAutofill/cardpicker/tests/test_stage_e_dispatch.py @@ -29,9 +29,18 @@ from django.test.utils import CaptureQueriesContext from cardpicker import stage_e_dispatch +from cardpicker.default_tags import seed_default_tags from cardpicker.harvest_fetch_limiter import GoogleFetchLockoutError from cardpicker.image_evidence import ExtractionResult from cardpicker.local_calculate_verdicts import JOIN_KEY_ANONYMOUS_ID +from cardpicker.local_detect_ai_art import AI_ART_ANONYMOUS_ID, AI_GENERATED_TAG_NAME +from cardpicker.local_identify_printing_tags import PHASH_ANONYMOUS_ID +from cardpicker.local_lands_identify import LANDS_ANONYMOUS_ID +from cardpicker.local_residual_classify import ( + ALTERED_FRAME_TAG_NAME, + ART_HASH_ARTIST_ANONYMOUS_ID, + RESIDUAL_CLASSIFY_ANONYMOUS_ID, +) from cardpicker.management.commands.run_image_evidence_cohort import ( MANIFEST_EXTRACTOR_CURRENT_VERSIONS, MANIFEST_EXTRACTOR_KEYS, @@ -40,8 +49,10 @@ _next_stage_d_backlog_ids, ) from cardpicker.models import ( + CardArtistVote, CardPrintingTag, CardScanLog, + CardTagVote, EnvelopeTrip, ImageEvidence, PilotRunLedger, @@ -66,10 +77,12 @@ dispatch_micro_batch, ) from cardpicker.tests.factories import ( + CanonicalArtistFactory, CanonicalCardFactory, CardFactory, CardPrintingTagFactory, ImageEvidenceFactory, + TagFactory, ) STREAMING_ON = override_settings(STAGE_E_STREAMING_ENABLED=True) @@ -625,6 +638,128 @@ def _fail_if_called(card, dpi=None): assert vote.printing_id == printing.pk +class TestEvidenceOnlyCalculators: + """The four channels `_run_evidence_only_calculators` wires into `_run_stage_d` + (2026-08-05): each test proves its own channel actually FIRES during a real + `dispatch_micro_batch` call - asserting on the vote/counter that lands on the batch's own + `DispatchOutcome`, never on the underlying calculator function called in isolation (that + isolated coverage already exists in each calculator's own test module). Every fixture below + gives its card(s) CURRENT `ImageEvidence` up front (`_full_evidence`) so Stage C is skipped + and the assertion is isolated to Stage D, matching `TestEndToEndMicroBatch`'s own convention.""" + + @STREAMING_ON + def test_ai_art_detector_fires_and_casts_a_tag_vote(self, db: Any, monkeypatch: pytest.MonkeyPatch) -> None: + seed_default_tags() + card = CardFactory(content_phash=1) + _full_evidence(card, artist_ocr_name="Midjourney") + + def _fail_if_called(card, dpi=None): + raise AssertionError("evidence-backed card should never re-fetch for Stage C") + + _install_stage_c_stub(monkeypatch, fetch_result=_fail_if_called) + + outcome = dispatch_micro_batch(card_ids=[card.pk]) + + assert outcome.status == "completed" + assert outcome.stage_d_ai_art_votes == 1 + vote = CardTagVote.objects.get(card=card, anonymous_id=AI_ART_ANONYMOUS_ID) + assert vote.tag.name == AI_GENERATED_TAG_NAME + + ledger = PilotRunLedger.objects.get(command="stage_e_streaming_dispatch") + assert ledger.counters["stage_d_ai_art_votes"] == 1 + + @STREAMING_ON + def test_ai_art_detector_missing_tag_seed_does_not_halt_the_batch( + self, db: Any, monkeypatch: pytest.MonkeyPatch + ) -> None: + # "AI-Generated" deliberately NOT seeded - mirrors _run_attribute_chip_casters' own + # missing-seed test convention: an operator setup gap in one advisory channel must not + # fail the whole dispatch, and the other three evidence-only channels still run. + artist = CanonicalArtistFactory() + printing = CanonicalCardFactory(artist=artist) + source_card = CardFactory(content_phash=555, canonical_card=printing) + sibling = CardFactory(content_phash=555) + for c in (source_card, sibling): + _full_evidence(c) + _install_stage_c_stub(monkeypatch, fetch_result=lambda card, dpi=None: (_ for _ in ()).throw(AssertionError)) + + outcome = dispatch_micro_batch(card_ids=[source_card.pk, sibling.pk]) + + assert outcome.status == "completed" + assert outcome.stage_d_ai_art_votes == 0 + assert outcome.stage_d_art_hash_artist_votes == 1 + + @STREAMING_ON + def test_residual_classify_fires_the_phash_only_path_and_casts_dual_yield_votes( + self, db: Any, monkeypatch: pytest.MonkeyPatch + ) -> None: + TagFactory(name=ALTERED_FRAME_TAG_NAME) + artist = CanonicalArtistFactory() + CanonicalCardFactory(name="Forest", image_hash=100, artist=artist) + card = CardFactory(name="Forest", content_phash=100) + _full_evidence(card) + # A durable flag from an EARLIER pass - this channel's own eligibility read, not + # something Stage C produces on this dispatch. + CardScanLog.objects.create(card=card, anonymous_id=PHASH_ANONYMOUS_ID, skip_reason="frame-mismatch") + + def _fail_if_called(card, dpi=None): + raise AssertionError("phash-flagged recovery must never fetch - it is the free path") + + _install_stage_c_stub(monkeypatch, fetch_result=_fail_if_called) + + outcome = dispatch_micro_batch(card_ids=[card.pk]) + + assert outcome.status == "completed" + assert outcome.stage_d_residual_artist_votes == 1 + assert outcome.stage_d_residual_tag_votes == 1 + artist_vote = CardArtistVote.objects.get(card=card, anonymous_id=RESIDUAL_CLASSIFY_ANONYMOUS_ID) + assert artist_vote.artist_id == artist.pk + tag_vote = CardTagVote.objects.get(card=card, anonymous_id=RESIDUAL_CLASSIFY_ANONYMOUS_ID) + assert tag_vote.tag.name == ALTERED_FRAME_TAG_NAME + + @STREAMING_ON + def test_art_hash_artist_propagation_fires_for_a_d0_sibling(self, db: Any, monkeypatch: pytest.MonkeyPatch) -> None: + artist = CanonicalArtistFactory() + printing = CanonicalCardFactory(artist=artist) + source_card = CardFactory(content_phash=555, canonical_card=printing) + sibling = CardFactory(content_phash=555) + for c in (source_card, sibling): + _full_evidence(c) + + def _fail_if_called(card, dpi=None): + raise AssertionError("d0 sibling propagation must never fetch") + + _install_stage_c_stub(monkeypatch, fetch_result=_fail_if_called) + + outcome = dispatch_micro_batch(card_ids=[source_card.pk, sibling.pk]) + + assert outcome.status == "completed" + assert outcome.stage_d_art_hash_artist_votes == 1 + vote = CardArtistVote.objects.get(card=sibling, anonymous_id=ART_HASH_ARTIST_ANONYMOUS_ID) + assert vote.artist_id == artist.pk + + @STREAMING_ON + def test_lands_identify_fires_the_evidence_backed_singleton_path( + self, db: Any, monkeypatch: pytest.MonkeyPatch + ) -> None: + artist = CanonicalArtistFactory(name="Rebecca Guay") + printing = CanonicalCardFactory(name="Plains", artist=artist, image_hash=7) + card = CardFactory(name="Plains", content_phash=7) + _full_evidence(card, artist_ocr_name="Rebecca Guay") + + def _fail_if_called(card, dpi=None): + raise AssertionError("evidence-backed lands card must never fetch at fetch_budget=0") + + _install_stage_c_stub(monkeypatch, fetch_result=_fail_if_called) + + outcome = dispatch_micro_batch(card_ids=[card.pk]) + + assert outcome.status == "completed" + assert outcome.stage_d_lands_votes == 1 + vote = CardPrintingTag.objects.get(card=card, anonymous_id=LANDS_ANONYMOUS_ID) + assert vote.printing_id == printing.pk + + class TestForceStageCReextract: """Issue #465's one conveyor change (`force_stage_c_reextract`, threaded through `dispatch_micro_batch` -> `_run_stage_c`) - `False` (the default, exercised by every other test diff --git a/docs/pipeline-fidelity-gate.md b/docs/pipeline-fidelity-gate.md index 37597fa83..756af6a87 100644 --- a/docs/pipeline-fidelity-gate.md +++ b/docs/pipeline-fidelity-gate.md @@ -1200,6 +1200,81 @@ casters for these two chips inside the live-fetch pilot and inside so both chips sat at zero rows with nothing able to re-derive them. See `docs/features/printing-tags.md`, "Who actually casts the attribute chips". +### Wiring status — the streaming conveyor's `_run_stage_d` (2026-08-05) + +The question this section answers: for EVERY identity the roster tether +derives from code (19 today, 2 of them not real vote-casting calculators — +`evidence-transfer-v1`/`question-feed-hypothetical-vote`, see +`CALCULATOR_ROSTER_ALLOWLIST` in `.github/scripts/docs_lint.py` — leaving +17 real vote-casting identities), does a full-catalogue pass (`run_pipeline`, +or the streaming conveyor's own `stage_e_dispatch._run_stage_d`) actually +INVOKE it, verified by a real call site rather than a name match against +this file? A prior claim in circulation held that a pass casts on "10 of +roughly 28" channels — the derived roster is 17 real identities, not ~28, +and as of this pass **11 of 17** are invoked by a real call site (up from 7 +before this pass — see below), 6 are deliberately unwired with a stated +reason. + +**Wired before this pass (7):** `stage-d-join-key-v1`, `stage-d-fallback-v1`, +`stage-d-illustration-v2`, `stage-d-slow-path-v1` (all four +`local_calculate_verdicts.py`, called directly by `_run_stage_d`), plus +`layout-class-cast-v1`/`frame-style-cast-v1`/`bleed-edge-cast-v1` (PR #654, +`_run_attribute_chip_casters`). `stage-d-slow-path-v1` is a router that +casts 0 votes by construction (see its own entry above) but IS invoked, +which is why it counts as wired here despite never appearing in a +vote-count table. + +**Newly wired by this pass (4)** — `ai-art-detector-v1`, +`lands-artist-decomp-v1`, `residual-classify-v1`, `art-hash-artist-v1`, via +a new `_run_evidence_only_calculators` step in `_run_stage_d`. All four +read only already-stored evidence (`ImageEvidence` OCR fields, +`Card.content_phash`, already-resolved artist/printing chains) and fetch +no image — `run_lands_identify`/`run_frame_mismatch_recovery` are called +with every live-fetch budget forced to 0, which their own docstrings +already documented as "the scoped, genuinely free [...] path". Per-card +cost was not independently benchmarked beyond what each calculator's own +docstring already measures (`run_lands_identify`'s cached +`CandidateNameIndex` build: 1.48s once per worker process, not per card; +every other cost is a plain indexed DB read) — no full-catalogue timing +run was performed as part of this pass, consistent with the constraint +that no management command or backfill ran against the live, contended +database while writing it. See the PR that shipped this wiring for the +per-channel dispatch-level tests (each proven to fire, and proven to fail +when the wiring call is removed). + +**Deliberately left unwired (6), with a reason and a tracked issue each:** + +- `art-edge-continuity-v1` — genuinely FREE, but the module's own author + gated it behind a stated validation precondition (agreement/false-positive + rate against Scryfall's own `frame_effects` ground truth) that has not + run. See its own entry below and + [issue #721](https://github.com/ProxyPrints/ProxyPrints.github.io/issues/721). +- `deductive-backfill-v1`, `local-name-frequency-v1` — read only + already-stored data (no fetch/OCR/network), but NEITHER has a `card_ids` + batch-scoping parameter: each rebuilds a whole-catalogue in-memory index + or census from scratch on every call, so wiring either into a + 25-card-at-a-time conveyor as-is would mean paying a full-catalogue-scale + cost on every single micro-batch. Classified EXPENSIVE on IMPLEMENTATION + grounds, not data-source grounds — a real finding, not an oversight. See + [issue #722](https://github.com/ProxyPrints/ProxyPrints.github.io/issues/722). +- `local-ocr-v1`, `local-phash-v1`, `local-fallback-v1` — these three + identities' PRIMARY caster (`run_pilot`/`run_fallback_for_card`) computes + its verdict from a real image for the first time, requiring a live CDN + fetch plus tesseract (OCR) or a hash computation over pixels (phash); + unlike the four newly-wired channels, there is no stored-evidence + reconstruction path for the PRIMARY computation (some votes under these + same identities are already cast for free as a side effect of OTHER + calculators' own evidence-only paths, e.g. `lands-artist-decomp-v1`'s + OCR-resolved branch — that is not this identity's own engine running for + free). See + [issue #723](https://github.com/ProxyPrints/ProxyPrints.github.io/issues/723). + +Backfill for the 4 newly-wired channels, against the EXISTING catalogue +(not future passes, which the wiring above already covers), is a SEPARATE +deliverable per this project's own practice (wiring ≠ backfill) — stated in +the PR that shipped this wiring, not run as part of it, since a +full-catalogue pass was live and the database was contended at the time. + ### Calculator roster — the three identities this page omitted (2026-07-29) Until 2026-07-29 this page enumerated **eleven** calculator identities. @@ -1275,6 +1350,7 @@ does not. See [`documentation-process.md`](documentation-process.md)'s non-extended black-bordered cards. That labelling is free and needs no human pass. Do not read its zero vote count as a measured statement about extended-art detection's yield — nothing has been measured yet. + Tracked: [issue #721](https://github.com/ProxyPrints/ProxyPrints.github.io/issues/721). - **`local-name-frequency-v1`** (`NAME_FREQUENCY_ANONYMOUS_ID`, [`MPCAutofill/cardpicker/local_identify_printing_tags.py`](../MPCAutofill/cardpicker/local_identify_printing_tags.py)) — **ZERO output of any kind. Under diagnosis; may be retired.** The @@ -1284,7 +1360,10 @@ does not. See [`documentation-process.md`](documentation-process.md)'s votes and no `CardScanLog` rows** — not even a skip log, which means it is not merely abstaining, it is not being reached. Whether it gets fixed or deleted is undecided; it is recorded here as a known hole - rather than left off the page. + rather than left off the page. Also missing a `card_ids` batch-scoping + parameter (2026-08-05 finding), which is a separate, more concrete blocker + than "under diagnosis" alone conveys — see the "Wiring status" section + above and [issue #722](https://github.com/ProxyPrints/ProxyPrints.github.io/issues/722). ### Calculator roster — the identity the tether itself could not see (2026-07-29)