Skip to content

Record, tally and delete cosigns of conference testimony - #2314

Draft
fastfadingviolets wants to merge 7 commits into
codeforboston:mainfrom
hyphacoop:cosign-data
Draft

fastfadingviolets wants to merge 7 commits into
codeforboston:mainfrom
hyphacoop:cosign-data

Conversation

@fastfadingviolets

Copy link
Copy Markdown
Contributor

This branch adds the data layer for cosigning conference testimony, as specified in the requirements it opens with. A cosign is published like any other testimony but names the publication it seconds and keeps that publication's position. Cosigned testimony carries a tally of its cosigners and their districts, and deleting or moderating away a publication now takes its cosigns with it. Along the way it records when testimony was first published, and stops the testimony notification trigger from crashing on incomplete publications and querying on every write.

Closes hyphacoop#91
Closes hyphacoop#93
Closes hyphacoop#94
Closes hyphacoop#95
Closes hyphacoop#114

What's in it

Not verified

🤖 Generated with Claude Code

fastfadingviolets and others added 4 commits October 27, 2026 14:20
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, so the
functions deploy and the backfill can run in either order.

backfillTestimonyFirstPublishedAt fills it from the archive, which every
publish has written to since 2022 and deletes keep. It takes the earliest
archived version with the publication's id, or, for archives from before
July 2022 that carry no id, with the same bill, court and target; the
earlier wins. A version 1 publication with no archive is its own first.
It has --dry-run, skips documents already set, and is listed in
scripts/README.md; its integration test joins the CI list.

Verified: yarn test:integration-ci on the six CI suites plus the new
backfill suite (94 passed), including first publication kept through an
edit, older testimony left for the backfill, and the backfill's id,
pre-July-2022 scope, admin-deleted republish, version 1, already-set and
dry-run cases; functions type check; root type check; prettier; lint on
the changed files.
Not verified: the backfill against dev or prod data, including how many
publications it leaves with no first publication found.

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 plus the backfill
suite, with Typesense running as in CI (94 passed), 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>
@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
maple-dev Ready Ready Preview Oct 27, 2026 7:45pm UTC

Request Review

fastfadingviolets and others added 3 commits October 27, 2026 15:41
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 seven CI suites with Typesense
(97 passed), 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, or a parent with thousands of cosigns, which are deleted one at
a time within the callable's timeout.

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 and backfilled document 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 and firstPublishedAt backfill
integration suites pass (71) with no crash from this trigger; 193
writes returned at the version check and one incomplete fixture was
skipped.
Not verified: on the dev project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,67 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this was a second delete route that reimplemented testimony deletion in a different way and would've had to be updated to cascade the cosign deletes. I don't think this code is being used, so it seemed easier to just drop it.

This branch was successfully deployed

1 active deployment
Preview – maple-dev — a9a87d87 Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant