You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Planned 2026-07-29. Interactive reference prototype (real August 2026 primary data; column pickers, filters, and difference highlighting live): docs/design/COMPARE_PROTOTYPE_2026-07-29.html (committed via #125 — open directly in a browser), also published at https://claude.ai/code/artifact/2de0c1fd-9879-4ea8-a8d4-222989e8bd22
Note: the prototype was built before#115's site-wide UI pass landed. Treat it as the structural and behavioral reference — column model, cell states, difference semantics, codec — not as the visual spec. Where it disagrees with the codebase's post-#115 conventions (shared shell, tokens, typography, component language), the codebase wins; update layouts and chrome to match the current conventions as you implement.
Outcome
Let a voter place two or three endorsement signals side by side — a single source, a
whole category, or the full audited panel — and see, race by race, where they agree
and where they split, with a shareable link for every configuration. The page answers
questions the guide's single-consensus view cannot: How does The Stranger compare to
The Times? Where does Labor split from the Democratic Party? Where does The Urbanist
leave the general consensus?
This issue is the parent outcome and design tracker. Implementation is owned by the
native child graph below; it must not be delivered as one monolithic PR.
Product contract
The audited consensus remains the published baseline everywhere. The All sources
column reproduces the published audited values exactly at the display-model level;
it is never recomputed client-side.
The page is descriptive, not prescriptive: it renders picks, shares, and source
counts. It never renders a grade, a recommendation, or an insufficiency suppression
— small denominators are disclosed (1 of 9 sources), never hidden.
The Seattle Times (and any future comparison-role signal) is selectable as a column,
is always visibly badged as comparison-only, and can never contribute to any
aggregate. panelAdmission()'s hard exclusion remains the enforced backstop.
Rows are races, grouped and ordered exactly as the Endorsements page groups them
(ballot sections), with the guide's existing race filters plus a Differences only
toggle.
Column order is meaningful: the first column is the baseline the other columns'
agree/differ presentation is measured against.
At most 3 columns in v1; narrow viewports show fewer without changing the
configured state, and a shared link's difference row-set never depends on the
viewer's viewport.
Complete state lives in the URL fragment. No account, personal data, or
server-side preference storage.
Default view: All sources | The Stranger | The Seattle Times.
gall is the reserved sentinel for the full audited panel. The lowercase-g
prefix — already reserved out of the source-code namespace and never a category
prefix — becomes the sentinel namespace; gmin (the viewer's lens selection) is
reserved for a future child and must not be accepted in v1.
The fragment codec mirrors lens-url.mjs: fixed-width 4-character tokens joined
with no separator, URLSearchParams-shaped fragment, full version binding
(panel, ph, data, scoring). One deliberate divergence, documented in the
codec: the token string is order-preserving (first token = baseline column),
where the lens's sel is canonically sorted.
A category column is the published equal-weight scoring contract applied to the
category's current membership: each explicitly endorsing member counts once, a
split allocation divides exactly, non-affirmative states stay out of the
denominator, and eligibility follows PersonalizationRace.eligible_source_codes.
Category rules follow current membership, exactly like the lens.
A comparison-role category column (Gcmp, single member) renders its member's
direct stance and carries the comparison-only badge; it never tallies.
Cell agreement is set intersection over leading picks: a co-endorsement or tie
agrees with a column whose lead set intersects it. A row differs when any two
data-bearing configured columns have disjoint lead sets — computed over the full
configured column set even when width hides a column.
All non-affirmative cell states (no_endorsement, not_covered, unavailable, unverified) render as one identical blank. A source absent from a race's
eligible set (a legislative-district organization outside its district) renders
as a distinct muted Outside district state, from published eligibility only.
Race labels link to the guide's existing race-detail permalinks; the compare page
reproduces no audit trail of its own.
Preset links (e.g., The Stranger vs. The Times) are pure fragment URLs — UI
sugar over the codec, no separate mechanism.
Only #117 is initially ready. #118, #119, and #120 can proceed in parallel after #117. #118 additionally coordinates with the #115 shared shell, which is in progress.
Epic acceptance criteria
The default page load server-renders All sources | The Stranger | The Seattle Times and is readable without JavaScript.
Every All sources cell equals the published audited consensus for that race
at the display-model level, proven by an exact-match validation over all races.
Category cells reproduce the published scoring contract for member sources,
including split allocations, ties, and district eligibility.
Selecting every comparison-role code in every slot can never change any
aggregate cell — proven by a test that forces the scenario.
A same-version shared URL replays columns, order, filters, and the differences
toggle exactly; the difference row-set is identical across viewport widths.
A cross-version or unknown-token link degrades deterministically with a
persistent disclosure, never a broken or silently different table.
Blanks are normalized: no rendering distinguishes no_endorsement from not_covered, unavailable, or unverified; Outside district renders only
from published eligibility.
Accessibility (table semantics, labeled controls, non-color difference
signals, aria-live status), mobile two-column behavior, browser, rendering,
release, and hosting-stage validation pass.
Non-goals
A My sources / custom-collection column (gmin is reserved, not implemented).
User-configurable weights, grades, or any scoring beyond the published contract.
Print/PDF output for the comparison view.
Sorting or ranking rows by disagreement.
Traffic-adaptive presets (a future consideration; v1 presets are static links).
Removing or changing the Times comparison on the Endorsements page (possible
follow-up this page enables; explicitly out of scope here).
Any visual-polish work beyond the shared shell — deferred to a separate pass.
Planned 2026-07-29. Interactive reference prototype (real August 2026 primary data; column pickers, filters, and difference highlighting live):
docs/design/COMPARE_PROTOTYPE_2026-07-29.html(committed via #125 — open directly in a browser), also published at https://claude.ai/code/artifact/2de0c1fd-9879-4ea8-a8d4-222989e8bd22Outcome
Let a voter place two or three endorsement signals side by side — a single source, a
whole category, or the full audited panel — and see, race by race, where they agree
and where they split, with a shareable link for every configuration. The page answers
questions the guide's single-consensus view cannot: How does The Stranger compare to
The Times? Where does Labor split from the Democratic Party? Where does The Urbanist
leave the general consensus?
This issue is the parent outcome and design tracker. Implementation is owned by the
native child graph below; it must not be delivered as one monolithic PR.
Product contract
All sourcescolumn reproduces the published audited values exactly at the display-model level;
it is never recomputed client-side.
counts. It never renders a grade, a recommendation, or an insufficiency suppression
— small denominators are disclosed (
1 of 9 sources), never hidden.is always visibly badged as comparison-only, and can never contribute to any
aggregate.
panelAdmission()'s hard exclusion remains the enforced backstop.(ballot sections), with the guide's existing race filters plus a
Differences onlytoggle.
agree/differ presentation is measured against.
configured state, and a shared link's difference row-set never depends on the
viewer's viewport.
server-side preference storage.
All sources | The Stranger | The Seattle Times.explicitly deferred to a separate site-wide pass (successor to UI polish pass: one cohesive site (brand, shell, tokens, meters, comparison bars) #115).
Design decisions
/e/<election-id>/compare/with aCompareitem in the sharednav between
EndorsementsandSources.(
strn,stim), category codes (Gdem,Glab).gallis the reserved sentinel for the full audited panel. The lowercase-gprefix — already reserved out of the source-code namespace and never a category
prefix — becomes the sentinel namespace;
gmin(the viewer's lens selection) isreserved for a future child and must not be accepted in v1.
with no separator, URLSearchParams-shaped fragment, full version binding
(
panel,ph,data,scoring). One deliberate divergence, documented in thecodec: the token string is order-preserving (first token = baseline column),
where the lens's
selis canonically sorted.category's current membership: each explicitly endorsing member counts once, a
split allocation divides exactly, non-affirmative states stay out of the
denominator, and eligibility follows
PersonalizationRace.eligible_source_codes.Category rules follow current membership, exactly like the lens.
Gcmp, single member) renders its member'sdirect stance and carries the comparison-only badge; it never tallies.
agrees with a column whose lead set intersects it. A row differs when any two
data-bearing configured columns have disjoint lead sets — computed over the full
configured column set even when width hides a column.
no_endorsement,not_covered,unavailable,unverified) render as one identical blank. A source absent from a race'seligible set (a legislative-district organization outside its district) renders
as a distinct muted
Outside districtstate, from published eligibility only.reproduces no audit trail of its own.
The Stranger vs. The Times) are pure fragment URLs — UIsugar over the codec, no separate mechanism.
Child issues
Native dependency graph
Only #117 is initially ready. #118, #119, and #120 can proceed in parallel after #117.
#118 additionally coordinates with the #115 shared shell, which is in progress.
Epic acceptance criteria
All sources | The Stranger | The Seattle Timesand is readable without JavaScript.All sourcescell equals the published audited consensus for that raceat the display-model level, proven by an exact-match validation over all races.
including split allocations, ties, and district eligibility.
aggregate cell — proven by a test that forces the scenario.
toggle exactly; the difference row-set is identical across viewport widths.
persistent disclosure, never a broken or silently different table.
no_endorsementfromnot_covered,unavailable, orunverified;Outside districtrenders onlyfrom published eligibility.
signals, aria-live status), mobile two-column behavior, browser, rendering,
release, and hosting-stage validation pass.
Non-goals
My sources/ custom-collection column (gminis reserved, not implemented).follow-up this page enables; explicitly out of scope here).