gestion_table_tournante_libre/src/application/commandes.js

540 lines
25 KiB
JavaScript
Raw Normal View History

[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Les commandes nommées (§ 8.2) : toute modification d'un événement passe par
// l'une d'elles. Une commande est une fonction pure (charge, arguments) →
// { charge, libelle, avertissements } : la charge rendue est neuve, celle
// reçue ne change jamais ; le libellé est le texte français que le journal
// fige (libelles.js) ; les avertissements, { code, details }, disent ce que le
// geste emporte de plus que son effet, sans rien empêcher. Un refus lève
// ErreurCommande(code, details), où details.remede nomme le geste qui le lève,
// null quand il n'y en a pas ; une faute du code — un argument de forme
// fausse, un état ou un champ inconnus — lève TypeError. Aucune commande
// n'écrit, ne lit l'horloge ni ne tire un aléa.
//
// La saisie entre en NFC, ses blancs de bord retirés, une chaîne vide valant
// null (§ 10.2) ; la règle d'un champ est celle du schéma du fichier d'état,
// que document.js tient seul. Les identifiants viennent de prochainsIds, qui
// ne recule jamais : un identifiant retiré ne revient pas (§ 4, § 5.7). Un
// plan bloqué refuse toute commande, débloquer excepté (§ 9).
//
// Les états du plan (§ 9) : brouillon, proposé et retenu se posent par
// changerEtat sans rien détruire, proposé quand une proposition existe,
// retenu quand un placement est retenu ; bloqué se pose de chacun d'eux, et
// seul debloquer le lève, vers l'état que le contenu impose (etatDeduit). Une
// génération fait passer un brouillon à proposé ; retenir pose retenu ;
// effacer les propositions ramène un plan proposé sans retenu à brouillon.
import { ErreurCsv } from '../csv/erreurs.js';
import { appliquerImport, bilanRemplacement } from '../csv/import.js';
import { serialiserCharge } from '../stockage/canonique.js';
import { SCHEMA, clesRangees, creerCharge, etatDeduit, premiereFaute } from '../stockage/document.js';
import { ErreurStockage } from '../stockage/erreurs.js';
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
import { fautes, formeNommee, versFichier } from '../stockage/placements.js';
import { COMMANDES_MOBILIER } from './commandes_mobilier.js';
import { COMMANDES_PLACES } from './commandes_places.js';
import { ErreurCommande, refus } from './erreurs.js';
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
import { libelle } from './libelles.js';
import { nomAffiche } from './personnes.js';
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
// Le refus d'une commande vit dans erreurs.js, le nom affiché d'une personne
// dans personnes.js : les importateurs de ce module les y trouvent encore.
export { ErreurCommande, nomAffiche };
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
// Un avertissement : il n'empêche rien.
const avertissement = (code, details) => ({ code, details });
// Règles du schéma : celle d'un champ d'objet, puis celles de l'événement et
// d'un participant.
const regleDuChamp = (regle, cle) => regle.champs.find(([nom]) => nom === cle)[1];
const CHARGE = regleDuChamp(SCHEMA, 'charge');
const EVENEMENT = regleDuChamp(CHARGE, 'evenement');
const PARTICIPANT = regleDuChamp(CHARGE, 'participants').element;
// Le plus grand identifiant qu'admet la règle : le compteur, qui la suit,
// n'aurait plus de valeur au-delà de lui.
const IDENTIFIANT_MAX = regleDuChamp(PARTICIPANT, 'id').max;
const ETATS = ['brouillon', 'propose', 'retenu', 'bloque'];
// Les champs de texte d'un participant que la saisie pose, dans l'ordre du
// fichier.
const CHAMPS_DE_SAISIE = ['nom', 'prenom', 'appartenance', 'courriel', 'titrePressenti', 'notes'];
// Une saisie de texte : en NFC, ses blancs de bord retirés, null quand il ne
// reste rien ; null et l'absence valent null.
function saisie(valeur, champ) {
if (valeur === undefined || valeur === null) return null;
if (typeof valeur !== 'string') {
throw new TypeError(`${champ} : chaîne ou null attendue, reçu ${JSON.stringify(valeur)}`);
}
const texte = valeur.normalize('NFC').trim();
return texte === '' ? null : texte;
}
// Les champs d'une saisie de participant : chaque champ de texte présent, lu
// par saisie, et exclu quand admisExclu, un booléen. Un champ inconnu, ou
// d'une autre sorte, est une faute du code.
function lireChamps(champs, { admisExclu }) {
if (champs === null || typeof champs !== 'object' || Array.isArray(champs)) {
throw new TypeError(`champs : objet attendu, reçu ${JSON.stringify(champs)}`);
}
const permis = admisExclu ? [...CHAMPS_DE_SAISIE, 'exclu'] : CHAMPS_DE_SAISIE;
const inconnu = clesRangees(champs).find((cle) => !permis.includes(cle));
if (inconnu !== undefined) throw new TypeError(`champs : ${inconnu} n'est pas un champ de cette saisie`);
const lus = {};
for (const champ of CHAMPS_DE_SAISIE) if (Object.hasOwn(champs, champ)) lus[champ] = saisie(champs[champ], champ);
if (Object.hasOwn(champs, 'exclu')) {
if (typeof champs.exclu !== 'boolean') {
throw new TypeError(`exclu : booléen attendu, reçu ${JSON.stringify(champs.exclu)}`);
}
lus.exclu = champs.exclu;
}
return lus;
}
// Un participant tel que la saisie le pose : nom requis (NOM_REQUIS) ; toute
// autre valeur hors de sa règle est une faute du code.
function exigerParticipant(participant) {
if (premiereFaute(participant.nom, regleDuChamp(PARTICIPANT, 'nom')) !== null) throw refus('NOM_REQUIS');
const faute = premiereFaute(participant, PARTICIPANT);
if (faute !== null) throw new TypeError(`participant : ${faute} hors de sa règle`);
}
// Le participant d'identifiant id, ou PARTICIPANT_INCONNU.
function participantDe(charge, id) {
if (!Number.isSafeInteger(id)) throw new TypeError(`id : entier attendu, reçu ${JSON.stringify(id)}`);
const participant = charge.participants.find((candidat) => candidat.id === id);
if (participant === undefined) throw refus('PARTICIPANT_INCONNU', { id });
return participant;
}
// Un plan bloqué refuse toute modification ; débloquer seul le lève (§ 9).
function exigerModifiable(charge) {
if (charge.evenement.etat === 'bloque') throw refus('PLAN_BLOQUE', {}, { geste: 'debloquer' });
}
// Titres de place pourvus (§ 4.1, § 4.4) : le compte que bilanRemplacement
// tient pour l'import, seule implémentation de la règle (§ 13.2).
const titresPourvus = (charge) => bilanRemplacement(charge).titresPourvus;
// La charge reçue, copiée, son participant id remplacé par ce que rend
// modifier de sa copie.
function avecParticipant(charge, id, modifier) {
const suivante = structuredClone(charge);
suivante.participants = suivante.participants.map((participant) =>
participant.id === id ? modifier(participant) : participant,
);
return suivante;
}
// La charge reçue, copiée, à l'état donné.
function avecEtat(charge, etat) {
const suivante = structuredClone(charge);
suivante.evenement.etat = etat;
return suivante;
}
/** ajouterParticipant {champs} : seul le nom est requis ; exclu vaut faux par défaut. */
function ajouterParticipant(charge, { champs }) {
exigerModifiable(charge);
const lus = lireChamps(champs, { admisExclu: true });
const id = charge.prochainsIds.participant;
const participant = {
id,
nom: lus.nom ?? null,
prenom: lus.prenom ?? null,
appartenance: lus.appartenance ?? null,
courriel: lus.courriel ?? null,
titrePressenti: lus.titrePressenti ?? null,
notes: lus.notes ?? null,
exclu: lus.exclu ?? false,
};
exigerParticipant(participant);
if (id >= IDENTIFIANT_MAX) throw refus('COMPTEUR_SATURE', { compteur: 'participant' });
const suivante = structuredClone(charge);
suivante.participants.push(participant);
suivante.prochainsIds.participant = id + 1;
return {
charge: suivante,
libelle: libelle('ENTREE_AJOUTER_PARTICIPANT', { personne: nomAffiche(participant) }),
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
avertissements: [],
};
}
/**
* modifierParticipant {id, champs} : les champs de texte donnés ; l'exclusion
* a ses deux commandes. Le libellé nomme la personne d'avant et chaque champ
* changé, puis le nouveau nom quand il change.
*/
function modifierParticipant(charge, { id, champs }) {
exigerModifiable(charge);
const avant = participantDe(charge, id);
const apres = { ...avant, ...lireChamps(champs, { admisExclu: false }) };
exigerParticipant(apres);
const changes = CHAMPS_DE_SAISIE.filter((champ) => apres[champ] !== avant[champ]);
if (changes.length === 0) throw refus('SANS_EFFET', { commande: 'modifierParticipant' });
const renomme = apres.nom !== avant.nom || apres.prenom !== avant.prenom;
return {
charge: avecParticipant(charge, id, () => apres),
libelle: libelle('ENTREE_MODIFIER_PARTICIPANT', {
personne: nomAffiche(avant),
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
champs: changes,
devenu: renomme ? nomAffiche(apres) : null,
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
}),
avertissements: [],
};
}
// Les comptes d'une exclusion ou d'une réintégration (§ 4.4) : les
// réservations de la personne, que l'exclusion suspend sans les retirer, et
// les titres que le geste cesse de pourvoir, ou pourvoit de nouveau.
function bilanExclusion(avant, apres, participant) {
const reservations = avant.reservations.filter((reservation) => reservation.participant === participant.id).length;
const titres = Math.abs(titresPourvus(avant) - titresPourvus(apres));
return { participant: participant.id, personne: nomAffiche(participant), reservations, titres };
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
}
/**
* exclureParticipant {id} (§ 4.4) : ses réservations restent, suspendues ; ses
* places reviennent aux autres et ses titres ne sont plus pourvus, ce que
* RESERVATIONS_SUSPENDUES dit quand il en porte.
*/
function exclureParticipant(charge, { id }) {
exigerModifiable(charge);
const participant = participantDe(charge, id);
if (participant.exclu) throw refus('SANS_EFFET', { commande: 'exclureParticipant' });
const suivante = avecParticipant(charge, id, (copie) => ({ ...copie, exclu: true }));
const bilan = bilanExclusion(charge, suivante, participant);
return {
charge: suivante,
libelle: libelle('ENTREE_EXCLURE_PARTICIPANT', { personne: bilan.personne }),
avertissements: bilan.reservations > 0 ? [avertissement('RESERVATIONS_SUSPENDUES', bilan)] : [],
};
}
/** reintegrerParticipant {id} (§ 4.4) : ses réservations et ses titres reviennent, RESERVATIONS_RETABLIES. */
function reintegrerParticipant(charge, { id }) {
exigerModifiable(charge);
const participant = participantDe(charge, id);
if (!participant.exclu) throw refus('SANS_EFFET', { commande: 'reintegrerParticipant' });
const suivante = avecParticipant(charge, id, (copie) => ({ ...copie, exclu: false }));
const bilan = bilanExclusion(charge, suivante, participant);
return {
charge: suivante,
libelle: libelle('ENTREE_REINTEGRER_PARTICIPANT', { personne: bilan.personne }),
avertissements: bilan.reservations > 0 ? [avertissement('RESERVATIONS_RETABLIES', bilan)] : [],
};
}
/**
* supprimerParticipant {id} : la personne et ses réservations partent dans la
* même entrée ; les titres restent à leurs places, les propositions et le
* retenu restent, en dérive (§ 9) ; le compteur ne recule pas.
*/
function supprimerParticipant(charge, { id }) {
exigerModifiable(charge);
const participant = participantDe(charge, id);
const suivante = structuredClone(charge);
suivante.participants = suivante.participants.filter((candidat) => candidat.id !== id);
suivante.reservations = suivante.reservations.filter((reservation) => reservation.participant !== id);
const reservations = charge.reservations.length - suivante.reservations.length;
const details = {
participant: id,
personne: nomAffiche(participant),
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
reservations,
titres: titresPourvus(charge) - titresPourvus(suivante),
};
return {
charge: suivante,
libelle: libelle('ENTREE_SUPPRIMER_PARTICIPANT', { personne: details.personne }),
avertissements: reservations > 0 ? [avertissement('RESERVATIONS_RETIREES', details)] : [],
};
}
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
// Les avertissements de chaque personne qu'un import exclut ou réintègre
// (§ 4.4) : celle que la charge suivante porte sous le même identifiant,
// exclu changé, et qui tient une réservation. Chacune a le bilan que
// donnerait sa commande seule, exclureParticipant ou reintegrerParticipant,
// appliquée à la charge d'avant : RESERVATIONS_SUSPENDUES ou
// RESERVATIONS_RETABLIES, par identifiant croissant.
function avertissementsDExclusion(avant, suivante) {
const exclusDApres = new Map(suivante.participants.map(({ id, exclu }) => [id, exclu]));
return avant.participants
.filter(({ id, exclu }) => exclusDApres.has(id) && exclusDApres.get(id) !== exclu)
.sort((a, b) => a.id - b.id)
.map((participant) => {
const seule = avecParticipant(avant, participant.id, (copie) => ({ ...copie, exclu: !copie.exclu }));
const code = participant.exclu ? 'RESERVATIONS_RETABLIES' : 'RESERVATIONS_SUSPENDUES';
return avertissement(code, bilanExclusion(avant, seule, participant));
})
.filter(({ details }) => details.reservations > 0);
}
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
/**
* importerParticipants {apercu, mode} (§ 10.1) : l'aperçu appliqué selon le
* mode (appliquerImport), en une seule entrée. Le refus du CSV — un aperçu
* refusé ou ambigu — devient le refus de la commande, sous le même code. Le
* résumé suit le rendu, pour le rapport d'import et le CSV des refus. Selon
* l'état : sans avertissement en brouillon et proposé ; en retenu,
* l'avertissement de dérive IMPORT_EN_RETENU, quel que soit le mode, sur le
* signal resume.derive d'appliquerImport ; refusé en bloqué, PLAN_BLOQUE.
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
* Une personne que l'import exclut ou réintègre le dit ensuite comme sa
* commande le dirait (avertissementsDExclusion).
*
* Un import dont la charge ne change pas, mais qui refuse des lignes, n'est
* pas sans effet : ses refus sont à lire et à réexporter. Il rend la charge
* reçue, un libellé null — aucune entrée ne s'écrit —, aucun avertissement,
* et son résumé.
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
*/
function importerParticipants(charge, { apercu, mode }) {
exigerModifiable(charge);
let resultat;
try {
resultat = appliquerImport(charge, apercu, mode);
} catch (erreur) {
if (erreur instanceof ErreurCsv) throw new ErreurCommande(erreur.code, { ...erreur.details, remede: null });
throw erreur;
}
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
const { ajoutes, misAJour, retires, derive, refusees } = resultat.resume;
if (refusees.length > 0 && serialiserCharge(resultat.charge) === serialiserCharge(charge)) {
return { charge, libelle: null, avertissements: [], resume: resultat.resume };
}
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
return {
charge: resultat.charge,
libelle: libelle('ENTREE_IMPORTER_PARTICIPANTS', { mode, ajoutes, misAJour, retires }),
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
avertissements: [
...(derive ? [avertissement('IMPORT_EN_RETENU', {})] : []),
...avertissementsDExclusion(charge, resultat.charge),
],
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
resume: resultat.resume,
};
}
/** changerEtat {etat} (§ 9) : un état du plan, sans rien détruire ; bloque se pose de chacun. */
function changerEtat(charge, { etat }) {
if (!ETATS.includes(etat)) throw new TypeError(`etat : ${ETATS.join(', ')} attendu, reçu ${JSON.stringify(etat)}`);
exigerModifiable(charge);
if (etat === charge.evenement.etat) throw refus('SANS_EFFET', { commande: 'changerEtat' });
if (etat === 'propose' && charge.propositions.length === 0) {
throw refus('AUCUNE_PROPOSITION', {}, { geste: 'ouvrirSection', section: 'generation' });
}
if (etat === 'retenu' && charge.retenu === null) {
throw refus('AUCUN_RETENU', {}, { geste: 'ouvrirSection', section: 'propositions' });
}
return { charge: avecEtat(charge, etat), libelle: libelle('ENTREE_CHANGER_ETAT', { etat }), avertissements: [] };
}
/** debloquer {} (§ 8.5, § 9) : l'état revient à celui que le contenu impose. */
function debloquer(charge) {
if (charge.evenement.etat !== 'bloque') throw refus('PLAN_NON_BLOQUE');
const etat = etatDeduit(charge);
return { charge: avecEtat(charge, etat), libelle: libelle('ENTREE_DEBLOQUER', { etat }), avertissements: [] };
}
/**
* enregistrerGeneration {propositions, configuration, produitVersion} (§ 5.7,
* § 8.9) : les propositions du moteur, mises en forme de fichier
* (versFichier) sous les identifiants qui suivent prochainsIds.proposition —
* la proposition d'identifiant k du moteur prend prochainsIds.proposition +
* k − 1 —, s'ajoutent aux précédentes, et le compteur passe au-delà de la
* dernière. Un brouillon passe à proposé.
*/
function enregistrerGeneration(charge, { propositions, configuration, produitVersion }) {
exigerModifiable(charge);
if (!Array.isArray(propositions)) throw new TypeError('propositions : liste attendue');
if (propositions.length === 0) throw refus('SANS_EFFET', { commande: 'enregistrerGeneration' });
const rangs = propositions.map(({ id }) => id);
if (!rangs.every((id) => Number.isSafeInteger(id) && id >= 1) || new Set(rangs).size !== rangs.length) {
throw new TypeError('propositions : identifiants du moteur entiers, ≥ 1 et distincts attendus');
}
const decalage = charge.prochainsIds.proposition - 1;
const options = { produitVersion, attribuerSieges: charge.reglages.attribuerSieges, decalage };
let fichiers;
try {
fichiers = propositions.map((proposition) => versFichier(proposition, configuration, options));
} catch (erreur) {
if (erreur instanceof ErreurStockage && erreur.code === 'COMPTEUR_SATURE') {
throw refus('COMPTEUR_SATURE', erreur.details);
}
throw erreur;
}
const ids = fichiers.map(({ id }) => id);
const premier = Math.min(...ids);
const dernier = Math.max(...ids);
const suivante = structuredClone(charge);
suivante.propositions.push(...fichiers);
suivante.prochainsIds.proposition = dernier + 1;
if (suivante.evenement.etat === 'brouillon') suivante.evenement.etat = 'propose';
return {
charge: suivante,
libelle: libelle('ENTREE_ENREGISTRER_GENERATION', { n: fichiers.length, premier, dernier }),
avertissements: [],
};
}
/**
* retenirProposition {id} (§ 5.7, § 9) : le placement de la proposition,
* copié, devient le retenu, que les retouches modifieront sans toucher la
* proposition ; le plan passe à retenu.
*/
function retenirProposition(charge, { id }) {
exigerModifiable(charge);
const proposition = charge.propositions.find((candidate) => candidate.id === id);
if (proposition === undefined) throw refus('PROPOSITION_INCONNUE', { id });
const { siegesAttribues, tables, capacites, tours, participants, placement } = structuredClone(proposition);
const suivante = avecEtat(charge, 'retenu');
suivante.retenu = { proposition: id, siegesAttribues, tables, capacites, tours, participants, placement };
return { charge: suivante, libelle: libelle('ENTREE_RETENIR_PROPOSITION', { id }), avertissements: [] };
}
/**
* effacerPropositions {} (§ 5.7) : la liste se vide, la proposition d'où vient
* le retenu exceptée ; le compteur ne recule pas. Un plan proposé sans retenu
* revient à brouillon.
*/
function effacerPropositions(charge) {
exigerModifiable(charge);
const origine = charge.retenu?.proposition ?? null;
const gardees = charge.propositions.filter(({ id }) => id === origine);
const n = charge.propositions.length - gardees.length;
if (n === 0) throw refus('SANS_EFFET', { commande: 'effacerPropositions' });
const suivante = structuredClone(charge);
suivante.propositions = suivante.propositions.filter(({ id }) => id === origine);
if (suivante.retenu === null && suivante.evenement.etat === 'propose') suivante.evenement.etat = 'brouillon';
return {
charge: suivante,
libelle: libelle('ENTREE_EFFACER_PROPOSITIONS', { n, gardee: gardees.length > 0 ? origine : null }),
avertissements: [],
};
}
/**
* Un registre réuni de plusieurs : chaque nom une fois, dans l'ordre des
* registres reçus, puis de chacun.
*
* @param {...Map<string, Function>} registres
* @returns {Map<string, Function>}
* @throws {RangeError} un nom porté par deux registres, qu'il nomme
*/
export function reunirCommandes(...registres) {
const reunies = new Map();
for (const registre of registres) {
for (const [nom, commande] of registre) {
if (reunies.has(nom)) throw new RangeError(`commandes : ${JSON.stringify(nom)} est dans deux registres`);
reunies.set(nom, commande);
}
}
return reunies;
}
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
/**
* Le registre des commandes, par nom : chacune (charge, arguments) →
* { charge, libelle, avertissements }, et importerParticipants y ajoute le
* résumé de l'import. Il réunit les commandes de ce module, celles du
* mobilier et celles des places.
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
*
* @typedef {{code: string, details: Object}} Avertissement
* @typedef {{charge: Object, libelle: string, avertissements: Avertissement[]}} Rendu
* @type {Map<string, (charge: Object, args: Object) => Rendu>}
*/
export const COMMANDES = reunirCommandes(
new Map([
['ajouterParticipant', ajouterParticipant],
['modifierParticipant', modifierParticipant],
['exclureParticipant', exclureParticipant],
['reintegrerParticipant', reintegrerParticipant],
['supprimerParticipant', supprimerParticipant],
['importerParticipants', importerParticipants],
['changerEtat', changerEtat],
['debloquer', debloquer],
['enregistrerGeneration', enregistrerGeneration],
['retenirProposition', retenirProposition],
['effacerPropositions', effacerPropositions],
]),
COMMANDES_MOBILIER,
COMMANDES_PLACES,
);
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
/**
* Applique la commande nom à la charge : ce qu'elle rend, avertissements []
* quand elle n'en donne pas. Un geste dont la charge rendue s'écrit comme la
* charge reçue ne change rien, et n'est pas une entrée (§ 8.2) : SANS_EFFET.
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
* Un rendu au libellé null ne change rien non plus, mais porte ce qu'il y a
* à lire — l'import dont seules des lignes refusées auraient changé la
* liste : il passe tel quel, et rien ne s'écrit.
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
*
* @param {string} nom
* @param {Object} charge
* @param {Object} [args]
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
* @returns {{charge: Object, libelle: string|null, avertissements: Array<{code: string, details: Object}>}}
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
* @throws {ErreurCommande} le refus de la commande, ou SANS_EFFET
* @throws {TypeError} une commande inconnue, un argument de forme fausse
*/
export function appliquerCommande(nom, charge, args = {}) {
const commande = COMMANDES.get(nom);
if (commande === undefined) throw new TypeError(`commande inconnue : ${JSON.stringify(nom)}`);
const rendu = commande(charge, args ?? {});
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
if (rendu.libelle === null) return { ...rendu, avertissements: rendu.avertissements ?? [] };
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
if (serialiserCharge(rendu.charge) === serialiserCharge(charge)) throw refus('SANS_EFFET', { commande: nom });
return { ...rendu, avertissements: rendu.avertissements ?? [] };
}
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
/**
* La forme nommée d'un placement de la charge (§ 8.9, point 4), pour la
* lecture et le signalement d'un défaut : une lecture, ni commande ni
* entrée. cible désigne { proposition: id } ou { retenu: true }. Rend
* { lignes, fautes } : fautes, celles que le placement porte (fautes) ;
* lignes, sa forme nommée (formeNommee), une ligne par identifiant écrit,
* ou null quand sa forme est fausse (faute FORME), qui ne se lit pas en
* lignes. Une proposition fautive n'est plus dans la charge qu'examiner
* rend ; le retenu y reste, fautif ou non. La charge n'est pas modifiée.
*
* @param {Object} charge
* @param {{proposition: number}|{retenu: true}} cible
* @returns {{lignes: Array<Object>|null, fautes: Array<Object>}}
* @throws {ErreurCommande} PROPOSITION_INCONNUE {id} ; AUCUN_RETENU, remède
* la section des propositions
* @throws {TypeError} une cible d'une autre forme
*/
export function formeNommeeDe(charge, cible) {
const cles = cible !== null && typeof cible === 'object' ? clesRangees(cible) : null;
let placement;
if (cles?.length === 1 && cles[0] === 'proposition' && Number.isSafeInteger(cible.proposition)) {
placement = charge.propositions.find(({ id }) => id === cible.proposition);
if (placement === undefined) throw refus('PROPOSITION_INCONNUE', { id: cible.proposition });
} else if (cles?.length === 1 && cles[0] === 'retenu' && cible.retenu === true) {
placement = charge.retenu;
if (placement === null) throw refus('AUCUN_RETENU', {}, { geste: 'ouvrirSection', section: 'propositions' });
} else {
throw new TypeError(`cible : {proposition: id} ou {retenu: true} attendu, reçu ${JSON.stringify(cible)}`);
}
const lues = fautes(placement);
const lisible = !lues.some(({ code }) => code === 'FORME');
return { lignes: lisible ? formeNommee(placement, charge) : null, fautes: lues };
}
[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
/**
* La charge d'un événement neuf (§ 8.1) et le libellé de sa création. Chaque
* champ se contrôle selon sa règle avant que l'identifiant ne se tire :
* NOM_REQUIS, DATE_INVALIDE {date}, SIEGES_INVALIDES {sieges},
* TOURS_INVALIDES {tours}. Le nom entre en NFC, ses blancs de bord retirés ;
* une date vide vaut null.
*
* @param {{nom: string, date?: string|null, siegesParDefaut: number, tours: number}} evenement
* @param {() => string} identifiant tiré une fois, après les contrôles
* @returns {{charge: Object, libelle: string}}
* @throws {ErreurCommande}
*/
export function chargeNeuve({ nom, date = null, siegesParDefaut, tours }, identifiant) {
const regle = (cle) => regleDuChamp(EVENEMENT, cle);
const nomLu = saisie(nom, 'nom');
const dateLue = saisie(date, 'date');
if (premiereFaute(nomLu, regle('nom')) !== null) throw refus('NOM_REQUIS');
if (premiereFaute(dateLue, regle('date')) !== null) throw refus('DATE_INVALIDE', { date });
if (premiereFaute(siegesParDefaut, regle('siegesParDefaut')) !== null) {
throw refus('SIEGES_INVALIDES', { sieges: siegesParDefaut });
}
if (premiereFaute(tours, regle('tours')) !== null) throw refus('TOURS_INVALIDES', { tours });
const charge = creerCharge({ id: identifiant(), nom: nomLu, date: dateLue, siegesParDefaut, tours });
return { charge, libelle: libelle('ENTREE_CREATION', { nom: nomLu }) };
}