[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';
|
[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';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Le refus d'une commande : son code, et des détails que l'appelant lit pour
|
|
|
|
|
|
* agir, remede compris. Le message, code et détails en JSON, sert aux traces ;
|
|
|
|
|
|
* le texte affiché vient de la table des libellés (§ 14.6).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class ErreurCommande extends Error {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param {string} code
|
|
|
|
|
|
* @param {Object} [details]
|
|
|
|
|
|
*/
|
|
|
|
|
|
constructor(code, details = {}) {
|
|
|
|
|
|
super(`${code} ${JSON.stringify(details)}`);
|
|
|
|
|
|
this.code = code;
|
|
|
|
|
|
this.details = details;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ErreurCommande.prototype.name = 'ErreurCommande';
|
|
|
|
|
|
|
|
|
|
|
|
// Le refus de code donné : ses détails, puis le remède, null sans lui.
|
|
|
|
|
|
const refus = (code, details = {}, remede = null) => new ErreurCommande(code, { ...details, remede });
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Le nom d'une personne tel que les libellés l'écrivent : prénom, puis nom.
|
|
|
|
|
|
const personneDe = ({ nom, prenom }) => (prenom === null ? nom : `${prenom} ${nom}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 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: personneDe(participant) }),
|
|
|
|
|
|
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: personneDe(avant),
|
|
|
|
|
|
champs: changes,
|
|
|
|
|
|
devenu: renomme ? personneDe(apres) : null,
|
|
|
|
|
|
}),
|
|
|
|
|
|
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: personneDe(participant), reservations, titres };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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: personneDe(participant),
|
|
|
|
|
|
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: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Le registre des commandes, par nom : chacune (charge, arguments) →
|
|
|
|
|
|
* { charge, libelle, avertissements }, et importerParticipants y ajoute le
|
|
|
|
|
|
* résumé de l'import.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @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 = new Map([
|
|
|
|
|
|
['ajouterParticipant', ajouterParticipant],
|
|
|
|
|
|
['modifierParticipant', modifierParticipant],
|
|
|
|
|
|
['exclureParticipant', exclureParticipant],
|
|
|
|
|
|
['reintegrerParticipant', reintegrerParticipant],
|
|
|
|
|
|
['supprimerParticipant', supprimerParticipant],
|
|
|
|
|
|
['importerParticipants', importerParticipants],
|
|
|
|
|
|
['changerEtat', changerEtat],
|
|
|
|
|
|
['debloquer', debloquer],
|
|
|
|
|
|
['enregistrerGeneration', enregistrerGeneration],
|
|
|
|
|
|
['retenirProposition', retenirProposition],
|
|
|
|
|
|
['effacerPropositions', effacerPropositions],
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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 }) };
|
|
|
|
|
|
}
|