// The versions of an article, for the list of open proposals (Yvv, 2026-10-30: // « Quand un article a plusieurs versions, faut pas ajouter un article dans la // liste »): one line per article, its best version first, the others inside. import type { Proposal, Unit } from './types'; export type Filter = 'all' | 'wellrated' | 'mine'; export interface Article { unit: Unit; /** The open proposals of the article, best first. */ versions: Proposal[]; /** The best version is strictly ahead of the next one (not a tie). */ leads: boolean; } /** How far a version has risen toward its threshold, from 0 to 1: its water level. */ export function level(p: Proposal): number { const required = p.evaluation?.required ?? 0; return required > 0 ? Math.min(1, p.tally.for / required) : 0; } /** * Negative when `a` comes before `b`: the highest water, then more for, then fewer * against. A version held back by an objection comes after the others: it is not * votable, and cannot enter into force, until the objection is lifted. */ export function compare(a: Proposal, b: Proposal): number { return ( Number(a.blocked) - Number(b.blocked) || level(b) - level(a) || b.tally.for - a.tally.for || a.tally.against - b.tally.against ); } /** * Plain excerpts of sibling versions, each starting a few words before the versions * part ways — two variants often share a long opening, and a one-line excerpt would * show only that. */ export function excerpts(texts: string[]): string[] { const LEAD = 3; const words = texts.map((t) => t.split(/\s+/).filter(Boolean)); const [first] = words; if (!first || words.length < 2) return texts; let common = 0; while (common < first.length && words.every((w) => w[common] === first[common])) common++; const start = common - LEAD; // A short common start reads well as it is. if (start < 4) return texts; return words.map((w) => `… ${w.slice(start).join(' ')}`); } export function passes(p: Proposal, filter: Filter): boolean { if (filter === 'wellrated') return !!p.evaluation?.wellRated; if (filter === 'mine') return p.mine; return true; } /** * One article per unit, in the order of the document; an article is kept when at * least one of its versions passes the filter, and then shows all of them. */ export function articles(items: { unit: Unit; proposal: Proposal }[], filter: Filter = 'all'): Article[] { const byUnit = new Map(); for (const { unit, proposal } of items) { const entry = byUnit.get(unit.id) ?? { unit, versions: [] }; entry.versions.push(proposal); byUnit.set(unit.id, entry); } return [...byUnit.values()] .filter(({ versions }) => versions.some((p) => passes(p, filter))) .map(({ unit, versions }) => { // Array.prototype.sort is stable: equal versions keep the order of the document. const ranked = [...versions].sort(compare); const [best, next] = ranked; return { unit, versions: ranked, leads: !!best && !!next && compare(best, next) < 0 }; }); }