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
Unblocked. This needed a geo-chat change first — geo-chat still derived a claim's kind from the "Is factual" flag and rejected our stance response, so factual claims could not be debated. That shipped as geo-chat#148 (GEO-2993) and reached testnet in build 137c493a at 00:03 UTC on 24 Sep. Details in the thread below.
Removes the Verify/Dispute vocabulary. Every claim is now answered with Agree/Disagree, whatever its "Is factual" flag says.
The flag itself stays. The extractor still writes it (debate-publish-draft.ts) and the debate surfaces that read it are untouched — it just no longer decides how a claim is answered.
What's gone
veracity response kind
with its vote kind (2) and its verify/dispute/unverify SDK methods — there is no longer a code path that can publish one
the chevron glyph
and VOTE_CHEVRON_SELECTED_CLASS, the hand-written #2A2B2E a selected chevron needed because it had no filled form to switch to
core/claims/response-kind.ts
deleted; resolveEntityResponseKind also loses its spaceId parameter, since "Is factual" was the only per-space value it read
the "Is factual" half of the unpublished-edit guard
a draft edit to the flag can't change which vote kind gets published, so blocking on it only disabled the pills for no reason. The Claim type half still blocks — that really does move an entity between curation and stance
Net −118 lines across 56 files.
Existing veracity responses go dark — as agreed
178 responses, on 145 claims, by 26 voters (Aug 6 → the day this was written; I queried the indexer). Those claims lose that part of their count, and those responders will see them as unanswered and can answer again. The votes are still on chain, so a backfill re-publishing them as stances remains possible.
One deliberate exception: the curator onboarding checklist still counts vote kind 2 when asking whether someone has ever taken a position on a claim. They did, and un-ticking a finished step has nothing to recommend it — unlike the tallies, where dropping the kind is the point. It's a named constant with the reasoning next to it.
Two crashes the wire forced
Both would have shipped without a test that fed in what geo-chat actually sends.
1. Blank surfaces. geo-chat still labels claims minted before this change "veracity". That value arrives typed as the narrowed kind it no longer matches, so TypeScript can't see it — but ENTITY_RESPONSE_COPY[kind] and the glyph Record both miss, return undefined, and throw on the first field read. That's a blank claims ticker over a debate video, and blank cards in the matches list. Claim surfaces now read CLAIM_RESPONSE_COPY without indexing anything, and ResponsePositionIcon falls back instead of throwing.
2. The retired word, rendered. geo-chat sends a position_label per side, and the pills preferred it over their own copy — so a stale "Verify" would have printed on a control that can only publish an Agree. Our label wins now.
Testing
596 files / 7013 tests passing; tsc and eslint clean (zero warnings on the diff).
Roughly 25 tests asserted the old vocabulary. Where a test named the behaviour being removed I inverted it rather than deleting it — those claims are exactly the ones that have to come out Agree/Disagree now:
matches-list and rematch-page-client: "uses Verify and Dispute for factual claims" → asserts Agree/Disagree on a claim geo-chat still calls factual
debate-claim-ticker: "65% verify" → "65% agree", and asserts the old string is absent
The reason will be displayed to describe this comment to others. Learn more.
Nicely done overall. The guards against stale wire values are real, and keeping kind 2 in the onboarding checklist is the right call. But I don't think this can merge on its own: once it ships, nobody will be able to start a debate on a claim marked "Is factual", until geo-chat changes too.
Blocker: geo-chat still treats factual claims as Verify/Dispute
geo-chat works out a claim's kind itself, from the graph's "Is factual" flag, and never uses what the web app tells it (checked against geo-chat main at f8a2129):
When someone responds, geo-chat rejects it. After a vote, the web app now always sends response_kind: "stance" to POST …/response-indexed. For a factual claim geo-chat resolves Veracity, and validate_response_kind_matches_claim returns 400 claim_response_kind_mismatch, so the viewer is never recorded as ready.
The background re-check can't recover it. For factual claims, resolved_debate_claim_from_data reads only the vote-kind-2 response (veracityResponse). The new kind-1 vote is never seen, so the position comes back empty.
Since the extractor keeps writing the flag, this hits every new factual claim, not just the 145 with existing veracity votes. People can pick Agree or Disagree, but "Request debate" never appears for anyone on those claims.
Suggested fix: a geo-chat change that treats every claim as a stance (read only vote kind 1, and relabel existing space_debate_claims.response_kind = 'veracity' rows), deployed before or together with this PR. I couldn't find one on geo-chat main or in its open PRs.
Should fix: "Verify" still renders in several places
The description says "Our label wins now", but that only holds for the pills. These still show geo-chat's position_label as-is, which is "Verify"/"Dispute" for a factual claim:
The rematch page: rematch-page-client.tsx:1952 and :2594. The comment at :2594 still says "an authoritative Verify/Dispute survives".
The debate room (debate-room-page-client.tsx:2317, 2537, 2589, 3709), the pre-join screen (debate-pre-join-screen.tsx:201, 272), the request dialog (debate-request-dialog.tsx:236) and request-parties.tsx:17.
If keeping the old labels on past debates is deliberate, it'd be worth saying so in the rematch comment and the description. Otherwise it goes away with the geo-chat fix.
Nits
JSDoc in the wrong place: in entity-response.ts, the responsePositionLabel comment now sits above CLAIM_RESPONSE_KIND. In queries.ts, getUserHasVoteOfKind has two comments stacked, and the first still describes veracity as a live kind.
Comments that still describe Verify/Dispute as current:debate-claim-ticker.tsx ("or 65% verify on a factual claim"), the ClaimResponseState.responseKind doc and the comments at use-claim-response-state.ts:119,134, the position prop doc on ResponsePositionIcon ("Upvote / Agree / Verify"), and claim-position-comment.tsx ("Disagree/Dispute").
Unused component:DebateEntityResponseControls has no callers outside its test, and still passes a wire-typed kind straight into EntityVoteButtons. Probably worth deleting.
Thanks — this was a good catch on the blocker. I checked it against geo-chat main at f8a2129 rather than taking it on trust, and you're right; this cannot merge alone. Everything else is addressed in c9b1e0c.
The blocker: confirmed, and I can't fix it here
The chain is exactly as described:
validate_response_kind_matches_claim rejects on resolved_claim.response_kind != request.response_kind, and it is called on the notify path at db_debates.rs:8258.
geo-chat resolves the kind itself from the graph flag — response_kind_from_is_factual — so a factual claim is Veracity regardless of what we send.
For Veracity it then reads data.veracity_response, which is the voteKind: 2 query, so our kind-1 vote is invisible to the background re-check.
So: 400 claim_response_kind_mismatch on the notify, and an empty position on the re-check. No "Request debate" on any factual claim, new ones included, since the extractor keeps writing the flag.
I also looked for the geo-chat side and couldn't find it either — nothing on main, and neither open PR (#146, #83) touches this. The relabel you mention would be space_debate_claims.response_kind, added with a CHECK (response_kind IN ('veracity', 'stance')) in migrations/0027, so that constraint has to move too.
I've left this PR as a draft and it should stay that way until the geo-chat change is deployed. I don't think there's a web-only workaround worth having: we could keep sending the kind geo-chat expects, but its re-check would still read vote kind 2 and find nothing, so Request debate stays broken and we'd be publishing one kind while claiming another.
"Verify" elsewhere: fixed, all eight
Not deliberate — I'd just missed them. Every participant type already carries position beside the label, and position_label_for_response only ever returns one of four fixed words, so the label is fully derivable and overriding it loses nothing.
DebateVideoTile now takes only the side and names it itself, which removed the prop from four call sites rather than fixing each. The rest — rematch :1952 and :2594 (including that stale comment), the debate room's three, the request dialog and request-parties — derive from position.
Three existing tests asserted the server label won, using invented labels (Yes/No, Against/For). I kept the fixtures and inverted the assertions, so they now pin that the invented label is ignored — and verified they fail if the override is removed.
Nits: all fixed
The orphaned JSDoc was mine — inserting CLAIM_RESPONSE_KIND split responsePositionLabel from its comment. Back together. getUserHasVoteOfKind's two stacked comments are now one that says why the retired kind is still passed there.
Stale comments fixed: the ticker's card doc and share comment, ClaimResponseState.responseKind, use-claim-response-state:119,134, the position prop on ResponsePositionIcon, and the end-slot prop in claim-position-comment.
DebateEntityResponseControls deleted with its test. Worth noting it was already unused on master, so that one predates this PR.
Reviewed at c9b1e0ca. Three issues, most severe first.
1. High — geo-chat main rejects the hardcoded 'stance' on any factual claim
CLAIM_RESPONSE_KIND (apps/web/core/responses/entity-response.ts:96) is now sent as the response kind by the readiness notifier and by backfill-readiness-for-held-position.ts:101. geo-chat still derives each claim's kind from "Is factual":
response_kind_from_is_factual (crates/api/src/db_debates.rs:968) returns Veracity when the flag is true
validate_response_kind_matches_claim (db_debates.rs:8205) returns 400 claim_response_kind_mismatch when the request's kind differs
The extractor still writes the flag, so this affects new factual claims as well as old ones. It isn't limited to claims "minted before this change".
Scenario: a user presses Agree on a factual claim. The stance vote lands on chain, but the readiness notification 400s, so no readiness row is created and the user is never matched for a debate on it. The held-position backfill fails the same way on every factual claim.
This needs a paired geo-chat change (stop deriving the kind from is_factual, or accept stance for all claims), merged first. I don't see an open geo-chat PR that does it.
2. Medium — old Verify/Dispute answers still show as held, so the first press publishes a clear
The description says previous veracity responders "will see them as unanswered". But viewerPosition in matchmaking-claim-card.tsx:465 (and the same pattern in use-claim-response-state.ts) comes from geo-chat's readiness.viewer_response. For a factual claim, geo-chat builds that from rows matching response_kind = claim.response_kind, which is still veracity.
Scenario: someone who verified a factual claim sees Agree already highlighted, while the on-chain tally now leaves their vote out. Pressing Agree runs submitResponse('clear'), which publishes an unagree for a stance they never cast. That clear's notification also hits the 400 from #1. They have to press a second time to actually agree.
3. Low/Medium — a re-answered claim drops out of the vote tab
voteKindById is built with Object.fromEntries (core/io/queries.ts:1549), and later pages overwrite earlier ones in use-user-voted-entity-ids.ts:234. Rows come back VOTED_AT_DESC, so the oldest row for an entity decides its kind.
Scenario: someone who verified a claim (kind 2) answers Agree after this PR (kind 1), as the PR invites. The kind-2 row wins, no longer equals responseKindToVoteKind('stance'), and the claim disappears from the Upvoted tab (use-vote-tab-entities.ts:105) even though they currently hold an Agree. Keeping the first-seen (newest) row per entity would fix it.
Update on the blocker: the geo-chat fix is merged, but testnet is not running it yet.
geo-chat#148 (855c7cb) landed on main and covers everything — response_kind_from_is_factual is gone, the kind-2 fetch and the "Is factual" lookup with it, and migrations/0056_every_claim_is_a_stance.sql relabels 'veracity' → 'stance' and swaps the CHECK on all five tables (space_debate_claims, debate_claim_readiness, debate_matches, debates, debate_rematch_requests), shipped in the same commit so the resolver and the data move together.
What is not done is the deploy. The testnet image pin to that build was reverted:
main is back on sha256:080e1fd9…, the 27b7748 build, which still contains response_kind_from_is_factual. So testnet resolves Veracity for factual claims exactly as before, and this PR's behaviour on those claims is unchanged there — worth knowing before anyone tests the preview link and concludes the web side is wrong.
Keeping this as a draft until testnet runs the fixed build.
One thing worth checking, since the revert commit carries no reason: whether migration 0056 has already been applied to the testnet database. If it has while the image is rolled back, the old code will try to write response_kind = 'veracity' against a CHECK that now admits only 'stance' — which fails louder than the original bug. If the migration has not run, nothing is worse than before. @ohohoreilly do you know which it is, and was the revert deliberate?
Out of draft — geo-chat#148 is live on testnet (build 137c493a, deployed 00:03 UTC), so the blocker is gone.
Rebased onto master (161e5a63), 18 commits of drift. One conflict, in claims-page-client.tsx: #2554-era work replaced that page's entity-level response batching with useSpaceActivityRowsInfinite, so the block this PR was editing no longer exists. I took master's version wholesale — the shared claim card it now renders resolves its own response state through the hooks this PR already changed, so nothing was lost. Everything else merged clean.
Re-verified after the rebase: tsc and eslint clean, and no live veracity left outside the comments that explain its removal.
The reason will be displayed to describe this comment to others. Learn more.
Medium: a claim answered with both vote kinds drops out of the Agreed/Disagreed tab.
This now compares against kind 1 (stance) for every claim, but voteKindById holds the kind of the oldest vote on each entity:
getUserEntityVotesByType builds its map with Object.fromEntries(nodes.map(...)) over rows ordered VOTED_AT_DESC, so the last (oldest) row wins.
In use-user-voted-entity-ids.ts (~233-236), later (older) pages overwrite earlier ones too.
Scenario: someone clicked Verify on a factual claim before this PR (a kind-2 positive vote, which stays live, since the UI can no longer clear it). After this PR they click Agree on the same claim (a kind-1 positive vote). The by-direction query returns both rows, the older kind 2 wins, this check fails, and the claim disappears from their Agreed tab even though they hold a live stance on it.
Before this PR the claim resolved to kind 2, so it matched and showed. The Verify-then-Agree path is exactly the migration this PR sends every factual-claim responder down.
Suggested fix: when an entity has votes of both kinds, prefer kind 1 (or take the newest row's kind rather than the oldest).
The reason will be displayed to describe this comment to others. Learn more.
Confirmed and fixed in 16669fe. This is the best find on the PR — a regression we caused, on exactly the path we send every factual-claim responder down.
I verified both halves of the mechanism rather than taking them on trust: Object.fromEntries gives a repeated key its last value, rows arrive VOTED_AT_DESC, so the map described each entity by its oldest vote. And use-user-voted-entity-ids had the same shape across pages, for an entity whose rows straddle a boundary.
I took the "newest row" option rather than "prefer kind 1". Preferring kind 1 special-cases the retired kind and leaves the underlying defect — the map is meant to hold the vote kind for an entity and was holding an arbitrary one. Newest-wins is what every consumer already assumes, and it fixes the migration path as a consequence rather than as a special case.
Two things that came with it:
votedAtByObjectId had the identical defect and is built in the same pass now. Read from a different row it can disagree with the kind, and it is the list's sort key — so an entity could sort by one vote and be filtered by another. Worth flagging: this changes the Agreed/Disagreed sort position for a claim holding both kinds. It now sorts by the Agree, which is the vote being displayed. I think that is right, but it is a visible change and I would rather name it than have it turn up.
The logic was untestable where it sat, inside a decoder closure. Extracted as indexVoteRowsByObject, which is also how the repo tends to make this kind of thing assertable.
Three tests in queries.test.ts, on your exact scenario — today's Agree over August's Verify. Restoring Object.fromEntries makes the first fail.
Swept the class: the only other Object.fromEntries over query rows is a static step list with no repeatable keys.
The reason will be displayed to describe this comment to others. Learn more.
Low: retired kind-2 rows still decide a claim's place in the Positions order.
The query (~line 57) still fetches voteKind 1 and 2. This line stops treating a kind-2 row as an answer, but the row still reaches the seen / order.push bookkeeping below. So:
Ordering: a claim answered Agree last month and Verified yesterday sorts by yesterday's veracity vote, not by the stance it actually shows.
Page budget: kind-2 rows also use up the ORDER_MAX_PAGES budget, which can push real stance rows past the cap.
Suggested fix: drop kind 2 from the query filter, or skip non-kind-1 rows before the seen bookkeeping.
The reason will be displayed to describe this comment to others. Learn more.
Confirmed and fixed in 16669fe — I took the first of your two options, dropping kind 2 from the filter, since it fixes both symptoms at once and stops fetching rows we then ignore.
Checked both effects in the code before changing anything:
Ordering — seen / order.push run for every node regardless of field, so a kind-2 row did take the entity's slot in the vote order. The trailing order.filter(...) only removes claims with no answer, so a claim holding both kinds stayed in the list, positioned by the retired vote.
Page budget — confirmed, those rows counted against ORDER_MAX_PAGES (20).
Skipping non-kind-1 rows before the bookkeeping would have fixed the ordering but not the budget, which is why the filter is the better half of your suggestion.
Two tests in person-positions-stance.test.ts assert the printed document asks for voteKind: {is: 1} and never {is: 2}; restoring the old or: filter fails the second.
The new stale-wire normalization is not covered: every fixture in backfill-readiness-for-held-position.test.tsx still uses response_kind: 'stance', so switching this back to readiness.response_kind would leave the suite green. Add a case with response_kind: 'veracity' that asserts the notification is still sent as stance.
These adjacent comments make opposite claims about which label wins. Since the implementation intentionally ignores choice.position_label, remove the obsolete server-label sentence so future changes do not restore the retired vocabulary.
Correct explanation of wire and application response types
apps/web/core/responses/entity-response.ts:92
This now contradicts the corrected API model: inbound values are typed as WireResponseKind, not the narrowed DebateResponseKind, so TypeScript does prevent passing them directly where ResponseKind is required. Update this explanation to describe the explicit wire/application type split rather than saying the compiler cannot catch it.
This issue also appears on line 104 of the same file.
Working through Copilot's "Previously missed (3)" section — these are in the review body rather than as threads, so replying here. All three were right, and all three were mine. Fixed in f42a745.
1. The backfill's normalization had nothing holding it (medium)
Correct, and it is the failure mode worth catching: useBackfillReadinessForHeldPosition sends CLAIM_RESPONSE_KIND rather than the row's word, but every fixture in its suite was already response_kind: 'stance' — so reading the row produced an identical call and the assertion could not distinguish. A test that passes against the behaviour it is meant to forbid is not a test.
A case with a 'veracity' row now asserts we still send stance. Reverting the line to readiness.response_kind fails it, and only it.
Sweeping that class found the same gap one file over, which Copilot did not flag. claim-response-indexed-notifier guards on isClaimResponseKind, and every negative case in that suite differed by account rather than by kind — so the guard itself had nothing asserting it, and widening it back to admit 'veracity' left the suite green. Now covered, and likewise proven by reverting.
I checked the rest of the normalization sites rather than assuming: the card's write path, the ticker, matches-list, the rematch page, the OG share card and the glyph fallback all already feed a veracity value and assert the stance result. Those two were the only fixtures that could not tell.
2. Contradictory comments in taggedPositionSummaries (low)
Correct — and worse than stale, since both sentences were sitting there making opposite claims:
// A server-supplied label wins, so an authoritative Verify/Dispute survives.
// Our label, never geo-chat's stale Verify/Dispute — see `positionSummariesFromCounts`.
I replaced the line and left the one above it. The old sentence is gone.
3. The entity-response.ts docs understate the guarantee (low)
Correct, and it is a nice catch on ordering: both comments said TypeScript could not catch a wire value being used as a kind. That was true when I wrote them and stopped being true two commits later, when WireResponseKind split the inbound type from the app's — a change made in response to Copilot's own earlier finding. The docs were describing a hazard the type system had since removed.
Both now describe the split that does the guarding. CLAIM_RESPONSE_KIND explains that a wire value will not fit where a ResponseKind is wanted, and keeps the consequences as the reason the type exists rather than as a live risk; CLAIM_RESPONSE_COPY says it is there so a surface has something to reach for without a lookup.
tsc and eslint clean across the 72-file diff, 2305 tests passing in the affected suites.
Rebased onto master (now 36b9d15c6). Conflicts resolved — but git only flagged two of the five, so worth reading before approving.
The two textual conflicts
debate-video-tile.tsx — master extracted DebatePositionChip; this branch stopped taking positionLabel as a prop and derives it from participantPosition. Kept both: the extracted component, called with the derived label.
fetch-profile-facts.ts — import block only. Master dropped normId and added the hidden-debates imports; this branch added POSITION_VOTE_KINDS. Kept both.
Three that merged cleanly and were wrong
Two features landed while this branch was open, both written against the two-kind world it removes. Nothing overlapped textually, so the merge was silent.
debate-feed-player.tsx (#2543) — the new position chip over a debater's video read geo-chat's position_label. That field still says "Verify" on a claim geo-chat calls factual, so the feature this PR exists to remove would have shipped in a new place, and tsc would not have said a word. Now named from position like every other caller. Its fixture keeps the retired label on purpose so that the chip ignoring it is what the assertions prove; the "empty label" test is deleted rather than adapted — position is a non-null boolean, so the state is gone, not untested. Reverting the one-line change fails three tests in that suite.
disagreement-counts.ts / person-disagreements.tsx (#2556) — ClaimMatch.responseKind was a fresh 'stance' | 'veracity' literal and the badge hand-rolled responseKind === 'veracity' ? 'Verify' : 'Dispute' beside the shared helper. The type now comes from ParticipantPosition so the two can't diverge again, and the badge uses responsePositionLabel. These three did fail tsc, which is how I found them.
Kind stays in positionContext's pairing key. It holds one value today, but it is half of what makes two responses comparable, and dropping it would silently pair answers to different questions if a second kind ever returns.
Three of that feature's tests used veracity as a second axis to prove a claim is counted once when a pair opposes each other twice. A second space is the only axis left, so they use that. The "does not compare positions from different spaces or response kinds" case is now unreachable rather than fixed — I narrowed the test and said so in a comment instead of leaving a name that promises coverage that no longer exists.
Left alone
debug-debates-page-client.tsx still prints position_label verbatim in two places. That page exists to show what geo-chat returns, so the raw field is the correct thing there. Untouched by both sides.
tsc clean, eslint clean across 79 files, 3093 tests passing in the affected suites. One unrelated flake in recording-stream.test.ts ("reports one summary of how the live upload went") — untouched by this branch, passes 3/3 in isolation.
A claim carrying the "Is factual" flag was answered with Verify/Dispute, drawn
with chevrons, and published as its own vote kind. Every claim now asks the same
question, whatever the flag says.
The flag itself stays. The extractor still writes it and the debate surfaces
that read it are untouched — it just no longer decides how a claim is answered.
What goes with the vocabulary:
- the `veracity` response kind, its vote kind (2) and its verify/dispute/unverify
SDK methods, so there is no longer a way to publish one
- the chevron glyph, and the hand-written `#2A2B2E` a selected chevron needed
because it had no filled form to switch to
- `claimResponseKind`, and the branch in `resolveEntityResponseKind`. The latter
no longer takes a space: "Is factual" was the only per-space value it read
- the "Is factual" half of the unpublished-edit guard. A draft edit to the flag
cannot change which vote kind gets published any more, so blocking on it would
only disable the pills for no reason. The Claim *type* half still blocks, since
that really does move an entity between curation and stance
Existing veracity responses — 178 of them, on 145 claims — stop being read. That
is deliberate: those claims lose that part of their count and those responders
see them as unanswered. The votes are still on chain if we ever want to backfill
them as stances. The one exception is the curator onboarding checklist, which
still counts vote kind 2 when asking whether someone has *ever* taken a position
on a claim: they did, and un-ticking a finished step has nothing to recommend it.
Two things the wire forced, both of which would have been crashes:
geo-chat still labels old claims `veracity`, and that value arrives typed as the
narrowed kind it no longer matches — so `ENTITY_RESPONSE_COPY[kind]` and the
glyph map both missed and threw during render, blanking the claims ticker and
the matches list. Claim surfaces now read `CLAIM_RESPONSE_COPY` without indexing
anything, and the glyph map falls back rather than throwing.
geo-chat also sends `position_label` per side, and the pills preferred it over
their own copy — which would have printed "Verify" on a control that can only
publish an Agree. Our label wins now.
The first pass narrowed the type and patched the two lookups that happened to
throw in a test. It left the actual source alone: geo-chat's `response_kind`,
which still says "veracity" for a claim minted before the vocabularies merged,
was still being read and threaded through the claim UI.
TypeScript cannot see it. The field is typed as the narrowed kind it no longer
matches, so every use type-checks and then misses at runtime:
- `matchmaking-claim-card` handed it to `useEntityResponse`, which is the write.
`getResponseActionMethod` selects no SDK method for it, so clicking Agree
threw on `undefined['positive']` instead of publishing — on every claim with
existing verify or dispute activity.
- `ClaimSummary` keyed the responders read on it, where `responseKindToVoteKind`
returns `undefined` and the query asks for no vote kind at all.
- the backfill forwarded it to geo-chat, recording the retired kind against a
response published as a stance.
`CLAIM_RESPONSE_KIND` is now the authority and no surface reads the row. The
leaf guards stay as defence in depth rather than as the only defence.
Three more places were still reading vote kind 2, which the clean break says
nothing reads:
- the profile's position *count* asked for kinds [1, 2] while the list beside it
had already narrowed to [1], so a profile would have printed a number bigger
than its own list. The two were separate literals; one now derives from the
other.
- `decodeVoteOrder` still decoded a kind-2 row into a `veracity` field that
nothing renders any more, so a claim answered only that way entered the
Positions list with no verdict under either button.
- the indexed-response notifier admitted "veracity" through a hand-written guard
and an `as DebateResponseKind` that made the cast look safe. Both copies now
share one type guard.
Review follow-up. "Our label wins" only held for the pills; eight other surfaces
still printed geo-chat's `position_label`, which reads "Verify" or "Dispute" on
a claim it still calls factual — the rematch page, the debate room, the pre-join
screen, the request dialog and the request parties row.
Every participant type already carries `position` beside the label, and
geo-chat's label is only ever one of four fixed words (`position_label_for_
response` in crates/debates), so the label is fully derivable and nothing is
lost by deriving it. `DebateVideoTile` now takes only the side and names it
itself, which removes the prop from four call sites.
Also from the review:
- `DebateEntityResponseControls` is deleted. It had no callers outside its own
test — already true on master — and passed a wire-typed kind straight into
`EntityVoteButtons`, so it was a trap for whoever revived it.
- `responsePositionLabel`'s doc comment was orphaned above `CLAIM_RESPONSE_KIND`
when that constant was inserted; it is back on the function. `getUserHas
VoteOfKind` had two stacked comments, the first still describing veracity as a
live kind; they are one comment that says why the retired kind is still passed.
- Comments that still described Verify/Dispute as current: the ticker's card and
share, the `ClaimResponseState.responseKind` docs, the two in
`use-claim-response-state`, the `position` prop on `ResponsePositionIcon`, and
the end-slot prop in `claim-position-comment`.
Not fixed here, because it cannot be: geo-chat still resolves a claim's kind
from the graph's "Is factual" flag and rejects a mismatching response kind, so
"Request debate" will not appear on factual claims until geo-chat changes too.
Verified against geo-chat main at f8a2129. See the PR thread.
Review follow-up, found by re-sweeping after the rebase. The debate share card
took each speaker's side from geo-chat's `position_label`, which still reads
"Verify" or "Dispute" on a claim geo-chat calls factual.
This is the worst remaining place for the retired word to land. The card's own
comment says it: generated once at publish time and never revisited, so a wrong
label is baked into the share image permanently and cannot be corrected.
`cardSpeaker` now names the side itself, like every other surface. Nothing was
lost — `position_label` is only ever one of four fixed words, and the `position`
boolean it is derived from sits right beside it on the participant.
The existing fixtures carry `position` but no `position_label`, which is why
this went unnoticed: the field was simply `undefined` in every test. The new
cases pass one, so a regression has something to fail against.
Also dropped a reimplementation of `responsePositionLabel` in the claim card's
action-title helper.
Copilot review, plus the preference rule.
**The wire type was lying.** `DebateResponseKind` was narrowed to `'stance'`
while the endpoint still returns `'veracity'`, and `geoChatRequest` casts raw
JSON straight to its type parameter — so there is no parse step that could make
the narrowing true. The tests carried `as 'stance'` casts to work around it,
which is the lie showing through.
`WireResponseKind` now describes what geo-chat can send and types the inbound
`response_kind` fields; `DebateResponseKind` stays what the app resolves to and
sends back. The casts are gone, and the compiler guards the boundary instead of
a comment: a wire value no longer fits where a vocabulary gets chosen. That
turned up two props typed off the wire field which should always have been the
app's kind.
It also made one test's input unrepresentable — the ticker's share verb comes
from the hook's kind, which cannot be `veracity` — so that case is gone and the
type is the guard. The claim-payload cases keep their meaning and now compile
without a cast; breaking `WireResponseKind` proves they rely on it.
**The profile tag no longer waits on claim metadata.** It was held until the
response kind resolved, because a factual claim would have read "agrees" for a
beat and then corrected itself to "verifies". One wording now, so the wait can
change nothing — and `isResponseKindResolved` is `row !== null || entity !==
null`, so on a claim where neither ever lands it never flips and the tag was
hidden for good. The gallery card renders it directly; the interactive controls
keep their gate, which is a different question.
`responseNote` loses its response-kind parameter across all three callers.
**Preference, per the new rule.** Someone holding both a verify and an agree
resolves to the agree, because kind 2 is not read anywhere a position or label
comes from. Already true; now pinned, including the sharp case where the two
disagree — a Verify beside a Disagree reads as Disagree, not as the agreement
the verify row implies.
`claim-position-comment` stopped typing its prop off `MatchmakingReadiness` last
commit and kept importing it.
Worth saying how it survived: the lint gate I had been checking each round was
running against an empty file list. The command built its list with
`git diff -- apps/web` from *inside* `apps/web`, where that pathspec matches
nothing, so eslint was handed no files and reported no problems. Seventy files
were going unchecked and reading as clean.
Re-run against the real list, the whole diff has exactly this one warning, so
the broken gate had hidden one stale import rather than a backlog.
Both from Patrick's review, and both are the same migration doing the damage:
Verify is gone, so a factual claim's responder answers it again with Agree —
and their kind-2 row stays on chain, because nothing can clear it any more.
That person now holds two vote rows on one claim.
**The Agreed tab dropped those claims.** `voteKindByObjectId` was built with
`Object.fromEntries`, which gives a repeated key its *last* value, and rows
arrive `VOTED_AT_DESC` — so the map described every entity by its *oldest* vote.
On a Verify-then-Agree claim it reported kind 2, `useVoteTabEntities` compared
that against the kind 1 the claim resolves to now, and dropped it. Before this
PR the claim resolved to kind 2 as well, so it matched and showed: the
regression is ours, on exactly the path this PR sends people down.
Extracted as `indexVoteRowsByObject` so it can be tested at all, and it now
takes the first row per entity. `votedAtByObjectId` is built in the same pass:
read from a different row it can disagree with the kind, and it is the list's
sort key. The cross-page merge in `use-user-voted-entity-ids` had the same
last-wins shape for an entity whose rows straddle a page boundary.
Note this changes the Agreed/Disagreed tab's sort position for a claim holding
both kinds — it now sorts by the Agree, which is the vote being shown.
**The Positions order still spent its budget on retired rows.** The query asked
for kinds 1 and 2; the decode stopped treating kind 2 as an answer last commit,
but the rows still arrived, still took a slot in the vote order, and still
counted against `ORDER_MAX_PAGES`. So a claim Agreed last month and Verified
yesterday sorted by yesterday, and enough retired rows could push real stance
rows past the cap. The filter asks for kind 1 alone.
Swept the class: the only other `Object.fromEntries` over query rows is a static
step list, and the remaining `voteKind: { in: … }` filters are the onboarding
checklist's documented exception and one already narrowed to stance.
Copilot's "previously missed" set, all three mine.
**The backfill's normalization had nothing holding it.** It sends
`CLAIM_RESPONSE_KIND` rather than the row's word, and every fixture in the suite
was already `response_kind: 'stance'` — so reading the row produced the same
call and the assertion could not tell the two apart. A case with a `'veracity'`
row now asserts we still send `stance`.
Sweeping that class turned up the same gap one file over:
`claim-response-indexed-notifier` guards on `isClaimResponseKind`, and every
negative case there differed by *account* rather than by kind, so the guard
itself was unasserted. A key written under the retired kind now has to be
ignored. Both fail when their guard is reverted.
The other normalization sites already had discriminating fixtures — the card's
write path, the ticker, matches-list, the rematch page, the share card and the
glyph fallback all feed a veracity value and assert the stance result.
**Two comments were stale, and one was self-contradicting.**
`taggedPositionSummaries` carried both "a server-supplied label wins" and the
line replacing it, which is worse than either alone.
The `CLAIM_RESPONSE_KIND` and `CLAIM_RESPONSE_COPY` docs both said TypeScript
could not catch a wire value being used as a kind. That was true when they were
written and stopped being true two commits later, when `WireResponseKind` split
the inbound type from the app's. They now describe the split that does the
guarding rather than the hazard it removed.
Copilot found the reverse of Patrick's case, and my fix for his did not cover
it. The two kinds are independent, so the Verify can be the *newer* row: answer
a claim Agree while it is ordinary, have it flagged factual, answer it again
with Verify. "Newest wins" then reports kind 2 and `useVoteTabEntities` drops
the claim from the Agreed tab, with the kind-1 stance still live — the same
symptom his finding had, reached from the other direction.
Nothing resolves to kind 2 any more, so a retired row can never be an entity's
current answer. `indexVoteRowsByObject` skips them instead of ranking them.
`RETIRED_VERACITY_VOTE_KIND` moves to `entity-response.ts`, beside the
vocabulary, since two places now have to name it and they do opposite things
with it: a tally or a current side ignores it, the onboarding checklist counts
it because it asks what somebody has ever done.
Swept the rest of the vote-row queries: the response-count and summary queries
pin an exact kind, the positions and participant queries were narrowed earlier,
and `UserHasVoteOfKind` admits kind 2 on purpose for that checklist. This was
the only query returning mixed kinds into a current-answer computation.
Not taking the other half of the suggestion — excluding kind 2 in the query
itself. It would also stop retired rows using pagination slots, but the tab
pages on scroll rather than capping, so those rows defer a few later rows by one
page rather than losing any; against that, the document is codegen'd and the
config points at a remote schema, so changing it means either regenerating
`gql.ts` wholesale or hand-writing the query the way `participant-positions`
does to avoid exactly that. Not worth it for 178 rows that can only shrink.
Skipping retired rows in the page's lookups left them in its id list, and the
two are read by different halves of the same hook. `useUserVotedEntityIds`
binds an id to the first page it appears on and reads its kind from the
lookups merged across every page, so a claim whose retired row ended one page
and whose live stance began the next was claimed by the earlier page — which
had no kind for it — and skipped as a duplicate on the later one, which did.
`useVoteTabEntities` banks a page against its ids, and those do not change
when the kind arrives, so the claim was never re-hydrated and stayed missing
from the tab for the rest of the session.
Deriving the ids in the same pass also stops a retired-only entity costing an
entity fetch it could never be classified by. `decodeVoteOrder` already has
this shape — its `entityIds` are the ids a response survived for.
…faces
Two features landed on master while this branch was open, and both were
written against the two-kind world the branch removes. Neither conflicted
textually, so the rebase produced code that was wrong rather than code that
refused to apply.
The feed player's new position chip (#2543) read geo-chat's `position_label`,
which still says "Verify" on a claim it calls factual — the one word this app
can no longer publish, printed over a debater's video. It now names the side
from `position` like every other caller. Its fixture keeps the retired label
deliberately, so the chip ignoring it is what the assertions prove; the test
for an empty label is gone, because `position` is a non-null boolean and there
is nothing left for the server to leave blank.
The People tab's disagreement counts (#2556) spelled the kinds again as a
local `'stance' | 'veracity'` literal and hand-rolled the Verify/Dispute
wording beside `responsePositionLabel`. The type now comes from
`ParticipantPosition` so the two cannot diverge a second time, and the badge
uses the shared label. Kind stays in the pairing key: it is half of what makes
two responses comparable even while it holds one value.
Three of those tests used veracity as a second axis to prove a claim is
counted once. A second space is the only axis left, so they use that, and the
"different response kinds" case is noted as unreachable rather than quietly
rewritten into something that passes for another reason.
#2541 rewrote entity-response.ts on master and it no longer imports getChecked
from the checkbox, so that piece of debt is paid. The staleness check caught it
on the merge commit, which is what it is for: an entry left behind would go on
licensing the same import if someone reintroduced it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes the Verify/Dispute vocabulary. Every claim is now answered with Agree/Disagree, whatever its "Is factual" flag says.
The flag itself stays. The extractor still writes it (debate-publish-draft.ts) and the debate surfaces that read it are untouched — it just no longer decides how a claim is answered.
What's gone
veracityresponse kind2) and itsverify/dispute/unverifySDK methods — there is no longer a code path that can publish oneVOTE_CHEVRON_SELECTED_CLASS, the hand-written#2A2B2Ea selected chevron needed because it had no filled form to switch tocore/claims/response-kind.tsresolveEntityResponseKindalso loses itsspaceIdparameter, since "Is factual" was the only per-space value it readNet −118 lines across 56 files.
Existing veracity responses go dark — as agreed
178 responses, on 145 claims, by 26 voters (Aug 6 → the day this was written; I queried the indexer). Those claims lose that part of their count, and those responders will see them as unanswered and can answer again. The votes are still on chain, so a backfill re-publishing them as stances remains possible.
One deliberate exception: the curator onboarding checklist still counts vote kind
2when asking whether someone has ever taken a position on a claim. They did, and un-ticking a finished step has nothing to recommend it — unlike the tallies, where dropping the kind is the point. It's a named constant with the reasoning next to it.Two crashes the wire forced
Both would have shipped without a test that fed in what geo-chat actually sends.
1. Blank surfaces. geo-chat still labels claims minted before this change
"veracity". That value arrives typed as the narrowed kind it no longer matches, so TypeScript can't see it — butENTITY_RESPONSE_COPY[kind]and the glyphRecordboth miss, returnundefined, and throw on the first field read. That's a blank claims ticker over a debate video, and blank cards in the matches list. Claim surfaces now readCLAIM_RESPONSE_COPYwithout indexing anything, andResponsePositionIconfalls back instead of throwing.2. The retired word, rendered. geo-chat sends a
position_labelper side, and the pills preferred it over their own copy — so a stale"Verify"would have printed on a control that can only publish an Agree. Our label wins now.Testing
596 files / 7013 tests passing;
tscandeslintclean (zero warnings on the diff).Roughly 25 tests asserted the old vocabulary. Where a test named the behaviour being removed I inverted it rather than deleting it — those claims are exactly the ones that have to come out Agree/Disagree now:
matches-listandrematch-page-client: "uses Verify and Dispute for factual claims" → asserts Agree/Disagree on a claim geo-chat still calls factualdebate-claim-ticker: "65% verify" → "65% agree", and asserts the old string is absententity-vote-buttons.batch: chevron viewBox (0 0 16 16) → thumb (0 0 12 12)entity-vote-buttons.claim-space: the two per-space factual cases collapse into one invariant — wherever the flag is set, the controls are identicalclaim-response-summaries: now pins that retired kind-2 rows are excluded from a claim's tally and from the viewer's own positionTests whose premise became impossible (a claim changing vocabulary) are removed with a comment saying why, rather than silently dropped.
New coverage, each verified by breaking the fix first:
stance, publishes the stance vote kind, and names its sides Agree/DisagreeResponsePositionIconfalls back for a kind that is no longer a kind (fails with the guard removed)PositionRowignores a stale"Verify"label from the server