Repository navigation
Cosign conference input from its card, with cosigns grouped under it - #2319
Open
fastfadingviolets wants to merge 19 commits into
Open
fastfadingviolets wants to merge 19 commits into
fastfadingviolets wants to merge 19 commits into
Conversation
Draft 1 of the cosign feature requirements: triggers, functional and non-functional requirements, failure behaviour and what is out of scope. The UI it describes is the client designer's /cosign-viz mockup, pinned at k-g-k/maple-conference@19bd101. Closes #91 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cosign is conference testimony that names the publication it seconds, by id and author, in two new fields that are both set or both null. It is published through the same path as other testimony: publish reads the parent in its transaction, refuses a parent that is gone, the cosigner's own, itself a cosign, or on another conference, and copies the parent's position on first publish and keeps it on every edit. A published cosign stays a cosign of that parent, and plain testimony stays plain. A cosign's note may be empty, and it takes no attachment; other testimony still needs content. The Firestore rules already allow an empty draft, so they are unchanged. Existing testimony has no reference and reads as before. Closes #93 Verified: yarn test:integration-ci on the six suites CI runs (83 passed), including new cosign cases for filing, note edits keeping the copied position after the parent changes, a double submit leaving one cosign, and each refusal; functions unit tests from src (128 passed); type checks (root errors only in untracked local scratch scripts); prettier check; lint on the changed files. Not verified: a cosign published against the dev backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
publishedAt moves with every edit, so nothing on a publication said when it was first filed. Publish now sets firstPublishedAt on a first publication and keeps it through edits. Editing older testimony that lacks it leaves it missing rather than stamping the edit time, which would claim the edit as the first publication. Nothing fills it on older testimony: the cosign tally is its only reader, and every cosign is new. Verified: yarn test:integration-ci on the six CI suites, including first publication kept through an edit and older testimony left without it; functions type check; root type check; prettier; lint on the changed files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cosigned testimony now carries how many have cosigned it, how many distinct Senate and House districts they come from, and when the latest cosign was first published. A trigger on published testimony recounts them from the cosigns whenever a write changes what a cosign contributes, inside a transaction that reads the parent and every cosign of it. Retried or reordered events therefore land on the same figures, and an edit to a note writes nothing. A cosigner with no district on file counts toward the total only. The recount reads cosigns through a new collection-group index on cosignOfId. Publish replaces the publication on every version, so it now carries the tally across when the parent's author edits. It also records the author's districts, which it never had: it read profile fields that don't exist rather than the district on the profile's senator and representative. Closes #94 Verified: yarn test:integration-ci on the six CI suites, with Typesense running as in CI, including the tally following cosigns published from shared and distinct districts and with none, a note edit not counting as a new cosign, an admin deleting the newest cosign, the parent's author editing, and districts recorded on plain testimony; cosignTally unit tests and the rest of functions src (132 passed); functions and root type checks; prettier; lint on the changed files. Not verified: the trigger or the cosignOfId index on the dev backend, or under a burst of concurrent cosigns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deleting testimony now deletes its cosigns, each through the same performDeleteTestimony as any other delete, so each cosign comes off the conference's totals and its cosigner's draft loses its published version. The sweep runs after the parent is gone, when publish already refuses new cosigns, and runs even if the parent was already deleted, so deleting it again finishes a cascade that stopped partway. Moderation removal now deletes inside resolveReport through the same function. It used to resolve the report and then call a Next API route that deleted the publication without touching the bill or conference counts. The route came from codeforboston#1046, written while the deleteTestimony callable was disabled by policy and could only delete the caller's own testimony. The admin version of the callable merged the same day, but the moderation page kept calling the route, so moderator removals have left counts high since. The route, its client helper and the extra archive copy it wrote are gone, since publish already archives every version. The cosign tally trigger reads the parent first and stops when it is gone, rather than reading every remaining cosign on each delete of a cascade. Closes #95 Verified: yarn test:integration-ci on the CI suites with Typesense, including a parent with two cosigns deleted by an admin and removed by a moderator (no cosigns left, drafts cleared, conference totals back to zero), one cosign deleted leaving the parent and the other, and the existing moderation case now checking the publication is gone; testimony and moderation suites again after the tally change (72 passed); functions unit tests from src (132 passed); functions type check; root type check (errors only in untracked local scratch scripts); prettier check; eslint on the changed files. Not verified: the cascade or the moderation removal against the dev backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report list and detail showed an archive id read from resolution.archiveTestimonyId, a field nothing writes; resolveReport wrote archivedTestimonyId instead. Since removal no longer writes a separate archive copy, that value was only the testimony id the report already carries, so the column, its label and the field are gone. RemoveTestimony also loses imports and a type it never used, among them firebase-admin, a server package, in a browser component. Verified: moderation integration suite (5 passed); root type check (errors only in untracked local scratch scripts); functions type check; eslint on the changed files; prettier check. Not verified: the moderation report pages in a browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trigger that records a notification event for each new publication crashed when a publication lacked the court, bill title or author: court.toString() threw, and add() rejects undefined fields. Each crash restarts the emulator's function worker. It now skips a publication whose event would have an undefined field, logging which ones. On every other write it queried notificationEvents before checking the version, so each cosign tally update paid a query. It now returns before the query when the version is unchanged. The handler is split out from the trigger, as the ballot question trigger does, so it can be unit tested. Closes #114 Verified: unit tests cover a complete creation, creations missing billTitle, court or authorUid, a tally-only update (no database call) and a version change. The testimony integration suite passes with no crash from this trigger, writes returning at the version check and one incomplete fixture skipped. Not verified: on the dev project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deleting testimony used to delete its cosigns inside the same call, one transaction after another. If that stopped partway, for instance when a parent with many cosigns ran the callable past its timeout, the parent was gone while some cosigns stayed published and still counted on the conference. Nothing could finish the job: the admin delete needs a publication to delete, and resolving the report again returned "testimony-already-removed" before reaching the cosigns, so the report also stayed open. Cosigns are now deleted by a trigger on any published testimony's deletion, retried until it succeeds, like the cosign tally. Each cosign still goes through performDeleteTestimony. Resolving a removal whose testimony is already gone now records the resolution instead of stopping, since the delete is what had already happened. Closes #95 Verified: yarn test:integration-ci on testimony.test.ts and moderation.test.ts with Typesense (75 passed), including admin and moderator deletes of a parent with two cosigns, and a removal resolved after its delete had already gone through; the emulator log shows the cascade trigger with no errors. yarn --cwd functions jest src (138 passed), functions tsc, eslint and Prettier on the changed files. Not verified: a cascade actually interrupted partway and retried (the test starts from a finished delete); behaviour on dev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Version numbers run across every publication a person makes on a bill, ballot question or conference, including deleted ones. Publish read any version past 1 as an edit, so a cosign filed after the person's earlier cosign was deleted with its parent was refused for lacking an edit reason, and after five such deletions the conference was closed to them. An edit is now a publish that replaces a current publication: only then is an edit reason required. The five-edit limit counts the publication's own archived versions rather than the version number. Archives from before July 2022 carry no publication id, so a publication that old is limited only on its later edits. Verified: yarn test:integration-ci on testimony.test.ts and moderation.test.ts with Typesense (75 passed), including a cosign refiled after a cascade publishing as version 2 with no edit reason, then taking five edits to version 7 and refusing a sixth on the edit limit. yarn --cwd functions jest src (138 passed), functions tsc, eslint and Prettier on the changed files. Not verified: the limit on a publication with pre-July-2022 archives; the detail page's history and "edited" flag for a refiled publication, which still show the deleted one's versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cosign panel tells a reader that the input they are cosigning has been
removed, or that the conference has stopped taking input. Both refusals are
failed-precondition, and a gone parent shares not-found with a gone draft,
so the error code cannot tell them apart. fail() now takes details, and
publish sends { refusal: "cosignParentGone" | "conferenceClosed" } with
those two refusals.
The rules for what can be cosigned move to a module with no dependencies,
so the website offers Cosign by the rule that publishing keeps: an
individual's or an organization's testimony, never a legislator's, a cosign,
or one's own. A legislator's testimony was cosignable before. The draft's
cosign shape lives there too, so the website's draft type can share it.
Verified: yarn test:integration-ci on the six suites CI runs (97 pass,
including the new legislator refusal and the gone-parent refusal's
details); yarn --cwd functions jest src (138); functions tsc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The testimony edit hook ignored a watched draft or publication that stopped existing, so a page kept what another tab, or now a cosign's cascade, had deleted: the conference panel reopened as an edit of a gone publication, asking for an edit reason, and saves failed against the deleted draft. A snapshot that the server, not the local cache, reports gone now goes through the hook's own delete and discard. Verified: on the local emulators, deleting the signed-in user's cosign and draft behind the open conference page, then filing plain input from Share without an edit reason, which posted. Not verified: the bill and ballot question pages, which share the hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reader can cosign an individual's or an organization's input on a conference from its card, or from the card opened whole. The panel fills the window with the input being signed beside it: the reader says they have read it, then adds an optional note or says they are adding none, and lands in Your Input, which names what they cosigned. Editing a posted cosign changes its note only, with a reason. A signed-out reader goes to sign in; Cosign is offered only while the reader has no input of their own here, posted or drafted; and the two refusals the server words are shown in the panel. The share-with-the-committee choice in the mockup is left for its own issue, since nothing reads it yet. The letter sits in the well our bill text uses, lighter than the mockup's grey. Both forms share one draft hook held by the page, and its autosave, so switching between them never files two drafts, and a quick post no longer saves twice. Below lg the panel is now as wide as the window, where a narrow phone used to cut off the plain form's edge. Closes #97 Verified: on the local emulators (seedLocal), beside the pinned mockup at about 1280px and 390px: the card's Cosign, both steps including the unticked Continue and the empty-note ask, filing with and without a note (the parent's tally went from 3 to 4), the landing, editing a cosign, deleting the parent and closing the conference mid-cosign, a signed-out Cosign opening sign-in, and plain Share, Review and Post unchanged; npx jest components/conference (new tests pass; load.test.ts fails as it does on main); tsc, prettier and lint. Not verified: the edit-cosign view, which the mockup does not draw, by the designer; widths other than about 1280px and 390px; the dev site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local seed adds four cosigns of its sample testimony, with and without notes, from cosigners with districts, so the cosign panel and Your Input can be laid beside the mockup with a parent that already has a count. The functions emulator's trigger keeps each parent's tally, as it would on dev. Every seeded testimony now carries firstPublishedAt, as publishing sets it. Verified: yarn firebase-admin run-script seedLocal --env local on fresh emulators, after which the Massachusetts Medical Society sample carried cosignCount 3 from 2 Senate districts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's mockup brings the card's furniture down with its type: the portrait to 34px under 767px and 26px under 450px, the account marks to 14px and 13px, and the position chip to 10px with less padding under 450px. Ours kept the desktop sizes at every width. The sizing sits on the card's own portrait rather than the shared disc, so the cosign panel's portraits keep theirs. Verified: computed sizes in a 390px frame beside the mockup at k-g-k/maple-conference@19bd101: portrait 26px with 10px letters, marks 13px, chip 10px with 6px padding; desktop unchanged at 40px. Not verified: the 767px tier, between phone and desktop widths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cosigned card now carries the mockup's band ("N constituents have
cosigned this", "including you" with a way to your own input, and the
Cosign button) and its floor (the count, the Senate and House districts
cosigners come from, and the last-cosigned date). Its cosigns are listed
under it, a tier quieter than the card, newest first, three shown and
the rest behind "Show N more cosigns".
This departs from the mockup's flat feed, where each cosign is a card
at its own date and noteless ones trail at the end: a card now ranks at
its latest cosign, so cosigning brings it back up, and its cosigns go
with it. Filters and paging take a card and its cosigns as one unit.
Cosigns without a note are listed only with "Show cosigns without
content" ticked, and counted either way. The local seed gains two more
noted cosigns so a card shows the cap.
Closes #98
Verified: band and floor beside the mockup at
k-g-k/maple-conference@19bd101 by computed style at desktop, and in a
390px frame; on local emulators, cosigning a card as the seeded test
user brought it to the top with "including you", its cosign listed
under it, and View input landed on Your Input; the noteless setting,
the expander and the badge count in the browser; components/conference
jest (the grouped feed's ordering, units, filters, paging, cap, noteless
setting and districts).
Not verified: the client's review of the grouped list, which is our
design; the 767px tier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
At 900px and under, the floor hid its "Last cosigned" date so the card's foot line could carry it, but only a narrow card has a foot line. A card 600px or wider in such a window (no rail, ~700px) showed the date nowhere. Entry now hands the floor the card's width, as it does its cosigns, and the floor hides the date only on a narrow card. Verified: npx jest components/conference passes apart from load.test.ts, which fails on main from a stale fixture. Not verified: the date by eye on a wide card in a narrow window, or at phone width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed save left the Post or Cosign button disabled for good and a timed autosave stuck on "Saving". A post pressed while the edit hook was still loading the draft had its save dropped and waited forever; it now saves again once the draft settles. A flush made while a write is in flight no longer starts a second one, so a quick Agree then Cosign can't add two drafts. Delete Draft comes back when the fields return to what is kept. The compose form, which stays mounted behind a closed panel, no longer takes a cosign draft for its own: it does not seed from one, and its draft check refuses one. Before, it could publish a cosign draft as a cosign, or once refusing them, autosave a cosign's note as the reader's own input after every page load. A cosign posted without words reopens with "no words" only when no note has been drafted since, so that note is not hidden and wiped. Signing out clears the page's record of the draft, so a signed-out visitor sees Cosign. Verified: npx jest components/conference passes apart from load.test.ts (stale fixture, fails on main); InputForm.test fails without the seeding fix. On local emulators, a cosign draft with a note survived a reload as a cosign. yarn check-types clean in the changed files. Not verified: failed saves and the dropped-save post by hand; sign-out with a draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reader has one draft on a conference. With a note drafted cosigning one input, agreeing to cosign another wrote over that note without a word. Cosign stays on every card, but the agreeing step now says that continuing deletes the note on the other cosign, naming it when it is still in the feed. Verified: npx jest components/conference passes apart from load.test.ts (stale fixture, fails on main), with tests for warning and for staying quiet over this parent's draft or one without words. On local emulators: drafted a note cosigning Fight for the Future, reloaded, pressed Cosign on Massachusetts Medical Society, and the warning named Fight for the Future. Not verified: the warning at phone width; the wording with the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
fastfadingviolets
marked this pull request as ready for review
October 29, 2026 15:25
fastfadingviolets
requested review from
alexjball,
mertbagt,
mvictor55,
nesanders,
sashamaryl and
timblais
as code owners
October 29, 2026 15:25
fastfadingviolets
requested review from
Mephistic and
kiminkim724
as code owners
October 29, 2026 15:25
This branch was successfully deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.



This builds the cosign UI on the data layer in cosign-data: a reader can cosign an individual's or an organization's input on a conference from its card, through the client's two-step panel, and edit their cosign's note afterwards. A cosigned card now shows who stands behind it, in the mockup's band and tally floor, with its cosigns listed under it and the card brought back up the feed when cosigned. Grouping cosigns under their card is a departure from the mockup's flat feed, where each cosign trailed as its own card, and it waits on the client's review. Along the way the server says why it refused a cosign, a legislator's testimony can no longer be cosigned, and the card is sized down on phones as the mockup draws it.
Closes hyphacoop#97
Closes hyphacoop#98
What's in it
Not verified
🤖 Generated with Claude Code