Files
decision/web/src/lib/basket.ts
T
YvvandClaude Opus 5.5 83f8ca0981 v3 : interface des objections, des coquilles validées et de la messagerie ; contrôles C28–C31
Une version retenue par une objection le dit sous l'article, à chacun
selon son rôle : la personne qui vote voit qu'elle ne se vote plus (les
votes faits restent) ; l'auteur, quoi faire (« Modifier l'article visé »
pour une redite, « Reformuler » pour une contradiction) ; la personne qui
objecte, lever ou réitérer avant l'échéance. Le panier la montre sans
choix et ne l'envoie jamais.

« Détails » : faire une objection (redit / contredit), les objections et
où elles en sont, les coquilles proposées par d'autres avec leur diff
resserré, valider / refuser / revenir, l'historique dit qui a validé.
Une coquille se propose sur toute version ouverte ou en vigueur.

Boîte « Messages » dans le menu, avec le nombre de non-lus (pastille sur
le bouton du menu en mobile) : objet, texte, gestes, échéance ; un message
traité n'a plus de geste. Répondre à une objection lit son message.

Trilingue (fr, en, es). C6 suit la règle : la coquille d'un administrateur
sur un texte du seed passe tout de suite ; C28–C31 couvrent le reste.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:21:20 +01:00

92 lines
3.9 KiB
TypeScript

// 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<string, Choice>;
/** 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<string> {
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<string>): { 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,
};
}