[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)
// Épreuves des commandes nommées (§ 4, § 4.4, § 5.7, § 8.2, § 9, § 10.1) :
// chacune rend une charge neuve et un libellé figé, ne touche pas la charge
// reçue, tire ses identifiants de prochainsIds sans jamais en réutiliser un,
// et refuse par un code. Les noms d'épreuve sont inventés ; la version vient
// du module engendré.
import assert from 'node:assert/strict' ;
import { describe , test } from '../../test/lanceur.js' ;
import { apercevoir } from '../csv/apercu.js' ;
import { bilanRemplacement } from '../csv/import.js' ;
import { rechercher } from '../moteur/recherche.js' ;
import { configurationDepuisCharge , creerCharge , etatDeduit } from '../stockage/document.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 { derive , fautes , formeNommee , planDepuisFichier } 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 { VERSION } from '../version.genere.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 { COMMANDES , ErreurCommande , appliquerCommande , chargeNeuve , formeNommeeDe } from './commandes.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
const V1 = VERSION . affichee ;
const personne = ( id , nom , prenom = null , appartenance = null ) => ( {
id ,
nom ,
prenom ,
appartenance ,
courriel : null ,
titrePressenti : null ,
notes : null ,
exclu : false ,
} ) ;
const table = ( id , x ) => ( { id , numero : id , sieges : null , forme : 'ronde' , position : { x , y : 0 } } ) ;
// Quatre personnes, deux tables de deux places, deux tours. Iris Ombrelle
// anime la table 1 : une réservation « tous » au siège 1, et le titre de ce
// siège.
function chargeEssai ( ) {
const charge = creerCharge ( { id : 'evt-lucioles' , nom : 'Soirée des Lucioles' , siegesParDefaut : 2 , tours : 2 } ) ;
charge . prochainsIds = { participant : 5 , table : 3 , proposition : 1 } ;
charge . participants = [
personne ( 1 , 'Ombrelle' , 'Iris' , 'Club des Merles' ) ,
personne ( 2 , 'Grisaille' , null , 'Club des Merles' ) ,
personne ( 3 , 'Pervenche' , 'Théo' ) ,
personne ( 4 , 'Lacasse' , 'Ondine' , 'Société Alpha' ) ,
] ;
charge . tables = [ table ( 1 , 0 ) , table ( 2 , 250 ) ] ;
charge . reservations = [ { participant : 1 , table : 1 , siege : 1 , portee : 'tous' , tour : null } ] ;
charge . titres = [ { table : 1 , siege : 1 , libelle : 'animation' } ] ;
return charge ;
}
// Les arguments d'enregistrerGeneration : une génération courte du moteur
// sur la configuration de la charge.
function generer ( charge , nombre = 3 , graine = 7 ) {
const configuration = configurationDepuisCharge ( charge ) ;
return { propositions : rechercher ( configuration , { graine , arret : 40 , nombre } ) , configuration , produitVersion : V1 } ;
}
// La charge d'essai, une cinquième personne exclue, deux propositions et la
// première retenue.
function chargeRiche ( ) {
const charge = chargeEssai ( ) ;
charge . participants . push ( { ... personne ( 5 , 'Ancolie' , 'Amandine' ) , exclu : true } ) ;
charge . prochainsIds . participant = 6 ;
const generee = COMMANDES . get ( 'enregistrerGeneration' ) ( charge , generer ( charge , 2 ) ) . charge ;
return COMMANDES . get ( 'retenirProposition' ) ( generee , { id : 1 } ) . charge ;
}
// Gèle une valeur en profondeur : une commande qui la modifierait lèverait.
function figer ( valeur ) {
if ( valeur !== null && typeof valeur === 'object' && ! Object . isFrozen ( valeur ) ) {
Object . freeze ( valeur ) ;
for ( const element of Object . values ( valeur ) ) figer ( element ) ;
}
return valeur ;
}
// Le refus attendu : une ErreurCommande de ce code, et de ces détails quand
// ils sont donnés.
function refuse ( appel , code , details ) {
assert . throws ( appel , ( erreur ) => {
assert . ok ( erreur instanceof ErreurCommande , ` ErreurCommande attendue, reçu ${ erreur } ` ) ;
assert . equal ( erreur . code , code ) ;
if ( details !== undefined ) assert . deepEqual ( erreur . details , details ) ;
return true ;
} ) ;
}
const executer = ( nom , charge , args ) => COMMANDES . get ( nom ) ( charge , args ) ;
const CSV _DEUX = 'nom;prenom;appartenance\nAncolie;Bastien;Club des Merles\nBruyère;Capucine;\n' ;
describe ( 'commandes : le registre (§ 8.2)' , ( ) => {
test ( 'le registre porte les onze commandes du plan, chacune une fonction' , ( ) => {
assert . deepEqual ( [ ... COMMANDES . keys ( ) ] . sort ( ) , [
'ajouterParticipant' ,
'changerEtat' ,
'debloquer' ,
'effacerPropositions' ,
'enregistrerGeneration' ,
'exclureParticipant' ,
'importerParticipants' ,
'modifierParticipant' ,
'reintegrerParticipant' ,
'retenirProposition' ,
'supprimerParticipant' ,
] ) ;
for ( const commande of COMMANDES . values ( ) ) assert . equal ( typeof commande , 'function' ) ;
} ) ;
test ( 'appliquerCommande rend charge, libellé et avertissements ; un nom inconnu est une faute du code' , ( ) => {
const rendu = appliquerCommande ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : 'Bruyère' } } ) ;
assert . equal ( rendu . libelle , 'Ajouté Bruyère' ) ;
assert . deepEqual ( rendu . avertissements , [ ] ) ;
assert . equal ( rendu . charge . participants . length , 5 ) ;
assert . throws ( ( ) => appliquerCommande ( 'commandeInventee' , chargeEssai ( ) , { } ) , TypeError ) ;
} ) ;
test ( 'un geste qui ne change pas la charge est refusé SANS_EFFET' , ( ) => {
const charge = chargeEssai ( ) ;
[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 apercu = apercevoir ( 'nom;prenom\nLacasse;Ondine\n' , { participants : charge . participants , mode : 'mettreAJour' } ) ;
[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
refuse ( ( ) => appliquerCommande ( 'importerParticipants' , charge , { apercu , mode : 'mettreAJour' } ) , 'SANS_EFFET' , {
commande : 'importerParticipants' ,
remede : null ,
} ) ;
} ) ;
} ) ;
describe ( 'commandes : les participants (§ 4, § 4.4, § 10.2)' , ( ) => {
test ( 'ajouter : seul le nom est requis ; les autres champs valent null, exclu faux ; le compteur avance' , ( ) => {
const { charge , libelle , avertissements } = executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : ' Pervenche ' } } ) ;
assert . deepEqual ( charge . participants . at ( - 1 ) , personne ( 5 , 'Pervenche' ) ) ;
assert . equal ( charge . prochainsIds . participant , 6 ) ;
assert . equal ( libelle , 'Ajouté Pervenche' ) ;
assert . deepEqual ( avertissements , [ ] ) ;
const complet = executer ( 'ajouterParticipant' , chargeEssai ( ) , {
champs : { nom : 'Parasol' , prenom : 'Iris' , appartenance : 'Club des Merles' , courriel : '' , notes : ' arrive tard ' , exclu : true } ,
} ) ;
assert . deepEqual ( complet . charge . participants . at ( - 1 ) , {
... personne ( 5 , 'Parasol' , 'Iris' , 'Club des Merles' ) ,
notes : 'arrive tard' ,
exclu : true ,
} ) ;
assert . equal ( complet . libelle , 'Ajouté Iris Parasol' ) ;
} ) ;
test ( 'ajouter : un nom saisi en NFD est enregistré en NFC' , ( ) => {
const { charge , libelle } = executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : 'Pervenche' , prenom : 'The\u{301}o' } } ) ;
assert . equal ( charge . participants . at ( - 1 ) . prenom , 'Th\u{E9}o' ) ;
assert . equal ( libelle , 'Ajouté Th\u{E9}o Pervenche' ) ;
} ) ;
test ( 'ajouter : un nom vide ou blanc est refusé NOM_REQUIS ; un champ inconnu ou de forme fausse est une faute du code' , ( ) => {
refuse ( ( ) => executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : ' ' } } ) , 'NOM_REQUIS' , { remede : null } ) ;
refuse ( ( ) => executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { prenom : 'Iris' } } ) , 'NOM_REQUIS' ) ;
assert . throws ( ( ) => executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : 'Bruyère' , age : 3 } } ) , TypeError ) ;
assert . throws ( ( ) => executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : 'Bruyère' , exclu : 'oui' } } ) , TypeError ) ;
assert . throws ( ( ) => executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : 42 } } ) , TypeError ) ;
} ) ;
test ( "un identifiant ne se réutilise jamais : supprimer ne fait pas reculer le compteur (§ 4)" , ( ) => {
const ajoutee = executer ( 'ajouterParticipant' , chargeEssai ( ) , { champs : { nom : 'Bruyère' } } ) . charge ;
const supprimee = executer ( 'supprimerParticipant' , ajoutee , { id : 5 } ) . charge ;
assert . equal ( supprimee . prochainsIds . participant , 6 ) ;
const rajoutee = executer ( 'ajouterParticipant' , supprimee , { champs : { nom : 'Bruyère' } } ) . charge ;
assert . deepEqual ( rajoutee . participants . map ( ( { id } ) => id ) , [ 1 , 2 , 3 , 4 , 6 ] ) ;
} ) ;
test ( 'modifier : le libellé nomme la personne et chaque champ changé' , ( ) => {
const { charge , libelle } = executer ( 'modifierParticipant' , chargeEssai ( ) , { id : 4 , champs : { courriel : ' o@exemple.test ' } } ) ;
assert . equal ( charge . participants [ 3 ] . courriel , 'o@exemple.test' ) ;
assert . equal ( libelle , 'Modifié Ondine Lacasse : courriel' ) ;
const deux = executer ( 'modifierParticipant' , chargeEssai ( ) , { id : 3 , champs : { notes : 'à jeun' , titrePressenti : 'hôte' , courriel : null } } ) ;
assert . equal ( deux . libelle , 'Modifié Théo Pervenche : titre pressenti, notes' ) ;
} ) ;
test ( "modifier : renommer garde l'ancien nom au libellé et nomme le nouveau" , ( ) => {
const { charge , libelle } = executer ( 'modifierParticipant' , chargeEssai ( ) , { id : 1 , champs : { nom : 'Parasol' } } ) ;
assert . equal ( charge . participants [ 0 ] . nom , 'Parasol' ) ;
assert . equal ( libelle , 'Modifié Iris Ombrelle : nom (désormais Iris Parasol)' ) ;
} ) ;
test ( 'modifier : sans changement SANS_EFFET ; personne inconnue ; nom vidé ; exclu passe par sa commande' , ( ) => {
refuse ( ( ) => executer ( 'modifierParticipant' , chargeEssai ( ) , { id : 4 , champs : { nom : 'Lacasse' , courriel : '' } } ) , 'SANS_EFFET' ) ;
refuse ( ( ) => executer ( 'modifierParticipant' , chargeEssai ( ) , { id : 9 , champs : { nom : 'Lacasse' } } ) , 'PARTICIPANT_INCONNU' , {
id : 9 ,
remede : null ,
} ) ;
refuse ( ( ) => executer ( 'modifierParticipant' , chargeEssai ( ) , { id : 4 , champs : { nom : ' ' } } ) , 'NOM_REQUIS' ) ;
assert . throws ( ( ) => executer ( 'modifierParticipant' , chargeEssai ( ) , { id : 4 , champs : { exclu : true } } ) , TypeError ) ;
} ) ;
test ( "exclure : les réservations restent, suspendues ; le titre n'est plus pourvu ; l'avertissement le dit (§ 4.4)" , ( ) => {
const avant = chargeEssai ( ) ;
assert . equal ( bilanRemplacement ( avant ) . titresPourvus , 1 ) ;
const { charge , libelle , avertissements } = executer ( 'exclureParticipant' , avant , { id : 1 } ) ;
assert . equal ( charge . participants [ 0 ] . exclu , true ) ;
assert . deepEqual ( charge . reservations , avant . reservations ) ;
assert . deepEqual ( charge . titres , avant . titres ) ;
assert . equal ( bilanRemplacement ( charge ) . titresPourvus , 0 ) ;
assert . equal ( libelle , 'Exclu Iris Ombrelle' ) ;
assert . deepEqual ( avertissements , [
{ code : 'RESERVATIONS_SUSPENDUES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ,
] ) ;
} ) ;
test ( 'réintégrer rend les réservations et le titre, et le dit' , ( ) => {
const exclue = executer ( 'exclureParticipant' , chargeEssai ( ) , { id : 1 } ) . charge ;
const { charge , libelle , avertissements } = executer ( 'reintegrerParticipant' , exclue , { id : 1 } ) ;
assert . equal ( charge . participants [ 0 ] . exclu , false ) ;
assert . equal ( bilanRemplacement ( charge ) . titresPourvus , 1 ) ;
assert . equal ( libelle , 'Réintégré Iris Ombrelle' ) ;
assert . deepEqual ( avertissements , [
{ code : 'RESERVATIONS_RETABLIES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ,
] ) ;
} ) ;
test ( "exclure sans réservation n'avertit pas ; exclure deux fois, réintégrer une présente : SANS_EFFET" , ( ) => {
assert . deepEqual ( executer ( 'exclureParticipant' , chargeEssai ( ) , { id : 3 } ) . avertissements , [ ] ) ;
const exclue = executer ( 'exclureParticipant' , chargeEssai ( ) , { id : 3 } ) . charge ;
refuse ( ( ) => executer ( 'exclureParticipant' , exclue , { id : 3 } ) , 'SANS_EFFET' ) ;
refuse ( ( ) => executer ( 'reintegrerParticipant' , chargeEssai ( ) , { id : 3 } ) , 'SANS_EFFET' ) ;
refuse ( ( ) => executer ( 'exclureParticipant' , chargeEssai ( ) , { id : 9 } ) , 'PARTICIPANT_INCONNU' ) ;
} ) ;
test ( 'supprimer : ses réservations partent, les titres et les propositions restent, signalées en dérive' , ( ) => {
const avant = chargeRiche ( ) ;
const { charge , libelle , avertissements } = executer ( 'supprimerParticipant' , avant , { id : 1 } ) ;
assert . deepEqual ( charge . participants . map ( ( { id } ) => id ) , [ 2 , 3 , 4 , 5 ] ) ;
assert . deepEqual ( charge . reservations , [ ] ) ;
assert . deepEqual ( charge . titres , avant . titres ) ;
assert . deepEqual ( charge . prochainsIds , avant . prochainsIds ) ;
assert . deepEqual ( charge . propositions , avant . propositions ) ;
assert . ok ( derive ( charge . propositions [ 0 ] , charge ) . some ( ( { code , participant } ) => code === 'PARTICIPANT_SUPPRIME' && participant === 1 ) ) ;
assert . equal ( libelle , 'Supprimé Iris Ombrelle' ) ;
assert . deepEqual ( avertissements , [
{ code : 'RESERVATIONS_RETIREES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ,
] ) ;
refuse ( ( ) => executer ( 'supprimerParticipant' , chargeEssai ( ) , { id : 9 } ) , 'PARTICIPANT_INCONNU' ) ;
} ) ;
} ) ;
describe ( "commandes : l'import (§ 10.1)" , ( ) => {
test ( 'ajouter : chaque ligne valide devient une personne, sous le compteur, en une seule commande' , ( ) => {
const avant = chargeEssai ( ) ;
const apercu = apercevoir ( CSV _DEUX , { participants : avant . participants } ) ;
const { charge , libelle , resume } = executer ( 'importerParticipants' , avant , { apercu , mode : 'ajouter' } ) ;
assert . deepEqual ( charge . participants . slice ( 4 ) . map ( ( { id , nom , prenom , appartenance } ) => [ id , nom , prenom , appartenance ] ) , [
[ 5 , 'Ancolie' , 'Bastien' , 'Club des Merles' ] ,
[ 6 , 'Bruyère' , 'Capucine' , null ] ,
] ) ;
assert . equal ( charge . prochainsIds . participant , 7 ) ;
assert . equal ( libelle , 'Importé 2 participants' ) ;
assert . equal ( resume . ajoutes , 2 ) ;
} ) ;
test ( 'remplacer et mettre à jour se nomment au libellé, chaque compte à sa place' , ( ) => {
const avant = chargeEssai ( ) ;
[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 apercu = apercevoir ( CSV _DEUX , { participants : avant . participants , mode : 'remplacer' } ) ;
[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
const remplace = executer ( 'importerParticipants' , avant , { apercu , mode : 'remplacer' } ) ;
assert . equal ( remplace . libelle , 'Remplacé la liste par import : 4 retirés, 2 ajoutés' ) ;
assert . deepEqual ( remplace . charge . reservations , [ ] ) ;
// Deux ajoutés, un modifié : des comptes distincts, qu'un libellé qui les
// intervertirait ne rendrait pas.
const csv = 'nom;prenom;courriel\nLacasse;Ondine;o@exemple.test\nAncolie;Bastien;\nBruyère;Capucine;\n' ;
const misAJour = executer ( 'importerParticipants' , avant , {
[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
apercu : apercevoir ( csv , { participants : avant . participants , mode : 'mettreAJour' } ) ,
[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
mode : 'mettreAJour' ,
} ) ;
assert . deepEqual ( [ misAJour . resume . ajoutes , misAJour . resume . misAJour ] , [ 2 , 1 ] ) ;
assert . equal ( misAJour . libelle , 'Mis à jour la liste par import : 2 ajoutés, 1 modifié' ) ;
} ) ;
test ( "importer selon l'état : rien en brouillon et proposé, l'avertissement de dérive en retenu, refusé en bloqué" , ( ) => {
const enRetenu = [ { code : 'IMPORT_EN_RETENU' , details : { } } ] ;
const brouillon = chargeEssai ( ) ;
const proposee = executer ( 'enregistrerGeneration' , chargeEssai ( ) , generer ( chargeEssai ( ) , 2 ) ) . charge ;
const retenue = chargeRiche ( ) ;
assert . deepEqual ( [ brouillon , proposee , retenue ] . map ( ( { evenement } ) => evenement . etat ) , [ 'brouillon' , 'propose' , 'retenu' ] ) ;
[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 ajout = ( charge , mode = 'ajouter' ) => ( { apercu : apercevoir ( 'nom\nBruyère\n' , { participants : charge . participants , mode } ) , mode } ) ;
[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
assert . deepEqual ( executer ( 'importerParticipants' , brouillon , ajout ( brouillon ) ) . avertissements , [ ] ) ;
assert . deepEqual ( executer ( 'importerParticipants' , proposee , ajout ( proposee ) ) . avertissements , [ ] ) ;
for ( const mode of [ 'ajouter' , 'mettreAJour' , 'remplacer' ] ) {
assert . deepEqual ( executer ( 'importerParticipants' , retenue , ajout ( retenue , mode ) ) . avertissements , enRetenu , mode ) ;
}
// Une mise à jour sans personne nouvelle avertit aussi : le placement
// retenu date de la liste d'avant.
[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 courriel = apercevoir ( 'nom;prenom;courriel\nLacasse;Ondine;o@exemple.test\n' , {
participants : retenue . participants ,
mode : 'mettreAJour' ,
} ) ;
[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
const misAJour = executer ( 'importerParticipants' , retenue , { apercu : courriel , mode : 'mettreAJour' } ) ;
assert . deepEqual ( [ misAJour . resume . ajoutes , misAJour . resume . misAJour ] , [ 0 , 1 ] ) ;
assert . deepEqual ( misAJour . avertissements , enRetenu ) ;
const bloquee = { ... retenue , evenement : { ... retenue . evenement , etat : 'bloque' } } ;
refuse ( ( ) => executer ( 'importerParticipants' , bloquee , ajout ( bloquee ) ) , 'PLAN_BLOQUE' , { remede : { geste : 'debloquer' } } ) ;
} ) ;
test ( "un aperçu refusé ou ambigu est refusé sous son code ; un plan bloqué nomme le remède « Débloquer »" , ( ) => {
const avant = chargeEssai ( ) ;
const sansNom = apercevoir ( 'prenom;courriel\nIris;i@exemple.test\n' , { participants : avant . participants } ) ;
refuse ( ( ) => executer ( 'importerParticipants' , avant , { apercu : sansNom , mode : 'ajouter' } ) , 'NOM_NON_ASSOCIE' , {
ligne : null ,
remede : null ,
} ) ;
const ambigu = apercevoir ( 'nom;organisation;entreprise\nAncolie;A;B\n' , { participants : avant . participants } ) ;
refuse ( ( ) => executer ( 'importerParticipants' , avant , { apercu : ambigu , mode : 'ajouter' } ) , 'AMBIGUITE' , {
champs : [ 'appartenance' ] ,
remede : null ,
} ) ;
const bloquee = { ... avant , evenement : { ... avant . evenement , etat : 'bloque' } } ;
const apercu = apercevoir ( CSV _DEUX , { participants : avant . participants } ) ;
refuse ( ( ) => executer ( 'importerParticipants' , bloquee , { apercu , mode : 'ajouter' } ) , 'PLAN_BLOQUE' , {
remede : { geste : 'debloquer' } ,
} ) ;
} ) ;
} ) ;
[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
describe ( "commandes : l'import qui exclut, réintègre ou ne change rien (§ 4.4, § 10.1)" , ( ) => {
// La charge d'essai, où Ondine Lacasse tient aussi une réservation, au
// tour 1, sans siège.
function deuxReservees ( ) {
const charge = chargeEssai ( ) ;
charge . reservations . push ( { participant : 4 , table : 2 , siege : null , portee : 'tour' , tour : 1 } ) ;
return charge ;
}
const importer = ( charge , csv ) =>
appliquerCommande ( 'importerParticipants' , charge , {
apercu : apercevoir ( csv , { participants : charge . participants , mode : 'mettreAJour' } ) ,
mode : 'mettreAJour' ,
} ) ;
test ( "une mise à jour qui exclut des personnes réservées le dit pour chacune, comme exclure (§ 4.4)" , ( ) => {
const avant = figer ( deuxReservees ( ) ) ;
const rendu = importer ( avant , 'nom;prenom;exclu\nOmbrelle;Iris;oui\nLacasse;Ondine;oui\nPervenche;Théo;oui\n' ) ;
assert . equal ( rendu . libelle , 'Mis à jour la liste par import : 0 ajouté, 3 modifiés' ) ;
assert . deepEqual ( rendu . avertissements , [
{ code : 'RESERVATIONS_SUSPENDUES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ,
{ code : 'RESERVATIONS_SUSPENDUES' , details : { participant : 4 , personne : 'Ondine Lacasse' , reservations : 1 , titres : 0 } } ,
] ) ;
// Le même bilan que la commande d'exclusion, personne par personne.
assert . deepEqual ( rendu . avertissements [ 0 ] , executer ( 'exclureParticipant' , avant , { id : 1 } ) . avertissements [ 0 ] ) ;
assert . deepEqual ( rendu . avertissements [ 1 ] , executer ( 'exclureParticipant' , avant , { id : 4 } ) . avertissements [ 0 ] ) ;
} ) ;
test ( 'une mise à jour qui réintègre une personne réservée le dit, comme réintégrer ; en retenu, après la dérive' , ( ) => {
const exclue = executer ( 'exclureParticipant' , chargeEssai ( ) , { id : 1 } ) . charge ;
const rendu = importer ( exclue , 'nom;prenom;exclu\nOmbrelle;Iris;non\n' ) ;
const retablies = { code : 'RESERVATIONS_RETABLIES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ;
assert . deepEqual ( rendu . avertissements , [ retablies ] ) ;
assert . equal ( rendu . charge . participants [ 0 ] . exclu , false ) ;
const retenue = chargeRiche ( ) ;
const enRetenu = importer ( retenue , 'nom;prenom;exclu\nOmbrelle;Iris;oui\n' ) ;
assert . deepEqual ( enRetenu . avertissements . map ( ( { code } ) => code ) , [ 'IMPORT_EN_RETENU' , 'RESERVATIONS_SUSPENDUES' ] ) ;
} ) ;
test ( "une mise à jour dont les lignes valides ne changent rien, mais qui refuse des lignes, n'est pas SANS_EFFET : elle rend son résumé, sans entrée" , ( ) => {
const avant = chargeEssai ( ) ;
avant . participants . push ( personne ( 5 , 'Lacasse' , 'Ondine' ) ) ;
avant . prochainsIds . participant = 6 ;
figer ( avant ) ;
const rendu = importer ( avant , 'nom;prenom;courriel\nLacasse;Ondine;o@exemple.test\nPervenche;Théo;\n' ) ;
assert . equal ( rendu . libelle , null ) ;
assert . deepEqual ( rendu . avertissements , [ ] ) ;
assert . equal ( rendu . charge , avant ) ;
assert . deepEqual ( rendu . resume . refusees . map ( ( { ligne , code , participants } ) => [ ligne , code , participants ] ) , [
[ 2 , 'PLUSIEURS_CORRESPONDENT' , [ 4 , 5 ] ] ,
] ) ;
assert . deepEqual ( [ rendu . resume . ajoutes , rendu . resume . misAJour , rendu . resume . inchanges ] , [ 0 , 0 , 1 ] ) ;
} ) ;
} ) ;
describe ( 'commandes : la forme nommée, une lecture (§ 8.9, point 4)' , ( ) => {
test ( "rend la forme nommée d'une proposition ou du retenu, avec ses fautes, sans toucher la charge" , ( ) => {
const charge = figer ( chargeRiche ( ) ) ;
assert . deepEqual ( formeNommeeDe ( charge , { proposition : 2 } ) , {
lignes : formeNommee ( charge . propositions [ 1 ] , charge ) ,
fautes : [ ] ,
} ) ;
const retenu = formeNommeeDe ( charge , { retenu : true } ) ;
assert . deepEqual ( retenu , { lignes : formeNommee ( charge . retenu , charge ) , fautes : [ ] } ) ;
assert . ok ( retenu . lignes . length > 0 ) ;
assert . ok ( retenu . lignes . every ( ( { nom } ) => typeof nom === 'string' ) ) ;
} ) ;
test ( "un retenu incohérent se lit quand même : une ligne par identifiant écrit, et ses fautes à côté" , ( ) => {
const charge = chargeRiche ( ) ;
const assis = charge . retenu . placement [ 0 ] . sieges [ 0 ] [ 0 ] ;
charge . retenu . placement [ 0 ] . reserve = [ ... charge . retenu . placement [ 0 ] . reserve , assis ] . sort ( ( a , b ) => a - b ) ;
const coherent = chargeRiche ( ) ;
const lue = formeNommeeDe ( charge , { retenu : true } ) ;
assert . deepEqual ( lue . fautes , [ { code : 'DOUBLON' , tour : 1 , participant : assis } ] ) ;
assert . deepEqual ( lue . fautes , fautes ( charge . retenu ) ) ;
assert . equal ( lue . lignes . length , formeNommee ( coherent . retenu , coherent ) . length + 1 ) ;
assert . equal ( lue . lignes . filter ( ( { tour , participant } ) => tour === 1 && participant === assis ) . length , 2 ) ;
} ) ;
test ( "un placement dont la forme est fausse n'a pas de lignes : ses fautes seules le disent" , ( ) => {
const charge = chargeRiche ( ) ;
charge . retenu . placement = 'illisible' ;
const lue = formeNommeeDe ( charge , { retenu : true } ) ;
assert . equal ( lue . lignes , null ) ;
assert . deepEqual ( lue . fautes . map ( ( { code } ) => code ) , [ 'FORME' ] ) ;
} ) ;
test ( "une proposition inconnue, un retenu absent sont refusés ; une cible d'une autre forme est une faute du code" , ( ) => {
refuse ( ( ) => formeNommeeDe ( chargeRiche ( ) , { proposition : 9 } ) , 'PROPOSITION_INCONNUE' , { id : 9 , remede : null } ) ;
refuse ( ( ) => formeNommeeDe ( chargeEssai ( ) , { retenu : true } ) , 'AUCUN_RETENU' , {
remede : { geste : 'ouvrirSection' , section : 'propositions' } ,
} ) ;
for ( const cible of [ { } , { retenu : false } , { proposition : '2' } , { proposition : 2 , retenu : true } , null ] ) {
assert . throws ( ( ) => formeNommeeDe ( chargeRiche ( ) , cible ) , TypeError , JSON . stringify ( cible ) ) ;
}
} ) ;
} ) ;
[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
describe ( 'commandes : les états du plan (§ 9)' , ( ) => {
// Une charge à l'état donné : vide, avec des propositions, ou avec un retenu.
function chargeA ( etat , contenu ) {
const charge = contenu === 'vide' ? chargeEssai ( ) : chargeRiche ( ) ;
if ( contenu === 'propositions' ) charge . retenu = null ;
charge . evenement . etat = etat ;
return charge ;
}
test ( 'les transitions admises et refusées, chacune sans rien détruire' , ( ) => {
// [de, contenu, vers, refus attendu ou null]
const cas = [
[ 'brouillon' , 'vide' , 'propose' , 'AUCUNE_PROPOSITION' ] ,
[ 'brouillon' , 'vide' , 'retenu' , 'AUCUN_RETENU' ] ,
[ 'brouillon' , 'vide' , 'bloque' , null ] ,
[ 'brouillon' , 'propositions' , 'propose' , null ] ,
[ 'brouillon' , 'propositions' , 'retenu' , 'AUCUN_RETENU' ] ,
[ 'propose' , 'propositions' , 'brouillon' , null ] ,
[ 'propose' , 'retenu' , 'retenu' , null ] ,
[ 'retenu' , 'retenu' , 'propose' , null ] ,
[ 'retenu' , 'retenu' , 'brouillon' , null ] ,
[ 'retenu' , 'retenu' , 'bloque' , null ] ,
[ 'retenu' , 'retenu' , 'retenu' , 'SANS_EFFET' ] ,
[ 'bloque' , 'retenu' , 'retenu' , 'PLAN_BLOQUE' ] ,
[ 'bloque' , 'retenu' , 'brouillon' , 'PLAN_BLOQUE' ] ,
] ;
for ( const [ de , contenu , vers , refus ] of cas ) {
const avant = chargeA ( de , contenu ) ;
if ( refus !== null ) {
refuse ( ( ) => executer ( 'changerEtat' , avant , { etat : vers } ) , refus ) ;
continue ;
}
const { charge , libelle } = executer ( 'changerEtat' , avant , { etat : vers } ) ;
assert . equal ( charge . evenement . etat , vers , ` ${ de } → ${ vers } ` ) ;
assert . deepEqual ( { ... charge , evenement : { ... charge . evenement , etat : de } } , avant , ` ${ de } → ${ vers } : rien d'autre ne change ` ) ;
assert . match ( libelle , /^Plan passé à l'état « / ) ;
}
} ) ;
test ( 'un refus de transition nomme le remède ; un état inconnu est une faute du code' , ( ) => {
refuse ( ( ) => executer ( 'changerEtat' , chargeA ( 'brouillon' , 'vide' ) , { etat : 'propose' } ) , 'AUCUNE_PROPOSITION' , {
remede : { geste : 'ouvrirSection' , section : 'generation' } ,
} ) ;
refuse ( ( ) => executer ( 'changerEtat' , chargeA ( 'propose' , 'propositions' ) , { etat : 'retenu' } ) , 'AUCUN_RETENU' , {
remede : { geste : 'ouvrirSection' , section : 'propositions' } ,
} ) ;
assert . throws ( ( ) => executer ( 'changerEtat' , chargeA ( 'brouillon' , 'vide' ) , { etat : 'archive' } ) , TypeError ) ;
} ) ;
test ( "débloquer rend l'état que le contenu impose ; un plan qui n'est pas bloqué est refusé" , ( ) => {
for ( const contenu of [ 'vide' , 'propositions' , 'retenu' ] ) {
const avant = chargeA ( 'bloque' , contenu ) ;
const { charge , libelle } = executer ( 'debloquer' , avant , { } ) ;
assert . equal ( charge . evenement . etat , etatDeduit ( avant ) ) ;
assert . match ( libelle , /^Plan débloqué/ ) ;
}
refuse ( ( ) => executer ( 'debloquer' , chargeA ( 'retenu' , 'retenu' ) , { } ) , 'PLAN_NON_BLOQUE' , { remede : null } ) ;
} ) ;
} ) ;
describe ( 'commandes : générer, retenir, effacer (§ 5.7, § 8.9)' , ( ) => {
test ( 'les propositions prennent leurs identifiants à partir du compteur, et le relèvent ; brouillon passe à proposé' , ( ) => {
const avant = chargeEssai ( ) ;
avant . prochainsIds . proposition = 4 ;
const args = generer ( avant , 3 ) ;
const { charge , libelle } = executer ( 'enregistrerGeneration' , avant , args ) ;
assert . deepEqual ( charge . propositions . map ( ( { id } ) => id ) , [ 4 , 5 , 6 ] ) ;
assert . equal ( charge . prochainsIds . proposition , 7 ) ;
assert . equal ( charge . evenement . etat , 'propose' ) ;
assert . equal ( libelle , 'Généré 3 propositions (n° 4 à 6)' ) ;
charge . propositions . forEach ( ( p , rang ) => {
const source = args . propositions [ rang ] ;
assert . deepEqual ( [ p . graine , p . arret , p . historique , p . produitVersion ] , [ source . graine , source . arret , source . historique , V1 ] ) ;
assert . deepEqual ( planDepuisFichier ( p ) , source . plan ) ;
assert . equal ( p . siegesAttribues , avant . reglages . attribuerSieges ) ;
} ) ;
} ) ;
test ( "une seconde génération continue la numérotation ; après un effacement, aucun identifiant ne revient" , ( ) => {
const premiere = executer ( 'enregistrerGeneration' , chargeEssai ( ) , generer ( chargeEssai ( ) , 3 ) ) . charge ;
const seconde = executer ( 'enregistrerGeneration' , premiere , generer ( premiere , 2 , 11 ) ) . charge ;
assert . deepEqual ( seconde . propositions . map ( ( { id } ) => id ) , [ 1 , 2 , 3 , 4 , 5 ] ) ;
assert . equal ( seconde . evenement . etat , 'propose' ) ;
const effacee = executer ( 'effacerPropositions' , seconde , { } ) . charge ;
const troisieme = executer ( 'enregistrerGeneration' , effacee , generer ( effacee , 1 , 13 ) ) . charge ;
assert . deepEqual ( troisieme . propositions . map ( ( { id } ) => id ) , [ 6 ] ) ;
} ) ;
test ( "une liste vide est sans effet ; des identifiants du moteur en double sont une faute du code" , ( ) => {
refuse ( ( ) => executer ( 'enregistrerGeneration' , chargeEssai ( ) , { ... generer ( chargeEssai ( ) , 1 ) , propositions : [ ] } ) , 'SANS_EFFET' ) ;
const args = generer ( chargeEssai ( ) , 2 ) ;
args . propositions [ 1 ] = { ... args . propositions [ 1 ] , id : 1 } ;
assert . throws ( ( ) => executer ( 'enregistrerGeneration' , chargeEssai ( ) , args ) , TypeError ) ;
} ) ;
test ( "retenir pose le placement en vigueur, copié de la proposition, et l'état Retenu" , ( ) => {
const generee = executer ( 'enregistrerGeneration' , chargeEssai ( ) , generer ( chargeEssai ( ) , 2 ) ) . charge ;
const { charge , libelle } = executer ( 'retenirProposition' , generee , { id : 2 } ) ;
const { id , graine , arret , historique , produitVersion , ... plan } = generee . propositions [ 1 ] ;
assert . deepEqual ( charge . retenu , { proposition : 2 , ... plan } ) ;
assert . equal ( charge . evenement . etat , 'retenu' ) ;
assert . deepEqual ( charge . propositions , generee . propositions ) ;
assert . equal ( libelle , 'Retenu la proposition n° 2' ) ;
assert . deepEqual ( [ id , typeof graine , typeof arret , typeof historique , produitVersion ] , [ 2 , 'number' , 'number' , 'number' , V1 ] ) ;
refuse ( ( ) => executer ( 'retenirProposition' , generee , { id : 9 } ) , 'PROPOSITION_INCONNUE' , { id : 9 , remede : null } ) ;
} ) ;
test ( "effacer épargne la proposition retenue ; sans retenu, le plan revient en brouillon ; rien à effacer : SANS_EFFET" , ( ) => {
const riche = chargeRiche ( ) ;
const { charge , libelle } = executer ( 'effacerPropositions' , riche , { } ) ;
assert . deepEqual ( charge . propositions . map ( ( { id } ) => id ) , [ 1 ] ) ;
assert . equal ( charge . prochainsIds . proposition , riche . prochainsIds . proposition ) ;
assert . equal ( charge . evenement . etat , 'retenu' ) ;
assert . equal ( libelle , 'Effacé 1 proposition, gardé la retenue (n° 1)' ) ;
refuse ( ( ) => executer ( 'effacerPropositions' , charge , { } ) , 'SANS_EFFET' ) ;
const proposee = executer ( 'enregistrerGeneration' , chargeEssai ( ) , generer ( chargeEssai ( ) , 2 ) ) . charge ;
const videe = executer ( 'effacerPropositions' , proposee , { } ) ;
assert . deepEqual ( videe . charge . propositions , [ ] ) ;
assert . equal ( videe . charge . evenement . etat , 'brouillon' ) ;
assert . equal ( videe . libelle , 'Effacé 2 propositions' ) ;
} ) ;
} ) ;
describe ( "commandes : la charge d'un événement neuf (§ 8.1)" , ( ) => {
test ( "chargeNeuve contrôle chaque champ avant de tirer l'identifiant, et convertit la saisie en NFC" , ( ) => {
let tirages = 0 ;
const identifiant = ( ) => {
tirages += 1 ;
return ` evt- ${ tirages } ` ;
} ;
const base = { nom : 'Soirée des Lucioles' , date : null , siegesParDefaut : 8 , tours : 3 } ;
refuse ( ( ) => chargeNeuve ( { ... base , nom : ' ' } , identifiant ) , 'NOM_REQUIS' ) ;
refuse ( ( ) => chargeNeuve ( { ... base , date : '2031-02-30' } , identifiant ) , 'DATE_INVALIDE' , { date : '2031-02-30' , remede : null } ) ;
refuse ( ( ) => chargeNeuve ( { ... base , siegesParDefaut : 1 } , identifiant ) , 'SIEGES_INVALIDES' , { sieges : 1 , remede : null } ) ;
refuse ( ( ) => chargeNeuve ( { ... base , tours : 0 } , identifiant ) , 'TOURS_INVALIDES' , { tours : 0 , remede : null } ) ;
assert . equal ( tirages , 0 ) ;
const { charge , libelle } = chargeNeuve ( { ... base , nom : ' Soire\u{301}e des Lucioles ' , date : '2031-05-17' } , identifiant ) ;
assert . equal ( tirages , 1 ) ;
assert . deepEqual ( charge , {
... creerCharge ( { id : 'evt-1' , nom : 'Soir\u{E9}e des Lucioles' , date : '2031-05-17' , siegesParDefaut : 8 , tours : 3 } ) ,
} ) ;
assert . equal ( libelle , "Création de l'événement « Soir\u{E9}e des Lucioles »" ) ;
} ) ;
} ) ;
describe ( 'commandes : la charge reçue ne change jamais (§ 13.2)' , ( ) => {
// Arguments d'exemple de chaque commande sur la charge riche ; debloquer
// reçoit la même charge, bloquée.
function exemples ( charge ) {
return new Map ( [
[ 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ] ,
[ 'modifierParticipant' , { id : 4 , champs : { courriel : 'o@exemple.test' } } ] ,
[ 'exclureParticipant' , { id : 1 } ] ,
[ 'reintegrerParticipant' , { id : 5 } ] ,
[ 'supprimerParticipant' , { id : 2 } ] ,
[ 'importerParticipants' , { apercu : apercevoir ( CSV _DEUX , { participants : charge . participants } ) , mode : 'ajouter' } ] ,
[ 'changerEtat' , { etat : 'bloque' } ] ,
[ 'debloquer' , { } ] ,
[ 'enregistrerGeneration' , generer ( charge , 1 , 5 ) ] ,
[ 'retenirProposition' , { id : 2 } ] ,
[ 'effacerPropositions' , { } ] ,
] ) ;
}
test ( "aucune commande ne modifie la charge qu'elle reçoit" , ( ) => {
const charge = chargeRiche ( ) ;
const args = exemples ( charge ) ;
assert . deepEqual ( [ ... args . keys ( ) ] . sort ( ) , [ ... COMMANDES . keys ( ) ] . sort ( ) ) ;
for ( const [ nom , commande ] of COMMANDES ) {
const recue = nom === 'debloquer' ? { ... charge , evenement : { ... charge . evenement , etat : 'bloque' } } : charge ;
const copie = structuredClone ( recue ) ;
const rendu = commande ( figer ( recue ) , args . get ( nom ) ) ;
assert . deepEqual ( recue , copie , nom ) ;
assert . notEqual ( rendu . charge , recue , nom ) ;
assert . equal ( typeof rendu . libelle , 'string' , nom ) ;
assert . ok ( Array . isArray ( rendu . avertissements ) , nom ) ;
}
} ) ;
test ( 'sur un plan bloqué, chaque commande sauf debloquer est refusée PLAN_BLOQUE, remède « Débloquer »' , ( ) => {
const charge = chargeRiche ( ) ;
const args = exemples ( charge ) ;
const bloquee = figer ( { ... charge , evenement : { ... charge . evenement , etat : 'bloque' } } ) ;
for ( const nom of COMMANDES . keys ( ) ) {
if ( nom === 'debloquer' ) continue ;
refuse ( ( ) => executer ( nom , bloquee , args . get ( nom ) ) , 'PLAN_BLOQUE' , { remede : { geste : 'debloquer' } } ) ;
}
} ) ;
} ) ;