Skip to content

THE MONOLITH: one command that runs the whole pipeline end to end (#618) - #660

Merged
WilfordGrimley merged 5 commits into
masterfrom
feat/monolith-one-command
Jul 30, 2026
Merged

THE MONOLITH: one command that runs the whole pipeline end to end (#618)#660
WilfordGrimley merged 5 commits into
masterfrom
feat/monolith-one-command

Conversation

@WilfordGrimley

@WilfordGrimley WilfordGrimley commented Jul 30, 2026

Copy link
Copy Markdown

What this is

One command that runs the whole identification pipeline end to end. Owner brief: "1 click". Every stage already existed and every stage was already run separately; what did not exist was anything that ran them together, in order, under one run_id.

manage.py run_pipeline

is a complete, working, from-scratch, whole-catalogue run that writes. No flag is required.

Stage 0   Scryfall reference refresh, once, at the front
Stage E   operating-envelope preflight
Stage C   evidence extraction (the pooled engine)
Stage D   join-key → fallback → illustration → slow-path, then the three attribute chips
Stage C+  distance-0 cluster vote propagation
Stage E   fidelity gate — machine-only resolutions must be zero
end       channel_report

This is assembly, not invention. run_pipeline.py contains no pipeline logic: it is imports, sequencing, run_id threading and error handling. Every stage is reached by calling the module that already owned it.

Wiring, per stage

stage what is called notes
0 stream_full_catalog.run_stage_zero_freshnessprinting_metadata_import full-set value-diffing upsert, so re-running IS the backfill
E operating_envelope.current_trip / check_envelope, stage_e_dispatch._sample_envelope_signals current_trip first, no self-resume
C run_image_evidence_cohort via call_command pooled engine, run-scoped resume (#645), md5 evidence transfer, RSS guard — none of it re-derived
D stage_e_dispatch._run_stage_d, batch_ids=None the one place the dependency order lives; carries the three chip casters from #654
C+ local_clustering.compute_two_threshold_clusters + local_identify_printing_tags.build_propagated_cluster_votes the pilot capability no engine had
E local_identify_printing_tags.verify_zero_resolutions the same gate local_calculate_verdicts runs between calculators
end channel_report via call_command reported, never folded into the exit status

Two thin extractions, no copies

Both lift an existing body to a module-level callable and have the original call it. Behaviour is unchanged.

  1. stream_full_catalog.Command._run_stage_zero_freshness → module-level run_stage_zero_freshness(require_fresh, is_resume, write, warn). It now also returns the bulk-file vintage (remote updated_at, cache path, cache mtime age, whether it refreshed, import stats), which the monolith records on its ledger row so a run's conclusions can be dated. stream_full_catalog's method is a wrapper that discards it and prints exactly what it always printed.
  2. run_pilot's propagate_cluster_vote closure → module-level build_propagated_cluster_votes(...), returning rows instead of writing them. It closed over six pieces of run_pilot-local state and was reachable from nothing; the closure now calls it and keeps its own batching and written-id ledgers.

Three parameters added to existing functions

stage_e_dispatch._run_stage_d / _run_attribute_chip_casters / _run_illustration_calculator:

  • batch_ids: Optional[list[int]]None is bulk mode, which every calculator underneath already supported. One Stage D now serves both engines.
  • dry_run: bool = False — threaded through, was hard-coded False.

Both defaults preserve existing behaviour byte-for-byte for the conveyor.

Write polarity — the organ table

Owner ruling: "the eventual intention for the monolith is that default is to write and flags are what prevents it. (opposite)". This is the inverse of every organ called. Inheriting their defaults is the dangerous failure: a pass that computes everything, logs every card, reports success and persists nothing is indistinguishable from a working run except by row counts.

organ its own gate what run_pipeline passes
import_scryfall_printing_metadata none — always writes called; skipped under --dry-run (no preview mode)
run_image_evidence_cohort --dry-run (already write-first) forwards --dry-run; nothing overridden
run_join_key_calculator dry_run=True default dry_run=False
run_fallback_calculator dry_run=True default dry_run=False
run_illustration_calculator dry_run=True default dry_run=False
run_slow_path_calculator dry_run=True default dry_run=False
run_layout_class_cast (border) dry_run=True default dry_run=False
run_attribute_chip_cast (frame + bleed) dry_run=True default dry_run=False
cluster vote propagation none (this PR's own write) gated on not dry_run

enforce_dry_run_precondition (the forced dry-run, #362) is deliberately not added: a write-by-default command has no --write to gate, and requiring a prior dry-run would put a flag back in front of the working run. Verified it only arms inside Stage C's --card-ids-file path — write_mode=(not dry_run) and bool(card_ids_file_for_scope) — so a bare run never trips it. --skip-dryrun-check is forwarded for the file path. Pinned by test_a_bare_run_never_trips_the_forced_dry_run_precondition.

--dry-run is a real pass that withholds the write, not a plan: every stage executes, every calculator reports what it would cast, and channel_report still runs. Exits 0.

Organs not wired (not in the brief's sequence): local_lands_identify, local_detect_ai_art, local_residual_classify, local_name_frequency_elimination (see finding 1), run_pilot's OCR/phash voting (standing owner deferral).

Verification

End-to-end test: cardpicker/tests/test_run_pipeline.py, 22 tests, drives the whole command over a 3-card fixture cohort. Only the network is stubbed (_fetch_one_card, _compute_one_card, and Stage 0's real download). Every Stage D calculator, all three chip casters, clustering, propagation, the gate and channel_report run for real against the test DB.

Mutation table — every stage unwired at the source, in turn:

# mutation result
M1 Stage 0 not called 🔴 2 failed
M2 envelope preflight not called 🔴 1 failed
M3 Stage C not called 🔴 3 failed
M4 Stage D not called 🔴 2 failed
M5 attribute chips not called 🔴 2 failed
M6 cluster propagation write removed 🔴 2 failed
M7 fidelity gate not called 🔴 1 failed
M8 channel_report not called 🔴 1 failed
M9 Stage D reverts to its organs' dry_run=True default 🔴 2 failed
M10 --dry-run leaks a write through Stage C 🔴 1 failed
baseline, unmutated 🟢 22 passed

M7 and M8 initially survived and the assertions were strengthened: violations == 0 is what you get whether the gate found nothing or was never called, and the CHANNEL REPORT banner is printed by run_pipeline itself either way. Both now assert a string only the stage can emit.

docs_lint.py --strict: clean. Full cardpicker/tests/ suite: 3541 passed, 8 skipped. CI: 13/13 green.

One cross-test defect found and fixed on the way: test_run_pipeline.py passed alone and halted every test at the envelope preflight inside the full suite. stage_e_dispatch._window is a process-global rolling fetch-outcome window that _sample_envelope_signals reads, and four fetch failures recorded by test_stage_e_dispatch.py earlier in the same pytest process tripped fetch_failure_rate at 4/4. Same autouse reset fixture that file already carries.

Findings — recorded, not fixed

Per the brief: this command's first run is how we discover what is broken, and changing what it measures while building it means the first reading cannot be trusted.

  1. run_name_frequency_elimination is a census predicate that leaks across runs. Its "exactly one unresolved eligible card for this name" test counts over _eligible_base_queryset called with no run_id (local_identify_printing_tags.py:2013), so the pool is depleted by its own prior votes. A name that gains a second card after run 1 can have that card voted on in run 2 where an empty-vote catalogue would abstain — a fresh wrong positive assertion, not a carried-forward one. It has never run in production, so the fix window is open. Not wired into the monolith.
  2. Only CardPrintingTag is archived on supersession. models.vote_archive_model maps that one model; CardTagVote (all three chip families) and CardIllustrationVote are deleted outright. So the printing grain stays diffable across generations via --generation-diff; the tag and illustration grains do not. CardIllustrationVote is additionally the only production caller of purge_stale_machine_votes that bypasses vote_write.purge_and_write_votes, so it would never record superseded_by_run_id even if registered.
  3. No retention janitor exists (Retention janitor: sweep the oldest runs of CardScanLog abstention history #575 is referenced in four places as forward-looking prose only). ArchivedCardPrintingTag has no deletion path at all except FK cascade. Nothing can sweep this run's rows.
  4. The attribute-chip and layout casters have no run_id scoping and no value-comparison split, so a card whose evidence is later re-extracted can never have its chip re-read under a new run. Monotonic suppression, not a false fire. Fresh surface — both were only wired into an engine in Wire the attribute-chip casters into an engine; strip the vote from the uncalled wrapper #654.
  5. _split_new_printing_tag_votes compares VALUES, so an identical recomputed verdict is skipped and the original row survives with its original run_id. Correct and desirable, but it means filter(run_id=R) returns what R changed, not what R concluded.
  6. PilotRunLedger.run_id is UNIQUE, so the monolith and its delegated Stage C cannot share a row. The data keeps the clean run_id and this command's summary row takes a -pipeline suffix — because channel_report scopes a channel's counts by the run_id on the rows, and a Stage C whose evidence carried a different run_id from the votes would read as a silent channel.
  7. Three write-gate conventions across ten commands (--write opt-in, --dry-run opt-out, and both plus enforce_dry_run_precondition). There is no single convention to inherit, which is what makes this surface error-prone even after this PR pins it. Noted, not fixed.
  8. bleed_diff_mm needs no backfill. image_evidence.compute_card_evidence already calls compute_bleed_diff_mm unconditionally (image_evidence.py:976). It was 97.9% NULL only because the field was added without an extractor version bump, so no existing row was re-extracted. A from-scratch run fills it.
  9. Stage C's writes still fire the evidence-change post_save echo. suppress_evidence_change_echo is a contextvars flag set by the dispatch loop; bulk-mode writes are deliberately unflagged, and the pooled engine's writes happen in worker processes/threads anyway. Since Stage D is invoked explicitly, the echo is redundant work rather than a correctness problem, and it is inert unless a django-q cluster is running with STAGE_E_STREAMING_ENABLED.
  10. docs/features/stage-e-operations.md is still not in .github/wiki-publish-map.json (issue 11 docs/features pages are absent from the wiki publish map, including stage-e-operations.md #657). Not fixed here, per the brief. Nothing enforces the absence — check_wiki_publish_map only validates listed entries.

What is thin

  • Stage E is a preflight, not a running envelope. The trip check happens once, before Stage C. Rate pressure and the global 7/s ceiling apply per request underneath Stage C (harvest_fetch_limiter / harvest_rate_coordinator) and are unchanged, but the monolith does not re-sample host load mid-pass the way the conveyor does per micro-batch.
  • Cluster propagation carries Stage D's verdicts, not the pilot's OCR/phash votes — see DEVIATIONS below.
  • Stage D runs in one bulk pass, so a kill loses that pass's uncommitted work. Stage C's within-run resume is unaffected.

Deviations

  1. Cluster propagation propagates Stage D's printing verdicts, not the pilot's OCR/phash votes. The brief says carry the clustering in and explicitly says not to carry pilot OCR/phash voting. Those two together leave no pilot vote to propagate, so the monolith propagates the votes the run actually cast, under the same identity, with the same already-voted guard. Same rule, different upstream.
  2. The monolith clusters over Card by stored content_phash (the whole catalogue), not over a selection pool. run_pilot clusters over its eligibility-narrowed pool, which makes membership a function of what earlier runs voted on — a genuine 3-card cluster can present as a 2-card one, and dropping the lowest-pk member changes which card becomes representative. Clustering over the catalogue is what makes the answer independent of run history.
  3. stage_e_dispatch._run_stage_d gained two parameters rather than the monolith re-listing its six calls. Re-listing would have been a second copy of the load-bearing dependency order.

Docs

docs/identification-pipeline.md and docs/features/stage-e-operations.md — the living pages, edited in place. No dated report.

Live state

Nothing deployed, nothing run against production. Branch feat/monolith-one-command; no migrations (checked origin/master's migration tree immediately before committing — none needed).

🤖 Generated with Claude Code

https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN

WilfordGrimley and others added 3 commits July 30, 2026 12:37
…ster-vote propagation

Neither piece is changed in behaviour; both are lifted from a caller-private scope
(a BaseCommand method / a closure over six pieces of run_pilot state) to a module-level
callable so a second engine can reach them without a copy.

- stream_full_catalog._run_stage_zero_freshness -> module-level run_stage_zero_freshness,
  taking write/warn callables instead of closing over self.stdout/self.style. It now also
  RETURNS the bulk-file vintage (remote updated_at, cache path, cache mtime age, whether it
  refreshed, the import stats) so a caller can date its own conclusions; the existing method
  is a wrapper that discards it and prints exactly what it always printed.
- local_identify_printing_tags.run_pilot's propagate_cluster_vote closure -> module-level
  build_propagated_cluster_votes, which returns the rows rather than writing them. The
  closure now calls it and keeps its own batching and written-id ledgers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
manage.py run_pipeline. A bare invocation is a complete, from-scratch, whole-catalogue
run that WRITES. Every flag either narrows the cohort, disables a stage, or withholds
the write; none is a precondition.

Stage 0  stream_full_catalog.run_stage_zero_freshness, and the bulk-file vintage it
         returns is recorded on the run's ledger row so the run can be dated
Stage E  operating_envelope.current_trip / check_envelope preflight
Stage C  run_image_evidence_cohort via call_command (pooled engine, run-scoped resume,
         md5 evidence transfer, RSS guard - none of it re-derived here)
Stage D  stage_e_dispatch._run_stage_d, called explicitly rather than reached by the
         post_save echo: join-key -> fallback -> illustration -> slow-path, then the
         border / frame-style / bleed-edge chips
Stage C+ local_clustering.compute_two_threshold_clusters +
         local_identify_printing_tags.build_propagated_cluster_votes - the pilot
         capability no engine had
Stage E  verify_zero_resolutions fidelity gate
end      channel_report, reported and never folded into the exit status

_run_stage_d/_run_attribute_chip_casters/_run_illustration_calculator gain two
parameters so one Stage D serves both engines: batch_ids=None is bulk mode, and
dry_run is threaded through. Both defaults preserve existing behaviour exactly.

Write polarity is inverted relative to every organ called: all six Stage D
calculators/casters default to dry_run=True, so the monolith passes dry_run
explicitly at every seam. Inheriting those defaults would compute a whole pass and
persist nothing while every log line reported success.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
docs/identification-pipeline.md gains a 'How to run all of it' section at the
front of the stage walkthrough - the sequence, that the command holds no
pipeline logic of its own, and the two load-bearing defaults (writes by
default, redoes everything from scratch). Stage 0's placement is noted where
the reference set is introduced.

docs/features/stage-e-operations.md gains the operational section: the
write-polarity organ table (which organ, its own gate, what run_pipeline
passes), how enforce_dry_run_precondition interacts with a write-by-default
command, what a dry run actually does, Stage 0's returned bulk-file vintage,
why Stage D is called explicitly rather than reached by the post_save echo,
the two deliberate differences between the monolith's cluster propagation and
the pilot's, exit codes, the ledger run_id suffix and why the DATA keeps the
clean name, and the recorded list of ways a fresh run still inherits from an
earlier one.

No dated report. docs_lint.py --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
WilfordGrimley and others added 2 commits July 30, 2026 13:19
The fidelity-gate and channel_report checks both passed with the stage
unwired: 'violations == 0' is what you get whether the gate found nothing or
was never called, and the CHANNEL REPORT banner is printed by run_pipeline
itself either way. Now assert a string only the stage itself can emit -
'FIDELITY GATE: clear over N cards' and channel_report's own roster family
title. Both mutations now go red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
…ests

test_run_pipeline.py passed alone and halted every test at the envelope
preflight inside the full suite: stage_e_dispatch._window is a process-global
rolling window that _sample_envelope_signals reads, and four fetch failures
recorded by test_stage_e_dispatch.py earlier in the same pytest process
tripped the fetch_failure_rate bar at 4/4. Same autouse reset fixture, same
reasoning, as that file already carries.

Full cardpicker/tests/: 3541 passed, 8 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
@WilfordGrimley
WilfordGrimley merged commit f947346 into master Jul 30, 2026
13 checks passed
WilfordGrimley added a commit that referenced this pull request Jul 30, 2026
…in test_valid_url (#663)

master went RED at f947346 (PR #660, the monolith) on
test_valid_url[scryfall]/[scryfall_with_www] - 2 failed, 3542 passed. The failure is
NOT in the monolith and touches no pipeline code: it is the third instance of a failure
mode this test already documents twice.

WHAT HAPPENED. `Scryfall.retrieve_card_list` fetches
`https://api.scryfall.com/decks/<id>/export/text` for real. CI received an HTML document
instead of the text export - `<!DOCTYPE html>` appears verbatim in the `base.py:64`
"Invalid response" log line - so `query_import_site` raised `InvalidURLException`.

WHY THIS IS NOT A SCRYFALL OUTAGE, AND WHY A RETRY IS NOT THE FIX. The endpoint is
healthy: a direct curl from outside CI returns HTTP 200, `text/plain`, and the expected
body. The immediately preceding master commit (1d433dd) went green on the identical
test 111 minutes earlier, and nothing between the two touches integrations. What CI got
was an edge/bot-challenge interstitial served to the GitHub Actions runner's address
space - a property of WHERE the request originates, not of the code or of Scryfall's
contract. The next runner sits in the same address space, so a re-run is a coin flip,
not a remedy.

THE FIX IS THE ONE THIS FILE ALREADY ESTABLISHED. tappedout.net (2026-07-20) and
manastack.com (2026-07-22) each hit this same class of live-network false-red and each
was mocked in place rather than skipped, so the site's own `retrieve_card_list` parsing
stays covered and only the transport is faked. Scryfall now gets the same treatment,
and it was the last un-mocked flaky caller left in this parametrize.

TWO THINGS THAT ARE EASY TO GET WRONG HERE, both handled and both commented in place:

  - THE HOST IS `api.scryfall.com`, NOT `Scryfall.get_host_names()`. The two existing
    mocks build their patterns from `get_host_names()` because those sites fetch from
    the host they are matched on. Scryfall does not: it matches the `scryfall.com` deck
    page a user pastes but requests `netloc="api.scryfall.com"`. A pattern derived from
    `get_host_names()` would match nothing and leave the test exactly as red.

  - THE MOCK IS SCOPED TO `/decks/`. `api.scryfall.com` also serves this suite's
    bulk-data, DFC and meld calls; a host-wide mock would gut
    `test_get_double_faced_card_pairs`/`test_get_meld_pairs`. `real_http=True` still
    carries every other Scryfall request to the real API unchanged.

The mocked body is the real response captured byte for byte (od -c verified: CRLF line
endings, no trailing newline), so `retrieve_card_list`'s own `"// Sideboard"` strip runs
on genuine input and the recorded snapshot is unchanged.

VERIFICATION (mutation red -> restore -> green, local, python 3.10):
  - baseline, before this change: test_valid_url[scryfall] passes locally, because this
    box's IP is not the one being challenged - which is itself the evidence that the
    failure is origin-dependent rather than code-dependent.
  - mutate the mocked body ("3 Past in Flames" -> "3 MUTANT"): 2 failed
    (scryfall, scryfall_with_www) on the snapshot comparison. This proves the mock is
    genuinely serving the request - had it fallen through to the real network, altering
    the mock body could not have changed the outcome - and that the snapshot assertion
    is live rather than vacuous.
  - restore: 13 passed, 2 skipped, 13 snapshots passed.
  - black --check: unchanged.


Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
WilfordGrimley added a commit that referenced this pull request Jul 30, 2026
…ess + census leak, mid-pass envelope re-sampling (#665)

FIX 1 - THE MD5 GROUP BEHAVES AS ONE UNIT THROUGH THE MONOLITH

Owner: "the md5 dedupe should only fetch each identical image once across sources and then
apply votes to the entire group as the fetched card passes through the monolith."

The fetch half already existed (`evidence_transfer`, keyed on `Card.md5_checksum`). The vote
half existed only on the phash distance-0 key, so the set that got a fetch saved and the set
that got a vote propagated were DIFFERENT SETS - byte-identical files always share a phash,
but files sharing a phash are not necessarily byte-identical.

CHECKED BEFORE BUILDING, as instructed: propagation is NOT redundant. `evidence_transfer`
gives every md5 sibling its own ImageEvidence row with byte-identical extractor values, so it
is reasonable to ask whether each member already reaches the same conclusion independently.
It does not, structurally: a Stage D printing deduction is not a function of the evidence row
alone. `_resolve_candidates_for_card` keys the candidate list on `Card.name`, and md5-identical
uploads from different sources routinely carry different names. Members also differ on per-card
eligibility. `test_the_unfetched_twin_has_no_verdict_of_its_own_without_propagation` falsifies
the "N independent deductions already agree" hypothesis on the fixture rather than arguing it.

Stage C+ now runs TWO tiers through ONE propagation engine (`_propagate_over_groups`), which
takes the grouping as a parameter and knows nothing about how it was keyed:

  md5       exact identity -> shares a PRINTING vote. New. Runs first.
  phash d0  unchanged from PR #660. Runs second, filling only what md5 did not.

Two defects in the PR #660 propagation are fixed on the way:

  - SOURCE VOTES WERE READ FROM REPRESENTATIVES ONLY. Stage D has no reason to reach a group's
    lowest pk first, so whenever it reached any other member, nothing propagated. Source votes
    are now read across every member, and one source per (group, identity) is chosen
    deterministically so two vote-holders in one group cannot generate duplicate rows inside a
    single write batch.
  - REPRESENTATIVES WERE NEVER PROPAGATION TARGETS. Same root cause, opposite direction.

PROPAGATION NEVER OVERRIDES A MEMBER'S OWN INELIGIBILITY (owner constraint). A member already
resolved, already confirmed to a `canonical_card`, not a CARD, or carrying a resolved
custom-art/non-english tag is skipped. custom-art is the catalogue DECLARING the image is not a
faithful depiction of a printing; a checksum must not overturn that.

THE PHASH TIER IS LEFT IN PLACE, flagged rather than accreted. Issue #661 holds what phash
grouping is FOR - the owner's direction is that it should eventually share an ILLUSTRATION
(same artwork, possibly a different printing), not a printing verdict. Removing it now would
itself be a behaviour change, and it is currently the only propagation reaching cards with no
md5 at all (md5 is NULL for every LOCAL_FILE source by design). The `groups` parameter is the
seam that tier plugs into later.

FIX 2a - `run_name_frequency_elimination` NEVER LOOKED AT THE IMAGE

Owner: "just because a card was printed exactly once doesn't mean that the image in our
catalogue is an accurate depiction of that card, it may have a different border or another
issue." Owner leaned toward adding the conjunct rather than dropping the tier; the conjunct is
what shipped, and the reasoning for keeping the tier is in its own docstring.

Everything the 1:1 gate checked was a COUNT. Counting establishes that IF the card depicts one
of the name's printings THEN it is the uncovered one; nothing established the antecedent, and
the only filters that spoke to it were the DECLARED custom-art/non-english tags - so an
untagged altered border sailed through. "It is only a vote" is weaker than it sounds: #593
established a machine vote is what the question feed renders as the suggestion to confirm, and
the human's click returns as a full-weight USER vote.

The missing conjunct now requires the card's ALREADY-STORED evidence to be consistent with the
candidate printing. NOT a new implementation: `_apply_agreement_checks`' border/frame check was
lifted to `local_identify_printing_tags.printing_attribute_disagreement` and both callers now
share it. That direction is forced - `local_calculate_verdicts` imports
`local_identify_printing_tags`, never the reverse. Sharing also inherits PR #656's `artist_ocr`
gate for free, which is the half a second copy would most likely have got wrong.

NO STORED EVIDENCE MEANS ABSTAIN. This module's "missing data is not evidence" rule protects a
match from being VETOED by silence; here silence is being asked to ESTABLISH something, so it
points the other way. Counted separately from mismatches so the cost is legible.

FIX 2b - THE CENSUS LEAK (a fresh wrong positive, not a stale vote)

`_eligible_base_queryset(NAME_FREQUENCY_ANONYMOUS_ID)` was called with no `run_id`, making its
"exclude cards already carrying this calculator's vote" LIFETIME. The gate is a COUNT over
exactly that population, so the calculator was taking a census over a pool it permanently
shrinks itself: run 1 votes on a card, a second upload of that name arrives, and run 2 sees one
unresolved card where there are really two - and votes. Nothing about the second card changed;
only the size of the population the gate counts.

`compute_covered_printing_pks()` stays catalogue-wide and unscoped, deliberately: "covered" is
a fact about the world, not about this calculator's progress. `run_pilot`'s `select_candidates`
and `count_below_resolution_floor` are LEFT UNSCOPED - neither gates on a count over the
returned population, so neither has this defect. Stated in `_eligible_base_queryset`'s docstring
so the asymmetry is visible from the function rather than only from its callers.

FIX 3 - THE MONOLITH RE-SAMPLES THE ENVELOPE MID-PASS

Owner: "host resampling is likely required (for steps that aren't fetch) as the same monolith
will run for small datasets and large ones so needs to fit the available compute appropriately."

PR #660 checked the envelope ONCE, before Stage C. `_EnvelopeSentry` now re-samples at every
stage seam: after Stage C, between each of Stage D's calculators/casters (via a new OPTIONAL
`envelope_check` callback on `stage_e_dispatch._run_stage_d`, defaulting to None so the
conveyor is byte-identical), and before each Stage C+ tier. Sample counts land on the ledger.

HALT SEMANTICS PRESERVED. A breach still persists an EnvelopeTrip, still exits 3, still needs
`resolve_envelope_trip` - no self-resume, and NOT converted to a throttle (that is rate
pressure's channel, beneath Stage C, PR #644). A mid-pass halt message differs from the
preflight's: rows already written STAY written, and it names the `--run-id` to resume with.

Interval-gated at 60s so the check cannot become its own load. The number is derived, not
tuned: the host-load bar reads the ONE-MINUTE load average, so sampling faster re-reads a
number that has not finished moving.

RESIDUAL, reported not hidden: the seams are BETWEEN calculators, not inside them. Closing that
gap means threading a callback into each of seven calculators' own batch loops - a real
refactor of shared code, deliberately not done here.

DELIBERATE DUPLICATION, WITH A TRIPWIRE. `_members_eligible_for_a_propagated_vote` expresses
four catalogue-level facts `_eligible_base_queryset` also expresses. It does not call that
function (which bundles workload rules wrong for a propagation target) and that function could
not be refactored to expose them (its own docstring records that tests and
`stream_backstop_sweep` assert against its COMPILED SQL).
`TestPropagationEligibilityMatchesTheBaseQueryset` fails if the two ever disagree.

VERIFICATION - mutation red, restore green (7 mutants, all red; 263 tests green restored):

  M1  md5 tier returns no groups                     3 failed
  M2  source votes read from representatives only    2 failed
  M3  propagation ignores member ineligibility       1 failed
  M4  envelope re-sampling reverted to preflight     2 failed
  M5  visual conjunct never disagrees                1 failed
  M6  no-evidence no longer abstains                 1 failed
  M7  propagation eligibility drops the tag excludes 1 failed (the tripwire)

Suites: test_run_pipeline, test_local_identify_printing_tags,
test_local_calculate_verdicts, test_stage_e_dispatch - 520+ tests, all green.
No model changes, so no migration.

Docs: living pages only, no dated report - docs/identification-pipeline.md (Stage C+ md5
section), docs/features/printing-tags.md (both name-frequency fixes),
docs/features/stage-e-operations.md (mid-pass re-sampling).


Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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