Skip to content

Governance and Decisions

Jaloliddin Ismailov edited this page Oct 4, 2026 · 3 revisions

The Hub does not treat governance as a generic polls feature. It models the constituency's actual decision pipeline — the one the S-series policies describe — from the moment a proposal is drafted to the moment the outcome is recorded, including the failure modes: insufficient support, too few votes, a confirmed veto, a withdrawal. This page walks that pipeline, then covers the two related machines beside it: selections and elections.

The decision lifecycle

A decision begins life as a draft held by its proposing body. When its authors are ready, it is presented — the transition that turns a private draft into a public process with a consultation window. Members comment and raise flags (formal objections that demand a response); the proposal can be revised; then it moves to the decision stage, where it is either adopted by consensus or sent to a vote.

stateDiagram-v2
    [*] --> Draft
    Draft --> Consultation: present
    Consultation --> Decision: consultation resolved
    Decision --> Adopted: consensus recorded
    Decision --> Voting: vote opened
    Voting --> Adopted: support + quorum met
    Voting --> Rejected: insufficient support
    Voting --> FailedQuorum: too few eligible votes
    Draft --> Withdrawn: withdraw
    Consultation --> Withdrawn: withdraw
    Adopted --> [*]
    Rejected --> [*]
    FailedQuorum --> [*]
    Withdrawn --> [*]
Loading

Every transition is an explicit endpoint call, never a timestamp the system infers. POST /api/decisions/:id/present, /close, /withdraw, plus /comments, /flags, /flags/:flagId/respond, /flags/:flagId/withdraw, /ballots, /vetoes, /vetoes/:vetoId/confirm. The full trail lands in the decision's event log — /api/decisions/:id/events — so the history of how a decision was reached is itself a first-class record, not a by-product.

A few properties of the pipeline that reviewers should notice:

  • The pre-check is a fast path, not the check. Ballot submission does an early eligibility test, then re-validates inside the transaction — concurrent submissions can both pass the pre-check, and only the transactional check decides. (This is the same lock-then-revalidate discipline as the authority write path.)
  • Vetoes are two-phase. A veto is raised, then confirmed by a second party before it takes effect — a guard against a single impulsive objection.
  • Operational state is not political status. A decision marked "adopted" on a demo deployment means the pipeline ran, not that YOUNGO endorsed it. The system records process; endorsement is a human act the record describes.

The Council electorate

Deciding who may vote is the hardest part of modelling a real constituency. The Council's electorate is not "all members" — it is the set of Council seats, and seats belong to scopes (a working group, a body, an organisation) rather than people. councilElectorate in src/lib/decisionRuntime.ts resolves the electorate as distinct seats that currently have an eligible holder:

// Simplified: a seat counts once if any of its holders is eligible
const seats = new Map<string, Account[]>()
for (const row of seatHolders) {
  const key = row.council_seat
  if (!key) continue
  const holders = seats.get(key) ?? []
  if (isCwActive(row.account)) holders.push(row.account)
  seats.set(key, holders)
}
const electorate = [...seats.values()].filter(h => h.length > 0).length

The subtleties are deliberate: a substitute covering a seat does not add a second vote to the electorate (the seat votes once, whoever holds it), a holder with lapsed Constituency Work membership does not qualify the seat, and there is no row cap that could silently truncate a large Council.

Elections

Elections are deliberately separate from decision ballots. An ordinary decision ballot is account-linked — the audit trail needs to know who voted. An election ballot must be secret, so the machinery is different: eligible members request a voting credential, a randomised token that authorises exactly one ballot, and the vote itself is cast against the credential rather than the account.

sequenceDiagram
    actor Voter
    participant Hub
    participant DB

    Voter->>Hub: GET /governance/elections/:id/credential
    Hub->>Hub: check eligibility + not yet issued
    Hub->>DB: store credential hash (not identity)
    Hub-->>Voter: one-time credential
    Voter->>Hub: POST /governance/elections/:id/vote
    Note right of Voter: ballot carries credential,<br/>not account id
    Hub->>DB: consume credential, store ballot
Loading

The lifecycle runs through /api/governance/elections/:id/advance: creation → nominations → candidate screening → voting → tally. Screening (/candidates/:cid/screen) is the human gate where eligibility is confirmed before a name reaches the ballot.

Worth being honest about: this is a credential-based ballot that preserves secrecy within the system, but it still relies on trust in the Hub server — it is not an independently verifiable cryptographic election. The code and this documentation say so explicitly rather than implying more.

Selections

Selections cover the appointed side of governance — the calls where a committee chooses people for roles rather than holding an election. The lifecycle mirrors what the S24 policy describes: a selection opens (/governance/selections/:id/open), applications arrive (/apply), the committee is named (/committee), members recuse themselves where conflicted (/recuse), applications are evaluated (/applications/:aid/evaluate), the committee decides (/decide), and the result is announced (/announce). Each step is its own endpoint so that recusals and committee composition are recorded as facts, not implied.

Consultations

Alongside formal decisions, /api/consultation/contributions provides the lighter-weight channel for gathering input — the mechanism COY programmes and working groups use when they want responses without a ballot. Contributions are typed (/consultation/kinds) so a call for agenda items does not look like a call for amendments.

Reading the event trail

For any decision, /api/decisions/:id/events returns the ordered record — presented, flagged, responded, revised, voted, closed. When someone disputes how an outcome was reached, that endpoint is the answer. It is also why transitions go through endpoints rather than direct row edits: a decision edited by hand has no story to tell.

Clone this wiki locally