Agency/docs/decisions/005-git-native-voting-system.md
Ryan Schultz 1b72218025 docs: ADR 005 + 006 — git-native voting and identity attestation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 20:22:10 -05:00

313 lines
14 KiB
Markdown

# 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](#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](003-public-data-only.md); 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
```json
{
"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](006-cross-platform-identity-attestation.md) |
| 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](../sites/active/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](006-cross-platform-identity-attestation.md) 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](006-cross-platform-identity-attestation.md)
and git-tracked proposals work independently before ring signature tooling is built