// The basket of a vote (Yvv, 2026-10-30: « Je peux voter tout d'un coup, ou une // section entière, ou un article avec son titre, ou séparément »). One chooses // article by article; « tout pour » or « tout contre » fills a section or the // whole document — but never an article whose versions compete: there, one // chooses one's version. Then one gesture sends it all: one write on the // server, or one transaction on the Ğ1 chain. import type { Choice, Proposal, Unit } from './types'; /** A choice in the basket: for, against, or withdrawing one's vote. Absent: nothing. */ export type Picks = Record; /** An article as the basket shows it. */ export interface Slot { unit: Unit; /** The versions in competition: the text in force, the open texts and removals. */ versions: Proposal[]; /** Open changes of its inertia: another question, never filled by a grouped gesture. */ inertia: Proposal[]; /** Versions held back by an objection: shown, not votable until it is lifted (ADR 0010). */ held: Proposal[]; } export interface Section { /** The section of the seed (introduction, fondamental…), or null. */ key: string | null; slots: Slot[]; } /** The articles of the document with something to vote, by section, in document order. */ export function sections(units: Unit[]): Section[] { const out: Section[] = []; for (const unit of units) { const open = unit.proposals.filter((p) => p.status === 'open'); const versions = [ ...(unit.inForce?.status === 'in_force' ? [unit.inForce] : []), ...open.filter((p) => p.kind !== 'inertia' && p.votable !== false), ]; const inertia = open.filter((p) => p.kind === 'inertia'); const held = open.filter((p) => p.kind !== 'inertia' && p.votable === false); if (!versions.length && !inertia.length && !held.length) continue; const last = out.at(-1); const slot = { unit, versions, inertia, held }; if (last && last.key === unit.section) last.slots.push(slot); else out.push({ key: unit.section, slots: [slot] }); } return out; } /** « Tout pour » / « tout contre » over some articles: only those with a single version. */ export function fill(slots: Slot[], choice: 'for' | 'against', picks: Picks): Picks { const next = { ...picks }; for (const s of slots) { const [only] = s.versions; if (only && s.versions.length === 1) next[only.id] = choice; } return next; } /** Articles of several versions where no version is chosen yet: the interface shows them. */ export function waiting(slots: Slot[], picks: Picks): Slot[] { return slots.filter((s) => s.versions.length > 1 && !s.versions.some((p) => p.id in picks)); } /** The versions one may vote now: those a choice of the basket is sent for. */ export function votableIds(all: Section[]): Set { return new Set(all.flatMap((s) => s.slots.flatMap((x) => [...x.versions, ...x.inertia].map((p) => p.id)))); } /** Empties the basket of these articles only. */ export function clearScope(slots: Slot[], picks: Picks): Picks { const ids = new Set(slots.flatMap((s) => [...s.versions, ...s.inertia, ...s.held].map((p) => p.id))); return Object.fromEntries(Object.entries(picks).filter(([id]) => !ids.has(id))); } /** What is sent: one line per version still in the document; withdrawing is a null choice. */ export function lines(picks: Picks, known: Set): { proposalId: string; choice: 'for' | 'against' | null }[] { return Object.entries(picks) .filter(([id]) => known.has(id)) .map(([proposalId, c]) => ({ proposalId, choice: c === 'none' ? null : c })); } export function counts(picks: Picks): { for: number; against: number; none: number; total: number } { const all = Object.values(picks); return { for: all.filter((c) => c === 'for').length, against: all.filter((c) => c === 'against').length, none: all.filter((c) => c === 'none').length, total: all.length, }; }