Skip to content

Add Freedom to Vote test and configurable competitiveness metrics to the evaluation panel - #723

Open
fangge518 wants to merge 32 commits into
devfrom
feat/ftv-proportionality-test
Open

Add Freedom to Vote test and configurable competitiveness metrics to the evaluation panel#723
fangge518 wants to merge 32 commits into
devfrom
feat/ftv-proportionality-test

Conversation

@fangge518

@fangge518 fangge518 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

The evaluation panel's "Election Results and Partisanship" section now scores each plan against the Freedom-To-Vote Test: a plan is presumed fair if its disproportionality (seat share minus vote share) stays within max(7%, 1/k) — k being the district count — for at least 3 of the 2 most recent Presidential and 2 most recent Senate elections. A paragraph plus a per-election table (D/R vote share and seat share, following the section's existing Dem/Rep toggle, plus a pass/tilt verdict) names the verdict, the 4 test contests, and the bound — phrased as whichever of "7%" or "1 out of k seats" actually binds. The 4 election keys are individually hoverable, both in the paragraph and in the table's own column headers, and highlight their matching row across all three partisan tables in this section; hovering the elections-passed count does the same for the passing subset. All of these use <span role="button"> triggers rather than <button> (matching HelpTip's own default trigger) so selecting and copying the paragraph text works normally; a plain <button> was interrupting that. Dropped the section's old "found in the political science literature..." disclaimer sentence, which no longer fit ahead of a results-and-methods paragraph.

Every metric the FTV table needs is already returned by the evaluation API (disproportionality, seats, vote_shares) — election columns follow a uniform {pres|sen}_{yy}_{dem|rep} naming convention across every state's gerrydb table, so a new selectFtvElections helper in app/src/app/utils/elections.ts picks the 2 most recent Presidential and 2 most recent Senate election keys by year, and PartisanSection.tsx scores and renders them.

State coverage (checked against dev's districtrmap/gerrydb tables across all 65 visible v2 state maps):

  • 56 states have pres_24, pres_20, and Senate data recent enough to run the intended test.
  • 9 states — AR, CT, ME, MI, NJ, OK, OR, PA, SD — are still missing pres_24 in dev, so the test falls back to pres_20/pres_16 for them. Likely a pipeline backfill gap rather than permanent unavailability; filed as a follow-up (PA is one of the paper's marquee gerrymandering states, worth prioritizing).
  • DC has only one Senate election in the dataset, reflecting its non-voting Senate representation, and shows "Not applicable" instead of a scored verdict. Any other map missing either election pair renders the same way.

Configurable competitiveness threshold and district hover-highlighting

The Competitiveness section's swing/competitive classification used to run server-side against one fixed ±3% band, discarding the per-district data behind it. competitive_metrics (backend/app/evaluation/partisans.py) now returns the sweep/swing classification as district-ID lists — dem_sweep_districts, rep_sweep_districts, swing_districts — independent of any band, since sweep/swing is just plain-majority win/loss per election, plus a flat sorted contest_dem_vote_shares list (one entry per district × election pair). The frontend classifies "competitive contests" at whatever band the user picks by filtering that list, instead of duplicating the classification logic client-side. n_districts/ n_elections stay in the payload directly, computed from context rather than derived from the lists, so the metric stays self-describing even for districts with zero election columns. competitiveness's version bumps 1 → 2 for the shape change.

Frontend: a Select control (±2/3/5/7/10 points, defaulting to ±3 to match the old fixed behavior), inlined directly into the explanatory paragraph rather than on its own line, re-filters contest_dem_vote_shares client-side — no refetch. The Swing/Dem Sweep/Repub Sweep district rows hover-highlight their actual districts on the map, which needed useDistrictHover (app/src/app/hooks/useDistrictHover.ts) generalized to accept a list of districts instead of one — a single district is just the one-element case — updating its 4 existing call sites (BasicsSection, CompactnessSection) to pass arrays.

District hover-highlight, restyled per design feedback: the highlighted district's outline is now a vivid orange (HIGHLIGHT_LINE_COLOR, #ff7a00), and every non-hovered district dims to 45% white overlay while one is focused — the same masking convention CountyLayers.tsx already uses for the Super Draw paint-mask, applied here to PublicDistrictLayers via a new conditionally-rendered mask layer keyed off MapLibre feature-state. Hover state itself lives on the existing mapStore (hoveredPublicZones/setHoveredPublicZones) rather than a new store, since useDistrictHover already depended on mapStore for the map ref.

Also in this branch

Testing the new HelpTip surfaced a pre-existing bug in the shared HelpTip component, unrelated to FTV: selecting and copying its hover text left the card stuck open permanently. Radix's own HoverCard close logic refuses to schedule its close timer once any text inside the card has been selected, and that internal flag only clears on a fresh pointerdown inside the content — combined with this component's own onPointerDownOutside preventDefault (added earlier for a different, click-to-reopen bug), there was no remaining path to close the card. Fix: onPointerDownOutside now dismisses the card on any outside click except one that lands back on the trigger itself (preserving the original click-to-reopen guard), giving a manual escape hatch that leaves Radix's hover-based auto-close untouched — it still correctly keeps the card open while text inside it is being selected.

Tested

  • bun run ts (incremental typecheck) passes cleanly.
  • pytest tests/test_partisan.py passes (33 passed), including the new competitive_metrics shape and the zero-elections edge case.
  • Evaluation panel's FTV paragraph and table render correctly for a scored state, and selecting/copying the paragraph text works (visual check in the running app).
  • HelpTip card dismisses on an outside click after selecting and copying its text, and text selection inside the card still works normally (visual check in the running app).
  • Competitiveness threshold dropdown re-filters the competitive- contests count, and the Swing/Dem Sweep/Repub Sweep rows highlight the correct districts on hover (visual check in the running app).
  • District hover renders the orange outline and dims every other district correctly (visual check in the running app, confirmed by the human).

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

🧹 Preview torn down

Preview resources for this PR have been destroyed (preview label removed).

@fangge518
fangge518 changed the base branch from dev to feat/static-copy-update August 21, 2026 19:13
@fangge518
fangge518 force-pushed the feat/ftv-proportionality-test branch from 83797ac to 9c9d2f6 Compare August 21, 2026 19:14
@fangge518 fangge518 changed the title Add Freedom to Vote proportionality test to the evaluation panel Add Freedom to Vote test and configurable competitiveness metrics to the evaluation panel Aug 21, 2026
@fangge518
fangge518 force-pushed the feat/ftv-proportionality-test branch from 9c9d2f6 to e4d53ea Compare August 21, 2026 21:07
Base automatically changed from feat/static-copy-update to dev August 26, 2026 17:41
Surfaces the S.2747 FTV redistricting test on the evaluation panel: a
plan passes if its disproportionality stays within max(7%, 1/k) for at
least 3 of the 2 most recent Presidential and 2 most recent Senate
elections. Computed entirely from data the API already returns (no
backend change) since election columns follow a uniform
{pres|sen}_{yy}_{dem|rep} naming convention across all states.

States without both election pairs (e.g. DC, which has no full voting
Senator) show "Not applicable" instead of a scored verdict.
The dedicated Freedom to Vote Test table duplicated data already shown
in the Proportionality table above it. Replaced with a single sentence
appended to Other Partisanship Metrics' intro paragraph, computing the
same pass/fail verdict inline. A HelpTip on "Freedom-to-Vote test"
carries the methodology detail and cites S.2747 as proposed but not
enacted legislation, plus the Duchin/Schoenbach paper it's drawn from.
Match the dictated phrasing verbatim (Freedom-To-Vote capitalization,
"absolute value of disproportionality", "larger than or equal to 3")
instead of the earlier paraphrase.
…d text

Hovering "the 4 recent statewide elections" in the FTV sentence now
outlines the corresponding Disproportionality cells in both the
Proportionality table and the Other Partisanship Metrics table, so a
reader can see exactly which rows the sentence refers to. The
Freedom-To-Vote test HelpTip trigger switches from an info icon to the
same underlined-dotted text style, matching BasicsSection's existing
hover-highlight convention.
<button> defaults to inline-block with nowrap text, invisible on
BasicsSection's short single-token labels but breaking layout for the
longer "the 4 recent statewide elections" trigger. Override to
display: inline / white-space: normal so it wraps like the
surrounding paragraph text.
Hovering "N election(s)" now highlights just the subset of the 4 FTV
rows that actually pass the threshold, distinct from hovering "the 4
recent statewide elections" (all 4). Handles singular/plural. Also
removed fontWeight: bold from HOVER_BTN_STYLE — the underline already
signals hoverability, and these triggers explain rather than emphasize.
Radix's own HoverCard close logic refuses to schedule its close timer
once any text inside the card has been selected (hasSelectionRef in
its source), and that flag only clears on a fresh pointerdown inside
the content. Combined with our own onPointerDownOutside preventDefault
(added earlier to stop a click-to-reopen bug), there was no path left
to close the card after copying its text and moving the pointer away.

Fix: HelpTip now owns closing the same way it already owns opening —
its own pointerenter/pointerleave timer on both the trigger and the
content, fully independent of Radix's internal selection-tracking
state.
…lose timer

The previous fix (own close timer on pointerleave) closed the stuck-open
bug but also broke Radix's legitimate "don't interrupt an active text
selection" protection, making the card's text painful to select/copy.

Reverted that timer entirely. Fix instead: HoverCard's onPointerDownOutside
now dismisses on any outside click except one that lands back on the
trigger itself (preserving the original click-to-reopen guard) — giving a
guaranteed manual escape hatch out of Radix's "stuck open after a
selection" state, without touching the hover-based auto-close at all.
BasicsSection and PartisanSection each defined their own HOVER_BTN_STYLE
with five identical properties (background, border, padding, font,
textDecoration) and only cursor/fontWeight/wrap behavior actually
differing per use case. Factored the shared base into a new file; each
caller layers its own overrides on top.
…lt cursor

HOVER_BTN_STYLE is now a single constant (default cursor, no bold) used
directly everywhere; bold is merged in per instance instead of being
baked into per-file variants. Triggers naming a specific result value
(District N, N election(s)) stay bold; triggers introducing or
explaining a concept (Freedom-To-Vote test, the 4 recent statewide
elections) don't. The wrap fix for the one long trigger phrase moves to
its own WRAPPING_HOVER_BTN_STYLE, layered on the shared base.
The two branches of the FTV sentence (scored vs not-enough-data) each
rendered an identical HelpTip trigger; hoisted it to one ftvHelpTipTrigger
element and reused it, and split the ternary into two ftvPassCount checks.

Also dropped WRAPPING_HOVER_BTN_STYLE — it didn't actually fix wrapping
in practice, so the "the 4 recent statewide elections" trigger now just
uses the plain shared HOVER_BTN_STYLE like the others.
…shares

New metric alongside the existing competitiveness aggregate (untouched,
same fixed ±3% band). Returns dem_sweep_districts/rep_sweep_districts/
swing_districts as district-ID lists (independent of any threshold —
sweep/swing is just plain-majority win/loss per election) and a flat
sorted contest_dem_vote_shares list (one entry per district x election
pair) for the frontend to apply any competitiveness band to without
duplicating classification logic server-side.

Cross-checked against the same grid_context ground truth
competitive_metrics is validated against, plus a hypothesis fuzz test
mirroring the existing one for competitive_metrics.
onDistrictEnter now takes (number | string)[] instead of a single
zone — a single district is just the one-element case. All four
existing call sites (BasicsSection x3, CompactnessSection x1) wrap
their zone argument in an array. Enables highlighting a whole
category of districts (swing, sweep) at once, not just one.

Also adds the competitive_districts field to DocumentEvaluation,
matching the new backend metric.
…tead

Reverts the previous approach of adding a second, parallel metric.
competitive_metrics (key "competitiveness") now returns the district-ID
lists and sorted contest-share array directly, replacing its old fixed
±3%-band aggregate counts. version bumped 1 -> 2 since the output shape
changed, per registry.py's own documented rule for when
CURRENT_PAYLOAD_VERSION should flip.

Existing tests updated to match: the two ground-truth constants
(originally generated against gerrychain/gerrytools reference values)
are unchanged as plain reference numbers, just no longer literal
CompetitiveMetrics instances since that shape moved; assertions check
cardinalities and the ±3% contest count via a shared helper instead of
dict equality.
Matches the backend's reshaped competitive_metrics: one field
(competitiveness), not two. PartisanSection.tsx still reads the old
n_* aggregate fields — fixed in the next commit alongside the
competitiveness threshold picker and hover-highlight.
…eshold + hover

- FTV verdict sentence replaced with a paragraph + per-election table
  (R vote share / R seat share / close-enough verdict), closing with the
  pass-count sentence. Threshold phrasing now states whichever bound
  actually binds ("one out of k seats (X%)" vs "7%").
- All FTV hover triggers (HelpTip, "the 4 recent statewide elections",
  "N election(s)") switched from <button> to <span role="button">,
  matching HelpTip's own default-trigger pattern — buttons were
  interrupting text selection/copy on the paragraph.
- Competitiveness section: user-configurable band (±2/3/5/7/10, default
  3, matching the old fixed backend behavior) re-filters
  contest_dem_vote_shares client-side. Swing/Safe Dem/Safe Rep rows
  hover-highlight their actual districts on the map via the generalized
  useDistrictHover.
…ing again

Dropping these in favor of deriving them frontend-side (summing the
three district lists, reusing another metric's election count) turned
out to have two real divergence risks: the zero-elections branch would
report n_districts=0 even when real districts exist with no election
data, and the frontend's election count came from a differently
prefix-filtered source than this metric's own context.elections.
Metrics also fail independently (MetricsEnvelope.failed), so leaning on
a different metric's data for this one's own labels was a real
coupling, not just a style choice.

n_districts/n_elections computed directly from context, not derived
from the lists, so they stay correct in the zero-elections edge case.
Frontend now reads them from competitiveness itself. Version stays at 2
(not yet merged, nothing to invalidate). New test locks in the
zero-elections edge case explicitly.
Dropped the "found in the political science literature..." sentence
entirely. Rewrote the FTV explanation to the human's exact dictated
wording rather than a paraphrase: PASSES/DOES NOT PASS in caps, the
hover trigger relabeled to "the last two Senate races and the last two
Presidential races", the four contests spelled out in a parenthetical
right after, and the bound split into its own sentence as "1 out of k
seats (X% of the seat share)".
Hovering "the last two Senate races..." (or the elections-passed count)
no longer highlights the new FTV table's own rows — that table is
already scoped to exactly those 4 elections, so highlighting it added
nothing. Added the highlight to the Proportionality table's Vote Share
and Seat Share cells instead, alongside the existing Disproportionality
cell, since the FTV table shows exactly those two figures (as R vote/
seat share) and the reader can now see where they came from.
"This is close enough N out of 4 times, so it passes/does not pass the
test. (3 out of 4 are needed to pass.)" — the hover trigger now wraps
"N out of 4 times" instead of "N election(s)".
"...(specifically, whether absolute disproportionality stays within
max(7%, 1/number of seats))..." — states the actual pass criterion,
not just what it's testing for.
…y-update

feat/static-copy-update's intro paragraph hardcoded "47-53" as the
competitive band, written before this branch's threshold picker
existed. Changed to defer to the picker instead of stating a fixed
number that the picker can now change.
The dropdown sat between two separate <Text as="p"> blocks, so it
rendered on its own line. Merged into one paragraph with the Select as
an inline child instead, matching the human's intent to have it follow
the sentence directly. Also fixed a doubled "competitive competitive"
left over from the in-progress edit.
Each of the 4 parenthetical election keys is now its own hover trigger
(hoveredFtvKey state, reusing isFtvHighlighted) — hovering one
highlights just that row across the other tables, instead of only the
all-4/all-hovered states already available via the two sentence-level
triggers. Also added the missing "and" before the last item: "(2024
Pres, 2020 Pres, 2020 Sen, and 2016 Sen)".
Same hoveredFtvKey trigger as the parenthetical election list — hovering
a column header highlights that election's row in the Proportionality
and Other Partisanship Metrics tables (not this table's own cells,
consistent with the earlier call that this table shouldn't self-highlight).
Reverses the earlier "always Republican" convention: repVoteShare/
repSeatShare become povVoteShare/povSeatShare, returning the Dem share
directly when pov is dem, 1-share when rep. Row labels switch between
"D"/"R" to match. The "Repub tilt"/"Dem tilt" verdict wording is
unaffected — it names the actually-favored party regardless of pov,
not a pov-relative framing, so nothing there needed to change.
@fangge518
fangge518 force-pushed the feat/ftv-proportionality-test branch from e4d53ea to eeb8661 Compare August 26, 2026 17:41
@fangge518 fangge518 self-assigned this Aug 26, 2026

@nofurtherinformation nofurtherinformation left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few non-blocking notes from me, but otherwise looks clean!

Full agentic notes below (new skill version is a bit more plain language, I'm hoping)

Summary

PR Review: Add Freedom to Vote test and configurable competitiveness metrics to the evaluation panel (#723)

Verdict: APPROVE WITH COMMENTS

Overview

This PR adds a Freedom-To-Vote proportionality test to the evaluation panel's partisan section, reshapes the backend competitiveness metric to return district-id lists plus a flat list of per-contest vote shares so the frontend can pick its own competitiveness band, generalizes the district hover hook to take a list of districts, and fixes a pre-existing HelpTip bug where the card stayed open forever after copying its text. It does what the description says, and I found nothing in the diff that the description leaves out.

Verified by execution: CI is green, the partisan test file passes locally (30 pass; the 3 errors are database-only tests untouched here), TypeScript, ESLint, Prettier, and ruff at the repo's pinned version are all clean on the changed files. I also confirmed against the dev database that every state's election columns use the two-digit-year naming the new selectFtvElections helper depends on. The test reviewer went further and ran mutation tests against the new backend code.

Good work worth calling out: the backend sweep and swing computation now aligns on the pandas index instead of relying on positional arrays, which removes a hidden ordering assumption. The frontend degrades gracefully if it receives an old-shape payload. The HelpTip fix is narrowly scoped and its long comment accurately describes Radix's internal selection guard.

Headline risks, none blocking. First, if the backend's disproportionality metric fails for a plan (it raises on zero votes, and failures are isolated per metric), the page shows a confident "DOES NOT PASS" instead of "not enough data". Second, the new sweep lists, which drive map highlighting, have no test that checks which districts land in which list: swapping the Dem and Republican lists passes every test. Third, two small rendering and accessibility slips in the new table. All four Important items are cheap to fix and independent of each other, so this is Approve With Comments rather than Request Changes.

Severity summary

Severity Count
High 0
Important 4
Personal preference 5
Opinion 3

Inline notes

app/src/app/components/EvalPanel/PartisanSection.tsx

  • L112 IMPORTANT [correctness] A missing disproportionality value is scored as a failed contest, producing a false "DOES NOT PASS"ftvPassingKeys keeps only keys where disprop !== undefined and within bound, so a key with seats but no disproportionality counts as a failure. If the backend's disproportionality metric fails for this plan (it raises ZeroDivisionError on an election with zero D+R votes, and backend/app/evaluation/main.py isolates per-metric failures so seats still arrives), the paragraph says "DOES NOT PASS" and "0 out of 4 times" while every verdict cell shows a dash. Fix: set ftvPassCount to null when any of the four FTV keys lacks a disproportionality value, so the existing "Not enough data" sentence renders instead.
  • L738, L755, L772 IMPORTANT [repo-coherence] HOVER_BTN_STYLE un-bolds the three district-count cells — the shared style's inline font: 'inherit' resets font-weight and beats Radix's class-based weight="bold", so the Swing, Dem Sweep, and Repub Sweep counts render at normal weight while the two bold count cells above them do not. Verified from Radix's stylesheet. BasicsSection and line 507 of this file already avoid this with {...HOVER_BTN_STYLE, fontWeight: 'bold'}. Same fix here, or split the button-only reset keys out of the shared constant.
  • L142, L381, L396, L419, L505 IMPORTANT [a11y] Five <span role="button"> triggers announce as buttons but do nothing when activated — no onClick, no Enter or Space handler, no label describing the highlight. BasicsSection uses a real <button type="button"> for the identical hover-to-highlight affordance, and DistrictLabel.tsx only sets role="button" when there is a click handler. Radix Themes HoverCard.Trigger is asChild, so a <button> child works inside HelpTip too. If the plain <button> was interrupting text selection, that is likely fixable with userSelect rather than by dropping the element. Alternatively drop role="button", keep tabIndex={0}, and add aria-describedby or title.
  • L191 PREF [correctness] Band filter drops exact-boundary shares because of float subtractionMath.abs(s - 0.5) <= 0.03 is false for s = 0.53 (0.53 - 0.5 is 0.030000000000000027). Verified in node for bands 2, 3, and the top of 5. The old backend and this PR's own test helper use inclusive 0.47 <= s <= 0.53. Suggest s >= 0.5 - band && s <= 0.5 + band. Needs an exact percentage share to matter, so rare.
  • L728, L745, L762 PREF [repo-coherence] cursor: pointer on rows that do nothing when clicked — CompactnessSection rows use pointer because clicking zooms; CountySplitsSection.tsx is the hover-only analogue and uses default. The inner text inherits cursor: default from the shared style, so the cursor changes shape across a single row. Either cursor: 'default', or add a click that zooms to the group.
  • L79 PREF [repo-coherence] sortElections and LEVEL_ORDER live in the component while their sibling helper went to utils/elections.ts — see the note on elections.ts below. Would you consider moving them together?
  • L108 OPINION [correctness] Partially drawn plans get an unqualified PASS or DOES NOT PASSk is seats.total, which is the count of non-empty districts. A plan with 3 of 14 districts drawn gets a 33% bound. Every other partisan metric already behaves this way, but this is the first one shown as a binary legal verdict. BasicsSection already computes completeness if you want to gate the sentence. Product call, no change requested.
  • L157 OPINION [correctness] Rep-POV seat share is 1 - dem/total, which differs from the Proportionality table's rep/total when a district ties — the FTV form is actually the one consistent with the verdict math, and exact ties are essentially nonexistent in precinct data. Noting only because two adjacent tables can disagree on the same number. No change requested.

backend/tests/test_partisan.py

  • L433 (helper), L377 and L605 (expected counts), L1008 (fuzz) IMPORTANT [tests] No test pins which districts land in the Dem-sweep and Rep-sweep lists — arguably High. Both gerrychain fixtures expect 0 Dem-sweep and 0 Rep-sweep districts, so the cardinality checks are 0 == 0 regardless of which party a list is labeled. Mutation runs confirmed: swapping the two output lists, swapping dem_wins and rep_wins, and disabling sweep detection entirely all pass all four competitiveness tests. These lists are what the map now highlights. Cheap fix: a 3 or 4 district, 2 election _StubEvaluationContext fixture (the zero-elections test at line 466 is the template) asserting exact lists such as dem_sweep_districts == [1], rep_sweep_districts == [2], swing_districts == [3, 4]. In the fuzz test, build the oracle from ctx.dem_wins and assert set equality.
  • L452, L1024 PREF [tests] contest_dem_vote_shares party orientation and exact contents are unverified — the assertions are a symmetric 47 to 53 count, range, and sortedness, so emitting Rep share instead of Dem share passes. The frontend filter is symmetric today, so nothing would break, but the field is documented as Dem share. The deterministic fixture above can assert the exact list, and the fuzz test can compare against sorted(d / t for ... if t > 0) instead of the <= n * n_e bound. That also pins the zero-vote mask directly instead of through an accidental NaN range failure.

app/src/app/utils/elections.ts

  • L24 PREF [repo-coherence] Third hand-rolled parser of the election key, with a different year indexformatElectionKey and sortElections take the year from the last _ part; selectFtvElections uses split('_')[1]. Identical today because every key is two-part (confirmed against the dev database). A small parseElectionKey returning {level, year} used by all three would remove the latent divergence.
  • L18 OPINION [tests] No unit test for selectFtvElections or the FTV scoring, but the app has no unit-test harnesspackage.json has only Playwright e2e scripts and there are no *.test.ts files outside e2e/. Out of scope for this PR. Worth a follow-up issue: a minimal vitest or jest config, starting with this helper and an extracted pure FTV scoring function.

app/src/app/components/HelpTip/HelpTip.tsx

  • L192 PREF [correctness] cloneElement({ref: triggerRef}) replaces any ref the caller put on the trigger child — under React 18.3 this overwrites the child's own ref. All 20 current call sites pass DOM elements or forwardRef Radix components with no ref of their own, so nothing breaks today. A future <HelpTip><Button ref={x} /></HelpTip> would silently lose x. Composing refs the way Radix Slot does is a two-line change.

Verified clean

  • FTV rule matches the description: bound is max(7%, 1/k), inclusive, pass at 3 of 4, and the "1 out of k seats" wording appears exactly when 1/k exceeds 7%.
  • selectFtvElections returns null unless both election pairs exist, and whenever it is non-null the district count is defined.
  • The inline Select inside a paragraph produces valid HTML; Radix Select renders phrasing content and portals its dropdown.
  • useDistrictHover handles a null map, an empty district list, and an old-shape payload without error.
  • HelpTip's new outside-click exemption targets the same DOM node Radix positions against, and only pointerdown behavior changed.
  • Backend: the pandas boolean Series share one index so alignment is exact, the three lists partition the districts, the zero-elections branch never touches dem_wins, and the version bump invalidates cached evaluation rows.
  • Zone ids serialize as plain Python ints (checked by execution), so the JSON payload is safe.
  • backend/tests/test_fl_metrics_integration.py does not reference the competitiveness metric, so the registry docstring's manual-run note is not made stale.
  • No security surface changed: no auth, SQL, secrets, or new input parsing.
Details

PR Review Findings: Add Freedom to Vote test and configurable competitiveness metrics to the evaluation panel (#723)

Reviewed 2026-09-03 against dev at 8af55df5, head at eeb8661b.

Scope and method

  • 674 reviewable lines across 13 files (9 frontend, 4 backend), medium tier. No generated or vendored files in the diff.
  • Review factors: correctness, test coverage, repo coherence. Security was not given its own reviewer: the diff adds no auth, SQL, secrets, or new input parsing. All rendered strings are React text nodes fed from the existing evaluation API. Verified clean by the lead reviewer.
  • Three focused reviewers ran in parallel, one per factor. Every finding below was re-checked by the lead reviewer against the full files before being kept.

Verification runs

  • gh pr checks 723 → all 3 checks pass (AWS preview, pipeline tests, container-job).
  • backend/.venv/bin/pytest tests/test_partisan.py -q with .env.test loaded → 30 passed, 3 errors. The 3 errors are database-backed Eguia and county tests that need a live Postgres and are untouched by this PR. All competitiveness tests pass. CI runs the same file green.
  • docker compose exec backend pytest ... → could not run. The running dev container is missing prometheus_fastapi_instrumentator, a dependency added to requirements.txt in June. The container image is stale. Not related to this PR.
  • bun run ts (tsc) → no errors in src/. The only errors are in stale generated files under .next/types that reference pages not on this branch. Not related to this PR.
  • bunx eslint <9 changed files> → clean.
  • bunx prettier --check <9 changed files> → clean.
  • uvx ruff@0.3.4 check and ruff@0.3.4 format --check (the version pinned in .pre-commit-config.yaml) on the 4 changed backend files → clean. Latest ruff reports import-order and assert-formatting differences, but those also exist on dev and are a ruff version drift, not this PR.
  • Read-only query of the dev database's gerrydb schema → every statewide election column is named {pres|sen|gov|ag}_{yy}_{dem|rep} with two-digit years. The selectFtvElections year sort is safe on real data.
  • Pandas Index iteration check → iterating df.index[mask] yields Python int, not numpy.int64. The zone-id lists serialize to JSON without a custom encoder. This falsified an initial suspicion.
  • Mutation testing (test reviewer, scratchpad-only pytest plugin, no repo files changed) on pytest tests/test_partisan.py -k competitive:
Mutant Result
baseline 4 pass
swap dem and rep sweep output lists 4 pass (survives)
swap dem_wins and rep_wins in the loop 4 pass (survives)
disable sweep logic (every district becomes swing) 4 pass (survives)
emit Rep share instead of Dem share in contest_dem_vote_shares 4 pass (survives)
drop the valid zero-vote mask 1 fail (fuzz range check catches NaN)
invert swing 3 fail
  • node -e float check → 0.53 - 0.5 <= 0.03 is false, 0.47 - 0.5 >= -0.03 is false, Math.abs(0.55 - 0.5) <= 0.05 is false.

Factor: correctness (reviewer scope: full diff)

Findings

  • [correctness] Missing disproportionality value renders as a failed FTV verdict, not "not enough data"app/src/app/components/EvalPanel/PartisanSection.tsx:112 [proposed: Important] [confidence: verified code path, likely in practice] ftvPassingKeys filters on disprop !== undefined && Math.abs(disprop) <= ftvThreshold. A test election that has a seats entry but no disproportionality entry counts as a failure. If all four are missing the page prints "This plan DOES NOT PASS" and "0 out of 4 times" while every verdict cell shows a dash. Reachable: the backend disproportionality metric raises ZeroDivisionError when any election has zero D+R votes in the assigned area (backend/tests/test_partisan.py:898 pins this), and backend/app/evaluation/main.py:154 catches per-metric failures so seats still arrives. Fix: set ftvPassCount to null when any FTV key lacks a disproportionality value, so the existing "Not enough data" branch renders.
  • [correctness] Competitive-band filter drops exact-boundary vote shares due to float subtractionapp/src/app/components/EvalPanel/PartisanSection.tsx:191 [proposed: Personal preference] [confidence: verified] Math.abs(s - 0.5) <= band fails for s = 0.53 at band 0.03 because 0.53 - 0.5 is 0.030000000000000027. Verified in node for bands 2, 3, and the upper end of 5. The old backend and the PR's own test helper use inclusive 0.47 <= s <= 0.53. Fix: s >= 0.5 - band && s <= 0.5 + band. Rare in practice, needs an exact percentage share.
  • [correctness] FTV grades partially drawn plans as if completeapp/src/app/components/EvalPanel/PartisanSection.tsx:108 [proposed: Opinion] [confidence: verified mechanics, uncertain intent] k is seats.total, which is num_nonempty_districts. A plan with 3 of 14 districts drawn gets a 33% bound and an unqualified "PASSES". This matches how every other partisan metric already behaves, but FTV is the first one shown as a binary legal verdict. BasicsSection already computes completeness. Product decision, no change requested.
  • [correctness] HOVER_BTN_STYLE's font: inherit cancels weight="bold" on the three district-count cellsapp/src/app/components/EvalPanel/PartisanSection.tsx:738 [proposed: Personal preference by this reviewer; raised to Important after the coherence reviewer confirmed the cascade] See the repo-coherence section.
  • [correctness] cloneElement({ref}) overwrites any ref a caller puts on the HelpTip trigger childapp/src/app/components/HelpTip/HelpTip.tsx:192 [proposed: Personal preference] [confidence: verified] In React 18.3 cloneElement replaces the child's ref. All 20 current call sites pass DOM elements or forwardRef Radix components with no ref, so nothing breaks today. A future <HelpTip><Button ref={x} /></HelpTip> would silently lose x. Fix: compose refs the way Radix Slot does.
  • [correctness] Rep-POV seat share in the FTV table is 1 - dem/total, not rep/totalapp/src/app/components/EvalPanel/PartisanSection.tsx:157 [proposed: Opinion] [confidence: verified] With a tied district the FTV table's R seat share differs from the Proportionality table's directly above by ties/total. The FTV form is actually the one consistent with the verdict math. Exact ties are essentially nonexistent in precinct data. No change requested.

Verified clean

  • FTV rule matches the stated test: Math.max(0.07, 1/k), inclusive <=, pass at 3 of 4. ftvBoundPhrase switches to the "1 out of k seats" wording exactly when 1/k > 0.07 (k of 14 or fewer).
  • ftvVerdict tilt direction is only reached when |disprop| > threshold > 0, so no sign ambiguity at zero.
  • selectFtvElections returns null unless both pairs exist; whenever it is non-null, n >= 4 and numDistricts is defined, because pres and sen are both in LEVEL_ORDER.
  • povVoteShare 1 - dem equals vote_shares.rep because the backend computes both over the same D+R total.
  • Inline Select.Root inside <Text as="p">: Radix Select renders a <button> plus a hidden native <select>, both phrasing content. Content is portaled. No invalid nesting, no hydration mismatch.
  • All hooks run before the early return null.
  • useDistrictHover: null map on enter leaves prevRef untouched; leave always resets it; empty zones array clears previous highlights and sets none. A v1 backend payload degrades gracefully through the ?? [] and ?? 0 defaults.
  • HelpTip: event.detail.originalEvent.target matches Radix DismissableLayer's event shape. Radix Themes HoverCard.Trigger is asChild, so Slot composes refs and triggerRef points at the same DOM node Popper measures. contains() handles nested icons. Focus-outside is already prevented by Radix HoverCard, so only pointerdown behavior changed.
  • Backend: pd.Series(True, index=zones) and dem_wins[e] share demographic_data.index, so &= alignment is exact. ~(dem | rep) partitions the zones. The zero-elections branch returns before touching dem_wins and reports the real n_districts. n_districts and the sum of the three lists both derive from rows with zone is not None; the fuzz test asserts they agree. Version bump to 2 flips CURRENT_PAYLOAD_VERSION and invalidates cached rows.
  • Competitive-contest denominator is now the count of valid contests (zero-vote pairs excluded), consistent with the fuzz bound.
  • Pandas Index iteration yields Python ints, so no numpy JSON serialization issue.

Highlights

  • Moving band classification client-side over a sorted flat list is a clean decoupling.
  • The frontend can deploy ahead of the backend without crashing on a v1 payload.
  • The HelpTip doc comment accurately describes Radix's hasSelectionRef behavior, and the trigger exemption is scoped to the one case that needs it.

Factor: tests (reviewer scope: backend tests, frontend logic)

Findings

  • [tests] Sweep classification is not pinned by any test; party swap survives every testbackend/tests/test_partisan.py:433 (helper), :377 and :605 (expected counts), :1008 (fuzz) [proposed: High by reviewer; lead sets Important, arguably High] [confidence: verified by executed mutants] Both gerrychain fixtures expect 0 Dem-sweep and 0 Rep-sweep districts, so the cardinality checks are 0 == 0 whichever party a list is labeled. A mutant that never marks any sweep still yields 8 swing districts. The fuzz test asserts partition and disjointness only, which any three-way split satisfies. The specific district ids in dem_sweep_districts and rep_sweep_districts are what the frontend highlights on the map, and no assertion checks them. Fix: add a 3 or 4 district, 2 election _StubEvaluationContext fixture (the zero-elections test at line 466 is the template) and assert exact lists, for example dem_sweep_districts == [1], rep_sweep_districts == [2], swing_districts == [3, 4]. In the fuzz test, compute the oracle independently from ctx.dem_wins and assert set equality.
  • [tests] contest_dem_vote_shares party orientation is unverifiedbackend/tests/test_partisan.py:452 and :1024 [proposed: Important] [confidence: verified] Assertions are a symmetric 47 to 53 count, range, and sortedness. Emitting the Rep share instead passes all of them. The frontend filter is also symmetric today, so nothing would notice, but the field is documented as Dem share. Fix: assert the exact list in the deterministic fixture above, and in the fuzz test replace the <= n * n_e bound with an exact oracle built from ctx.dem_votes and ctx.total_votes where total is greater than 0. That also pins the valid mask directly instead of through the accidental NaN range failure.
  • [tests] Fuzz length assertion is a bound where an exact value is one linebackend/tests/test_partisan.py:1024 [proposed: Personal preference] [confidence: verified] Subsumed by the previous finding.
  • [tests] No unit tests for selectFtvElections or the FTV scoring, but the app has no unit-test harnessapp/src/app/utils/elections.ts:18, app/src/app/components/EvalPanel/PartisanSection.tsx:112 [proposed: Opinion] [confidence: verified] app/package.json has only Playwright e2e scripts. jest and testing-library are devDependencies but there is no config and no *.test.ts files outside e2e/. The e2e suite only checks that the Evaluate view opens. Demanding a harness is out of scope for this PR. Suggest a follow-up issue.

Verified clean

  • backend/tests/test_fl_metrics_integration.py does not reference competitive_metrics or any old field name. The registry docstring's "run the FL suite on version bump" note is not made stale by this change.
  • No other backend test references the competitiveness metric, so nothing else was silently weakened.
  • The replacement helper still asserts every piece of the old recorded ground truth (both counts, all three cardinalities, and the 47 to 53 count recomputed from the flat list). Nothing from the old oracle was dropped.
  • The new zero-elections test pins n_districts == 5 with empty lists, a real gap in the old code.
  • The fuzz strategy generates zero-vote pairs, so the valid mask path is exercised. The swing inversion mutant is caught.

Highlights

  • Keeping the gerrychain counts as ground truth and recovering the old 47 to 53 count from the new flat list is exactly the cross-check that justifies moving band selection to the client.

Factor: repo coherence (reviewer scope: full diff, sibling files)

Findings

  • [repo-coherence] font: inherit in the shared style un-bolds the three hoverable competitiveness cellsapp/src/app/components/EvalPanel/PartisanSection.tsx:738, :755, :772; app/src/app/components/EvalPanel/hoverTriggerStyle.ts:12 [proposed: Important] [confidence: verified from node_modules/@radix-ui/themes/styles.css] Radix applies weight="bold" through the class .rt-r-weight-bold. An inline font: inherit shorthand resets font-weight to the parent's normal weight and wins over any class. Font size and line height happen to come out identical because the table cell already uses --font-size-2. Result: the Swing, Dem Sweep, and Repub Sweep counts render at regular weight while the "Elections analyzed" and "Competitive contests" counts two rows up render bold. The PR already knows the footgun: BasicsSection lines 186 and 199 and PartisanSection line 507 spread {...HOVER_BTN_STYLE, fontWeight: 'bold'} for this reason. Fix: same spread on these three, or split the button-only reset keys out of the shared constant.
  • [a11y] Five <span role="button"> triggers have no activation behavior, no keyboard handler, and no labelapp/src/app/components/EvalPanel/PartisanSection.tsx:142, :381, :396, :419, :505 [proposed: Important] [confidence: verified] role="button" promises Enter or Space activation. A screen-reader user hears "button", presses it, and nothing happens. In-repo precedents: BasicsSection uses a real <button type="button"> for the identical hover-to-highlight affordance (and this PR edits those). DistrictLabel.tsx sets role only when there is an onClick. DistrictMeters.tsx has a comment declining role="button" where it would be invalid ARIA. The one existing <span role="button"> in HelpTip carries aria-label. Radix Themes HoverCard.Trigger is asChild, so a <button> child works with HelpTip too. The PR description says a plain <button> interrupted text selection; that may be a user-select issue on the button rather than a reason to drop the element. Fix: use <button type="button"> like BasicsSection, or drop role="button" and add aria-describedby or title explaining the highlight.
  • [repo-coherence] Third hand-rolled parser of the election key, with a different year indexapp/src/app/utils/elections.ts:24; app/src/app/components/EvalPanel/PartisanSection.tsx:79 [proposed: Important by reviewer; lead sets Personal preference] [confidence: verified] formatElectionKey and sortElections take the year as the last _ part; selectFtvElections takes split('_')[1]. Identical today because every key is two-part. sortElections and LEVEL_ORDER live in the component while the sibling helper went to utils/elections.ts. Fix: a small parseElectionKey in utils/elections.ts used by all three, optionally moving sortElections there too.
  • [repo-coherence] cursor: pointer on rows that do nothing when clickedapp/src/app/components/EvalPanel/PartisanSection.tsx:728, :745, :762 [proposed: Important by reviewer; lead sets Personal preference] [confidence: verified] CompactnessSection rows use cursor: pointer because they have onClick to zoom. CountySplitsSection.tsx:310 is the hover-only analogue and uses cursor: default. The inner Text inherits cursor: default from HOVER_BTN_STYLE, so the cursor changes shape moving across one row. Fix: cursor: default, or add a click that zooms to the group.
  • [repo-coherence] cast(DistrictId, z) wraps numpy scalarsbackend/app/evaluation/partisans.py:284 [proposed: Personal preference] DROPPED by lead. Iterating a pandas Index yields Python int, verified by execution. The values match how sibling call sites produce DistrictId.

Verified clean

  • Switching from positional numpy arrays to index-aligned pandas Series is more coherent with the rest of partisans.py, where mean_median and partisan_bias already operate on the context's Series. The version bump follows existing precedent for reshaped metrics.
  • hoverTriggerStyle.ts placement is reasonable: the repo has no shared-styles location, and the two consumers are both in EvalPanel/.
  • var(--green-9) for pass matches MapValidation.tsx. var(--accent-9) for the FTV outline matches other Radix token usage in EvalPanel. DEM and REP literals are pre-existing in this file.
  • useDistrictHover array generalization is minimal and all callers were updated; grep found no stragglers.
  • Inline Select.Root size="1" matches existing usage in BasicsSection and CountySplitsSection.
  • HelpTip change matches the file's heavily commented style, and the comment describes the new behavior accurately.
  • helpTipContent.freedomToVoteTest matches the entry shape and satisfies usage of its neighbors.
  • getEvaluation.ts mirrors the backend TypedDict field for field, as the other result types do.

Highlights

  • Index-aligned sweep computation removes a hidden ordering assumption in the backend.
  • Extracting ftvHelpTipTrigger once avoids duplicating the HelpTip wiring across both sentence branches.
  • The ftvBoundPhrase logic and comment ("state the bound in whichever form is binding") are well explained.

Lead reviewer notes on severity calls

  • Sweep-membership test gap: the reviewer proposed High. The HIPPO definitions put missing tests for new behavior at Important, and the production code was verified correct by a separate reviewer. Set to Important, arguably High, because it is the PR's primary new output and a party swap would ship silently.
  • Un-bolded cells: one reviewer said Preference, one said Important. Set to Important because it is a visible inconsistency inside a five-row table with a two-token fix, and the shared constant will keep defeating weight props.
  • Election-key parser duplication and cursor: pointer: downgraded from Important to Personal preference. Both are real but small, and the author may reasonably defer them.
  • Numpy cast finding dropped after execution showed Python ints.

Live environment log

Read-only only. One query against the local dev Postgres container:

select column_name, count(*) from information_schema.columns
where table_schema = 'gerrydb' and column_name ~ '^(pres|sen|gov|ag)_'
group by 1 order by 1

Result: all statewide election columns follow {type}_{yy}_{dem|rep} with two-digit years.

),
Metric[CompetitiveMetrics](
key="competitiveness", version=1, compute=partisans.competitive_metrics
key="competitiveness", version=2, compute=partisans.competitive_metrics

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming, will this invalidate old cached metrics so they are recalculated on the next fresh request?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. All metrics will be recomputed for any version bump of any metric.

Comment on lines -280 to +276
dem_districts = np.logical_and(dem_districts, context.dem_wins[election])
rep_districts = np.logical_and(rep_districts, context.rep_wins[election])
dem_sweep &= context.dem_wins[election]
rep_sweep &= context.rep_wins[election]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice syntax :)


const ftv = selectFtvElections(Object.keys(evaluation.seats ?? {}));
const ftvThreshold = ftv && numDistricts ? Math.max(0.07, 1 / numDistricts) : null;
const ftvPassingKeys =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I: A test election with seats but no disproportionality value is counted as a failure rather than "not enough data". This is potentially reachable because the backend isolates per-metric failures and that metric raises on zero votes. But in practice, could a 0 disproportionality case exist?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added check for invalid responses.


// Shared between both branches of the FTV sentence below (scored and
// not-enough-data) so the HelpTip trigger isn't duplicated.
const ftvHelpTipTrigger = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PP: This could be moved out of the PartisanSection component (nothing stateful here) and span does lack some a11y if we can use a different element here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

<Table.Cell justify="center">
<Text size="2" weight="bold">
{competitiveness.n_swing_districts} / {competitiveness.n_districts}
<Text size="2" weight="bold" style={HOVER_BTN_STYLE}>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I: I think the style will clobber the weight="bold" here. style={...HOVER_BTN_STYLE, fontWeight: 'bold'} might be needed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed.

<Table.Row>
<Table.Row
tabIndex={0}
style={{cursor: 'pointer'}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O: Why pointer?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Changed to 'default'.

Per Dylan's feedback: swap the highlight line back to a vivid orange
(replacing the neon magenta tried on this branch), and dim every
non-hovered district while one is focused, same masking convention as
the paint-mask overlay in CountyLayers.tsx. Needed a small reactive
store (districtHoverStore) mirroring useDistrictHover's feature-state
writes, since the dim mask layer must know whether anything is
currently hovered to gate itself on/off.

Exploratory on this branch pending visual confirmation; folds into
this PR if it looks good.
…mask opacity

Per review: a dedicated Zustand store for one piece of hover state wasn't
justified when useDistrictHover already depends on mapStore, the
established general-purpose reactive container for map state. Moved
hoveredZones there as hoveredPublicZones/setHoveredPublicZones and
deleted districtHoverStore.ts. Also cut the highlight-color comment
(no longer needed) and lowered HIGHLIGHT_MASK_OPACITY 0.6 -> 0.45 per
visual feedback.
…ring, sweep tests

- ftvPassCount now goes null (not a false failure) when any of the 4 FTV
  keys lacks a disproportionality value — the backend metric can raise on
  a zero-D+R-vote election and per-metric failures are isolated, so seats
  can arrive without disproportionality.
- Fixed HOVER_BTN_STYLE's font:'inherit' silently un-bolding the three
  Swing/Dem-Sweep/Repub-Sweep count cells (weight="bold" loses to the
  inline style) by folding fontWeight into the style object instead of a
  separate Radix prop, matching the existing pattern elsewhere in this file.
- Converted 5 <span role="button"> hover triggers to real <button
  type="button">, matching BasicsSection's identical hover-to-highlight
  pattern with the same HOVER_BTN_STYLE (already proven not to break
  paragraph text selection) — spans announced as buttons to screen
  readers but had no activation behavior.
- HelpTip's cloneElement({ref: triggerRef}) was silently replacing any ref
  a caller already put on its trigger child; added a small mergeRefs
  helper (same purpose as Radix's own composeRefs) so both refs populate.
- Backend: added a deterministic 4-district/2-election fixture pinning
  exact dem_sweep_districts/rep_sweep_districts/swing_districts membership,
  and strengthened the fuzz test with an independent oracle built from
  ctx.dem_wins/rep_wins and ctx.dem_votes/total_votes. The prior tests
  only checked cardinalities, so swapping the Dem/Rep sweep lists (or the
  Dem/Rep vote share) silently passed everything — verified via a live
  mutation test (swapped the two output lists, confirmed both new/
  strengthened tests fail, then reverted).
- cursor: 'pointer' -> 'default' on the Swing/Sweep table rows: they only
  respond to hover (no onClick), so pointer misleadingly implied a click
  action: Dylan's own inline question ("Why pointer?").
- Hoisted ftvHelpTipTrigger to module scope: a static JSX constant with
  no closure over props/state was being rebuilt every render.

Reverted, per human judgment: the Math.abs(s - 0.5) <= band float-boundary
rewrite — real vote shares landing exactly on a boundary is negligible in
practice, and the reviewer's own tag was Personal preference.

All four quality gates pass (pre-commit, frontend ts, frontend build,
backend pytest) via run-quality-gate.
deb.debian.org is a CDN in front of many mirror origins; a -security
point-release rotation can leave one edge's index listing a filename
another edge has already purged, producing a 404 an apt-get install
can't recover from mid-run. Wrapping update+install in a retry loop
lets a fresh apt-get update (which usually lands on a consistent edge)
resolve it within the same build, instead of needing a whole CI job
rerun.

Verified live: this branch's AWS Preview workflow hit this exact
failure 3 times in a row on 3 different packages (libtiff, libc-l10n,
libperl); reproduced locally against the unmodified Dockerfile
(libde265-0, arm64) while testing the fix. Confirmed the retry loop
itself works (recovered from the same live mirror flakiness during a
local arm64 build) and that the full build succeeds end-to-end on
linux/amd64 (the real CI architecture) with the fix applied.

No deploy-api.yml failure has ever been caused by this — checked its
last ~30 runs; its only 2 failures were unrelated migration-task
issues, apt-get succeeded in both. Folding this into #723 rather than
a separate PR so the fix gets verified through this branch's own
preview deploy, which is already hitting the failure live.
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.

2 participants