14 KiB
ADR 005 — Git-native, anonymous voting system
Date: 2026-05-11 Status: Proposed
Context
As the agency scoring system matures, the community will need to make two kinds of binding decisions:
Governance proposals — changes to the scoring system itself: adding or removing data sources, adjusting platform weights, changing eligibility criteria. These define what "participation" means and directly affect contributor scores.
General community proposals — decisions about project direction, technical choices, resource allocation, or any other matter the community wishes to put to a vote. These are not specific to the scoring system; the voting infrastructure is general-purpose.
Both types require a process that is legitimate, tamper-resistant, and accessible to any community that forks this system. The mechanism is the same for both; only the vote weighting differs — see Proposal types and vote weighting.
Several constraints shape the design:
- Fork-friendly — any community adopting this system should get the voting infrastructure for free, without depending on OSArch's servers, keys, or accounts
- Public data only — consistent with ADR 003; no dependency on private APIs or authenticated services
- No central authority — no single person or role should be able to unilaterally determine an outcome
- Anonymous ballots — in small, tight-knit communities, public voting creates social pressure that distorts honest signal; contributors should be able to vote their genuine view without fear of social consequence
- Verifiable — anyone should be able to independently verify that votes are legitimate and the tally is correct
Decision
Votes are anonymous, cryptographically verifiable, and stored entirely in git.
The mechanism is a linkable ring signature — the same cryptographic primitive used by Monero for private transactions. Combined with a community norm for vote submission, this achieves full ballot anonymity without requiring any external infrastructure.
Why other approaches were rejected
Public voting (PR reactions, forum polls with visible results) The most obvious approach — and the most problematic. In a small community where contributors know each other and score rankings are visible, public voting creates implicit coercion. People vote with influential contributors, avoid opposing popular proposals, and self-censor minority views. Secret ballots exist in political systems for exactly this reason. The dynamics are no different here.
Platform polls (hidden-results forum polls) Some forum platforms support polls with results hidden until close, which addresses the social pressure problem during voting. But this is not fork-friendly — communities use different forum platforms or none at all, and features vary. The vote record lives outside git, making it harder to audit and impossible to reproduce independently. Rejected as a primary mechanism; acceptable as an informal discussion tool during the proposal phase.
Commit-reveal An improvement over public voting — voters submit a hash of their vote during the voting period, then reveal after it closes. Prevents bandwagon effects and hides the running tally. But the reveal phase is still public: observers know Alice voted yes and Bob voted no. In a small electorate this is meaningful information. Ring signatures provide stronger guarantees at acceptable complexity.
On-chain voting (smart contracts, DAO tooling) Maximally tamper-resistant but introduces dependencies (gas costs, wallet management, specific blockchain infrastructure) that make the system inaccessible to most communities and impossible to self-host simply. Reserved as a future direction if the community grows to a scale where on-chain guarantees are worth the overhead.
How it works
Cryptographic foundation
Each eligible voter holds a private key. When casting a vote, they produce a linkable ring signature over the set of all eligible voters' public keys. This signature:
- Proves the vote came from someone in the eligible set (verifiable by anyone)
- Reveals nothing about which voter cast it (unlinkable)
- Produces a key image — a deterministic value derived from the voter's private key that is the same for any two votes from the same key, enabling double-vote detection without identity disclosure
Vote file format
{
"proposal": "add-new-platform",
"proposal_hash": "8f3a...",
"choice": "yes",
"ring_signature": "...",
"key_image": "8f3a..."
}
No username. No identity. The proposal hash locks the vote to a specific version of the proposal — votes cast against an amended proposal are distinct from earlier votes.
Vote lifecycle
1. Proposal phase
A contributor opens a PR adding a file to docs/sites/proposed/{proposal-name}/proposal.md.
A community discussion period opens (on whatever channel the community uses). A minimum
discussion period (recommended: 14 days) must elapse before voting opens.
2. Vote opens — eligible voter snapshot
When voting begins, the current eligible voter list (public keys of all contributors
meeting the eligibility criteria) is committed to:
docs/sites/proposed/{proposal-name}/eligible-voters.json
This snapshot is locked. Late score changes do not affect eligibility for this vote.
3. Commit phase — vote creation Each eligible voter runs locally:
python src/voting/vote.py create --proposal add-new-platform --choice yes
→ Vote file saved to: ~/.agency/votes/add-new-platform.vote
The vote file never touches the repo directly from the voter.
4. Submission — community norm Voters share their vote files with other community members for submission. Three equivalent approaches:
- Peer submission — send your vote file to a trusted community member; they commit it
- Drop channel — post your vote file to a designated public channel (chat room, forum thread); anyone collects and commits
- Returning officer — a rotating, temporary role; one community member per vote collects all files from the drop channel and commits them in batches
The returning officer cannot fabricate votes (ring signatures would fail verification) but could suppress them. This is mitigated by the drop channel being public — anyone can count files posted versus files committed. Any discrepancy is visible and challengeable. The role rotates; no permanent privileged position.
5. Tally Anyone runs:
python src/voting/vote.py tally --proposal add-new-platform
→ 12 valid votes: 9 yes, 3 no
→ Quorum: 12/20 eligible (60%) ✓
→ Result: PASSED
Deterministic. Anyone gets the same result. No trusted tallier required.
6. Result committed
The tally result is committed to docs/sites/proposed/{proposal-name}/result.md.
The proposal file moves to docs/sites/active/ or docs/sites/retired/ accordingly.
Git record for a complete vote
commit "vote: open add-new-platform — eligible voter snapshot"
commit "vote: add-new-platform batch 1 — 6 votes (returning officer: contributor-a)"
commit "vote: add-new-platform batch 2 — 6 votes (returning officer: contributor-a)"
commit "vote: add-new-platform PASSED 9/12 (75%) — result committed"
commit "docs: move add-new-platform from proposed/ to active/"
Proposal types and vote weighting
Not all proposals are equal in stakes or scope. Two distinct vote modes are defined:
Governance proposals — changes to the scoring system itself: adding or removing data sources, adjusting platform weights, changing eligibility criteria, modifying the scoring formula. For these, votes are equal weight above the eligibility threshold. High scorers should not have disproportionate power over the rules that produced their score. Equal voting above a participation gate prevents the system from being captured by whoever currently benefits from it most.
General community proposals — anything outside the scoring system: project direction, technical decisions, resource allocation, community initiatives. For these, votes are weighted by agency score. The score is the whole point of this system — a signal of sustained participation. Weighting general votes by score is precisely what the agency signal is designed to enable.
This distinction is the reason the system exists. It produces a participation signal (score-weighted decisions) while protecting the integrity of that signal (equal-weighted governance). Conflating the two modes would allow either capture (high scorers define their own rules) or under-representation (everyone gets equal say on decisions where experience and contribution depth genuinely matter).
The vote file format, ring signature mechanism, and git lifecycle are identical for both types. The difference is in how the tally script aggregates results: equal count vs. score-weighted sum.
Eligibility
A single score threshold is insufficient — it addresses low-effort sybils but not a determined actor maintaining multiple high-scoring identities. Eligibility requires all three:
| Criterion | Purpose |
|---|---|
| Score above threshold (TBD) | Excludes low-commitment accounts |
| Activity spanning at least N platforms | Makes multi-identity maintenance costly — platform accounts verified via ADR 006 |
| Activity spanning at least M months | Prevents last-minute score farming |
The specific values for threshold, N, and M are community decisions to be made before
the first real vote. They should be committed to config.yaml and subject to the same
governance process as weight changes.
Different proposal types warrant different thresholds — adding a data source is lower stakes than removing one; changing the scoring formula is the highest-stakes change. A tiered threshold system is recommended but left for the community to define.
Tooling
All scripts live in src/voting/:
| Script | Purpose |
|---|---|
vote.py keygen |
Generate a voting key pair |
vote.py create |
Create a ring-signed vote file |
vote.py verify |
Verify a single vote file |
vote.py tally |
Tally all votes for a proposal |
snapshot.py |
Generate eligible voter snapshot at vote open |
All inputs and outputs are files. All outputs are committed to git. No server required.
Trust surface
| Trust assumption | Mitigation |
|---|---|
| Eligible voter snapshot is accurate | Public, auditable before voting opens |
| Returning officer submits all received votes | Drop channel is public; posted vs committed count is visible |
| Score calculation is honest | Anyone can re-run the script independently |
| Ring signature library is correct | Use audited, well-established implementation |
Bootstrap problem
Before the system has live scores, no contributor meets the eligibility criteria. The first votes — including votes on the eligibility criteria themselves — require a founding contributor set. This set must be defined explicitly at genesis and committed to the repo as a named, dated file. It expires once the system produces live scores.
Rather than a politically-chosen insider list, the recommended approach is to derive the founding set from existing, publicly verifiable community signals:
Primary signal — forum gamification score: Contributors above a defined threshold on the community forum (e.g. 30+ points in the platform's built-in points system) are included. This is publicly verifiable by anyone, requires no insider decision, and reflects accumulated standing in the community's primary discussion venue.
Supplementary signal — platform activity fallback: Contributors who are active on other tracked platforms (e.g. above a minimum GitHub contribution count) but have low or no forum presence are also included. This prevents the founding set from excluding developers whose primary contribution is code rather than discussion.
Calibration note: The specific threshold (e.g. 30 points) should be chosen by inspecting the actual distribution of scores in the community before it is committed. A threshold that includes 200 people and one that includes 20 produce very different founding electorates. The goal is "meaningfully active member," not "most active member."
Important caveat: Using the forum's built-in gamification score at bootstrap is a pragmatic one-time choice, not an endorsement of that system as a long-term signal. Vanilla's (or any forum platform's) gamification formula is opaque and outside community control — see community-osarch.md for why it is excluded from ongoing score calculation. At genesis, the formula doesn't need to be perfect; it needs to be a reasonable, verifiable proxy for community standing. Once the agency system produces live scores, the bootstrap set expires and the formal eligibility criteria take over.
The founding set, the threshold used, and the date it was generated must all be committed to the repo with a clear note that this represents a bootstrap, not permanent special status. Any contributor who qualifies under the live system but missed the bootstrap snapshot gains full eligibility automatically once live scores are available.
Limitations
- Small electorate inference — with few eligible voters, even anonymous ballots leak information through elimination. No cryptographic scheme fully solves this for small groups.
- Sybil resistance — ring signatures prevent the same key from voting twice but cannot prevent a person with multiple legitimately-registered keys from voting multiple times. See ADR 006 for the identity layer that addresses this.
- Anonymity of submission, not of participation — observers know you submitted a vote file (or asked someone to), even if they can't read the contents. Full participation anonymity is not achievable.
Consequences
- The voting system ships with the repo — every fork gets it for free
- No external service required for any part of the process except the platform APIs already used for scoring
- The trust surface is small, named, and publicly auditable
- The system can be adopted incrementally — human attestation and git-tracked proposals work independently before ring signature tooling is built