Skip to content

Society votes: members amend the charter by liking a proposal - #15

Merged
Spider333 merged 5 commits into
mainfrom
feat/society-votes
Oct 24, 2026
Merged

Spider333 merged 5 commits into
mainfrom
feat/society-votes

Conversation

@Spider333

@Spider333 Spider333 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Society votes: members change their charter by liking a proposal, in any Farcaster app, and anyone can recount the result from their own node. Outpost already had exit (Leave); this adds voice.

Draft. Do not merge before the prize deadline (Sat 24 Oct 14:47 UTC). David decided on 23 Oct: merge after the deadline without the live mainnet test. Before merging: mark ready, bring in main (merges cleanly as of 23 Oct), CI green. Merging to main redeploys the hosted client on GitHub Pages.

The four rules (src/core/votes.ts)

  1. A proposal is a member's cast that names the charter's current head and lists changes (+/- member, +/- moderator), each account as a signed mention.
  2. An approval is a member's like on it. Once a client has seen it, it counts for good (kept in an approval cache, so an unlike or pruning changes nothing).
  3. It passes when more than half of the members of that version approve. Leaving needs only your own approval.
  4. The charter becomes an append-only log: the founder's signed charter, then each passed proposal with the signed likes that passed it. The head is the hash of the last passed proposal. A log that branches is shown as a fork, never merged.

No deadlines: Farcaster timestamps are author-set and can be backdated (Snapchain only refuses dates more than 10 minutes ahead), and block times survive 3 days on a node, so a deadline could not be re-checked later. A proposal stops counting once another passes, because it then names an old head.

What is in it

  • Core: proposal format and strict parser, log type and replay (recount), live tally with approval cache, advance (appends what passed, refuses to adopt when two proposals pass at once), compareLogs (same / extends / behind / fork / other society), propose, announce.
  • Key checks for votes: Verifier.voteKey checks that the key belongs to the account and, for gasless keys, that its scope allows the message type (Snapchain refuses out-of-scope likes, but a hand-made log could contain them). Likes must be mainnet. Keys revoked since a vote are labelled "cannot be re-checked now", and an amendment whose majority rests on them is "not recountable from scratch"; importing such a log needs an explicit yes.
  • Node reads: likesByCast and repliesTo (plain GETs, so the society gate needs no change; a gate test covers it).
  • buildCast takes mentions and sends casts over 320 bytes as LONG_CAST. This also fixes a pre-existing bug: casts of 321 to 1024 bytes were sent as CAST, which the node rejects.
  • CLI: charter proposals | propose | announce | import, charter verify recounts logs; census, feed and join use the current version.
  • Web client: a Votes section in the Society card (open proposals with tally and Approve, Propose a change, version history, Recount, Download log, Post the result).
  • Docs: README "Society votes", TRUST.md "What a society vote proves".
  • test/votes-demo.ts: a fake node with a 5-member society and an open proposal, to try the web client without mainnet accounts.

Tests and checks

  • npm test: 91 pass (70 existing + 21 new in test/votes.test.ts, including end-to-end CLI runs against HTTP fake nodes, tamper tests, forks, conflicts, revoked keys, out-of-scope keys, testnet likes, clock skew, forged logs).
  • Security review (automated, on the first commit): the CLI used logs whose majority rests on keys no node lists (as made-up keys would be) without asking. Fixed in f1944e8: every CLI command that loads a log refuses such a version unless --trust-unrecountable is given, and charter verify exits 1. Not adopted: treating unlisted keys as forged, because a legitimately revoked gasless key looks the same, so honest history would stop replaying.
  • tsc strict, plus --noUnusedLocals --noUnusedParameters: clean. npm run build: clean. jscpd: 0 clones.
  • Mutation check: disabling the mainnet rule makes a test fail.
  • Real browser (Chrome) against the demo node: load society, approve, pass, recount, propose (and refusal of an FID with no keys), post the result once.
  • Blind verify (verify-impl), round 1: FAIL. A browser recount flagged a fresh amendment as not recountable. Cause: the browser kept an older demo run's log for the same founder's charter, whose keys the new demo's second node no longer listed, so the label was correct. Fixed the demo (fresh founder's charter per run, clean-storage step) and the label wording (it now says "revoked since, or your second node does not list it" instead of claiming a revocation).
  • Blind verify round 2: PARTIAL. Checks 6 and 7 passed from clean storage (approve, pass, recount checks out). The open tally and the unrecountable label were not captured because the browser tool detached. I then saw both in Chrome myself, not blind: the tally "2 of 5 members approved · passes at 3", a plain feed Like passing the vote, and the new label wording.
  • The fake node's reactionById ignored the reaction type (the client correctly rejected its answer as "not matching the request"); fixed in the test world.

Not done

  • Live mainnet test (PRD 9.2): needs 2 new test FIDs (about $0.20 each, onchain steps run by David) and David's own account liking from the Farcaster app.
  • P1/P2 items in the PRD: editing other charter fields, hosted latest log, custom thresholds, founder-led mode.

Pre-existing, not changed here

  • verify.ts (comment near checkKey) and secondnode.ts still say Snapchain revokes a removed key's messages. Since engine V14 it does not, so feeds may drop genuine old casts of accounts that rotated keys. Proposed as a separate PR.
  • Reported by the blind verifier, not re-checked yet: verifyMessage accepts some encodings that do not change signed content (a hex prefix other than 0x, renamed null fields, non-canonical base64 padding). For votes this fails safe: a log with such a field is shown as a fork, never merged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ut7NR5KyubX7WMMggTFNEb

A proposal is a member's cast naming the charter's current head and the
changes (+/- member, +/- moderator, as signed mentions). An approval is a
member's like, from any app; once seen it counts for good. It passes when
more than half of that version's members approve; leaving needs only your
own. The charter becomes an append-only log of the founder's charter plus
each passed proposal with its signed likes, so anyone can replay it from
their own node. No deadlines: author timestamps can be backdated.

- core: votes.ts (format, parser, replay, tally, advance, compareLogs,
  propose, announce); Verifier.voteKey checks key ownership and gasless
  key scopes; likesByCast and repliesTo; buildCast takes mentions and
  sends casts over 320 bytes as LONG_CAST (fixes 321-1024 byte casts)
- CLI: charter proposals | propose | announce | import; verify replays logs
- web: Votes section in the Society card
- docs: README "Society votes", TRUST.md "What a society vote proves"
- tests: 20 new (90 total), fake node world and a local demo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ut7NR5KyubX7WMMggTFNEb
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Spider333 and others added 3 commits October 23, 2026 16:53
census, feed, join, proposals, propose and announce accepted any log
that replayed, including one whose majority rests on approvals with keys
no node lists, as a hand-made log with made-up keys would have. A forged
log could then change the member list (and join would follow it). They
now refuse such a version unless --trust-unrecountable is given, as
import already did, and charter verify exits 1 for it. The web client
already asks before using such a log.

Found by the automated security review of the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ut7NR5KyubX7WMMggTFNEb
The site lists votes under what is different, explains them in the
societies step with the three commands, and adds a known gap: votes have
not run on mainnet yet. AGENTS.md gains the property that a charter
changes only by a majority of its members' likes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ut7NR5KyubX7WMMggTFNEb
# Conflicts:
#	README.md
#	src/core/index.ts
@Spider333
Spider333 marked this pull request as ready for review October 24, 2026 18:10
@Spider333
Spider333 merged commit 083c08a into main Oct 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant