Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Drop the nomination lexicon and its OAuth scope
Votes-only Task 1: delete dev.wafflehouse.influential.nomination, strip nomination fields/values from defs, getStats, and getVoterStatus, and remove nominationRecord/NominationRecord from the lexicons.ts barrel and buildScope. pnpm check is expected to stay red in server/browser files that still reference the removed fields until later tasks.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Drop nominations from schema and squash migrations to init
Remove the nominations table and the recordCols() factory it shared with votes now that only voting remains; inline votes' columns directly and rename its subject/status index to vote_status_subject (status, subject) to match the WHERE status='counted' GROUP BY subject query shape. RECORD_STATUSES drops not_nominated, leaving the four values the vote lexicon now declares.
Since the product is pre-release, squash the six-migration history into a single drizzle/0000_init.sql covering the current ten-table schema.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Rank votes per author and drop the pool cascade
Pool membership is now "a did with at least one counted vote", so a vote's status depends only on its own author's rows. put/del each re-rank exactly one author in one synchronous transaction; inPool, votersOf, subjectsOf, cascade, the watch map, placeholderStatus, tableFor and maxFor are gone, along with RecordKind and maxNominations.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Rank the pool from votes alone across every read path
The pool CTE now groups votes once instead of unioning two record tables:
rank ties on the vote count alone, and position — the gapless paging key —
breaks those ties by the subject's earliest counted vote, then by did. That
MIN(created_at) is a string compare, correct only because the indexer
canonicalizes every stored createdAt, the same way stats.ts documents its
MAX.
nominationCount is gone from RankedRow/SELECT_NOMINEE and so from the
leaderboard and profile bodies; getStats drops participants and reads one
aggregate over counted votes; getVoterStatus answers with votes only and
takes { maxVotes }; the actor refresher's backfill union reads the two vote
columns.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Wire config, XRPC, and Jetstream for votes-only, retire nominate OG/SEO copy
Drops maxNominations from config, .env.example, +layout.server.ts, and the getVoterStatus/getStats XRPC handlers. jetstream/start.ts no longer registers a nominationRecord commit handler; toRecordEvent/toDeleteEvent/onPut/onDel lose their kind parameter now that only votes flow through the indexer. Updates the site and profile OG cards and the site SEO description to votes-only copy.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Drop the collection axis from browser stores for votes-only
records.svelte.ts loses RecordKind, nominations, nominationsLeft, hasNominated, nominate(), and Limits.maxNominations; lastWrite is now { subject, handle? }, justFilled() and statusOf(uri) take no kind argument, and DidString derives from VoteRecord instead of NominationRecord. Guard messages become vote-specific ("You can't vote for yourself.", "You already voted for them.", "You have used all N of your votes. Remove one first.", "Couldn't read your existing votes. Reload and try again."). Test file rewritten first (TDD): mock bodies drop the nomination fields, NOMINATION_URI becomes VOTE_URI, and the exhausted-limit case is reached via configure({ maxVotes: 1, closed: false }) plus one existing vote.
engagement.svelte.ts drops the now-unused pool/checkPool/POOL_BATCH (the vote page's own pool lookup, going away in Task 8) and its getProfilesQuery import. optout.svelte.ts's getProfile failure copy becomes "Couldn't check whether you're a nominee ()."
pnpm test is fully green (543 passed, 1 pre-existing skip); pnpm check's 47 remaining errors are confined to six .svelte files for Tasks 7-9 to fix.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Fix fabricated RANK_LIMIT rationale in engagement.svelte.ts
The reworded comment from the previous commit claimed the 25-vs-fewer-shown headroom exists so a page has something left after filtering out a self-vote or already-voted-for account. That isn't how EngagementRecs works: it renders every ranked row with a rowAction and no filter/resolving step. The real reason is pagination, already documented in EngagementRecs.svelte:91-92 ("The store keeps more than any page shows, which is what 'Show more' walks") — reworded RANK_LIMIT's comment to match.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Retire the filed-nomination slip and cut nav down to three tabs
+layout.svelte drops the slip's state, effect and markup, collapses stackHeight/slipHeight into one stackBase, and publishes only --stack-h; every lastWrite?.kind === 'vote' becomes a plain undefined check now that MyRecords no longer distinguishes record kinds, and the mobile tab bar and desktop cluster carry Leaderboard/Vote/My ballot only. layout.css drops .mo-tuck and its lg override, and every comment that named the slip or a "nomination filing". Toast, StatusBadge, NomineeCard and BallotCard lose their nomination-shaped props, statuses and doc lines; NomineeCard gains an optional note for Task 8's zero-vote empty state. Icon gains a pencil glyph. Login copy now names the four votes-only permissions the site actually asks for.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Let NomineeCard's note open the tally row without a vote count
hasTally was gated on voteCount alone, so a note passed for an account outside the pool (voteCount undefined) never rendered — exactly the case Task 8 needs it for. hasTally now also opens on note being defined.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Merge the nominate page into the vote page
One page, one card, one Vote button. The handle typeahead and its NomineeCard move here from /nominate, backed by a single abortable getProfile read: a pool member's card arrives with a rank and a tally, and anyone else gets the line saying the first vote is still going. The pool search goes — anyone on Bluesky can be voted for now, so searching the standings was the wrong way in.
EngagementRecs and FollowSampler lose their panel variants and their pick callbacks: both are list-only, both take a required rowAction, and the page hands each the same Vote snippet, so every suggested face votes in place. FollowSampler picks up the engagement list's row anatomy.
/nominate is deleted.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Move the campaign editor under the profile header and finish the votes-only copy
The profile page's action row becomes the single entry point into the campaign editor: on your own profile its primary slot is an "Edit campaign profile" button that calls the component's newly exported start() and scrolls the editor block — moved up under the header card, because it edits that card — into view. CampaignEditor loses its own collapsed-state button, and /profile/#edit opens the editor once the session settles, so /me can deep-link to it.
The badge row counts the vote you just cast, so voting for a stranger flips
it from "No votes yet" to "1 vote" a firehose hop before the server agrees,
and top posts now render for everyone — off the pool they say so rather than
claiming a pull that was never queued. VoteState drops its self kind,
which the editor branch made unreachable.
/me trades its nominations half for a "Campaign profile" tile pointing at that deep link, and the home page drops the nominate call to action, the participants tile and the per-row nomination column.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Let the opt-out badge beat a just-cast vote
The pool query excludes an opted-out account outright, so no vote can
re-admit them — showing "1 vote" on their profile the moment you voted was
a promise the leaderboard never keeps. The opt-out branch is now tested
ahead of inPool || justVoted; the other two branches are unchanged.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Update README and CLAUDE.md for the votes-only model
Drop every reference to the removed nomination lexicon/record type: pool membership is now "a did with at least one counted vote," MAX_NOMINATIONS is gone, the status model has four values (no not_nominated), and the XRPC contract is eight queries plus setOptOut. Adds a Railway runbook for the one-time volume wipe this migration requires (migrations were squashed to a single 0000_init.sql) and fixes four leftover source comments that still described the old two-record-type world.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Pin the pool's tie key to MIN, and close RANKED over its one caller
The two tie tests seeded every subject's votes at a single instant, so MIN(created_at) and MAX(created_at) ordered them identically and neither test could have caught the key being swapped. Re-seeded so one subject's votes straddle the other's (:01/:09 against :02/:03): under MIN it leads, under MAX it trails. Verified RED by flipping the SQL to MAX — exactly those two tests failed — then restored MIN.
RANKED is imported nowhere outside pool.ts, so the doc paragraph claiming nomineeProfile.ts consumes it was stale twice over; it is now a module-private const with a comment saying every read path goes through getPool.
Also: the generated voter dids in the votesFor helpers are unique by construction rather than by digit length, the MAX_VOTES comment is singular, and the stats seed comment says what the seed now is.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Hide top posts for opted-out profiles, and tidy the vote page's edges
The top-posts block promised "will load once someone votes" on a profile that opted out — but the pool query excludes them permanently, so no pull is ever queued and nothing could ever arrive. Gated on !profile.optedOut.
Vote page: the standing fetch swallowed every error into a bare catch, so a real failure was indistinguishable from the abort that supersedes a pick — it now returns on AbortError and console.debug()s the rest; and a vote cast while that fetch is in flight no longer has its card overwritten by the late response. "Surprise me" told a reader with an empty pool they had "already voted for every nominee", since getRandomNominee answers 404 for both; it now says "No one to suggest yet". handleValue carries a note that it exists for bind:value and is never read.
Profile page: openEditor awaited start(), which opens the form synchronously and then reads the record from the PDS — so the scroll waited on the network. It now fires the open and scrolls after a tick.
FollowSampler: FollowActor.handle is required, so the {#if} around OpenIn was always true; and the empty
spacer under the list is a real conditional now, with justify-end holding the button in place.
Also: the leaderboard's empty state uses the ballot icon (plus had no other caller and is gone from Icon), and EngagementRecs imports $lib/ui once.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW
Correct PROFILE_VISIBLE's home, and note the leftover nomination records
PROFILE_VISIBLE moved to pool.ts with RANKED; CLAUDE.md still pointed at leaderboard.ts. And voters who took part before the votes-only change may still hold dev.wafflehouse.influential.nomination records in their own repos — nothing indexes them and the narrowed OAuth scope means this app cannot delete them either, so the README now says so rather than leaving the next reader to wonder.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01My1pvJK8oPRefd51xKiyUW