[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 de la table des libellés (§ 14.6, § 14.2). Le balayage relève, dans
// les sources de src/stockage, src/csv et src/application — épreuves et table
// exclues —, chaque code qu'elles émettent : le premier argument littéral d'un
// appel, constructeur d'erreur ou fabrique d'avertissement ; la valeur
// littérale d'une clé code ; celle d'une clé raison, cause ou remede, qui
// nomme un code de détail ; la valeur littérale d'une constante, qui nomme le
// code que le module émet par elle ; et les tables de codes des contrats,
// celle de src/stockage/types.js et celle de src/csv/erreurs.js quand il en
// porte une. Chaque code relevé a son texte, chaque texte a un émetteur, et
// un balayage vide échoue. Chaque texte se compose ensuite de détails
// d'exemple. Les noms d'épreuve sont inventés.
import assert from 'node:assert/strict' ;
import { readdirSync , readFileSync } from 'node:fs' ;
import { join , relative } from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
import { describe , test } from '../../test/lanceur.js' ;
import { CODES , libelle } from './libelles.js' ;
const RACINE = fileURLToPath ( new URL ( '../..' , import . meta . url ) ) ;
// Les arbres balayés, et les fichiers qui portent une table de codes.
const ARBRES = [ 'src/stockage' , 'src/csv' , 'src/application' ] ;
const TABLES = [ 'src/stockage/types.js' , 'src/csv/erreurs.js' ] ;
// La table des textes elle-même n'émet rien : ses clés sont ce qu'on éprouve.
const TABLE _DES _TEXTES = join ( 'src' , 'application' , 'libelles.js' ) ;
// --- Le balayage ----------------------------------------------------------
// Commentaires et littéraux de chaîne, dans l'ordre où ils s'ouvrent : un
// commentaire se blanchit, sauf ses fins de ligne, et une chaîne reste, si
// bien qu'un « // » dans une chaîne n'ouvre rien.
const JETONS = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\[\s\S]|[^`\\])*`/g ;
const sansCommentaires = ( texte ) =>
texte . replace ( JETONS , ( jeton ) => ( jeton . startsWith ( '/' ) ? jeton . replace ( /[^\n]/g , ' ' ) : jeton ) ) ;
// Un code : des capitales, des chiffres, des soulignés entre deux mots.
const CODE = String . raw ` '([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*)' ` ;
const MOTIFS _EMISSION = [
// Premier argument d'un appel : new ErreurStockage('ABSENT', …),
// refuser('UTF8_INVALIDE'), ajouter('DERIVE', …), libelle('ENTREE_…', …).
// normalize reçoit le nom d'une forme Unicode, non un code.
new RegExp ( String . raw ` \b (?!normalize \b )[A-Za-z_ $ ][ \w $ ]* \s * \( \s * ${ CODE } ` , 'g' ) ,
// Un objet qui porte un code : { code: 'NOM_ABSENT', … }.
new RegExp ( String . raw ` \b code \s *: \s * ${ CODE } ` , 'g' ) ,
// Un code de détail : { raison: 'BLOQUE' }, { cause: 'NOMBRE_DE_CHAMPS' },
// { remede: 'ENREGISTRER_EN_UTF8' }.
new RegExp ( String . raw ` \b (?:raison|cause|remede) \s *: \s * ${ CODE } ` , 'g' ) ,
// Une constante qui nomme un code, que le module émet ensuite par elle :
// const RELU = 'RELECTURE_DIFFERENTE', puis { cause: RELU }.
new RegExp ( String . raw ` \b (?:const|let|var) \s +[A-Za-z_ $ ][ \w $ ]* \s *= \s * ${ CODE } ` , 'g' ) ,
] ;
/** Codes qu'un texte source émet, hors commentaires, dans l'ordre du texte. */
function codesEmis ( texte ) {
const net = sansCommentaires ( texte ) ;
return MOTIFS _EMISSION . flatMap ( ( motif ) => [ ... net . matchAll ( motif ) ] . map ( ( trouve ) => ( { indice : trouve . index , code : trouve [ 1 ] } ) ) )
. sort ( ( a , b ) => a . indice - b . indice )
. map ( ( { code } ) => code ) ;
}
/ * *
* Codes d ' une table de contrat : dans un commentaire de documentation qui
* s ' ouvre sur « Codes d … » , chaque ligne qui porte , trois espaces après
* l ' astérisque , un code en tête .
* /
function codesDesTables ( texte ) {
const codes = [ ] ;
for ( const [ bloc ] of texte . matchAll ( /\/\*\*[\s\S]*?\*\//g ) ) {
if ( ! /^\s*\*\s*Codes d/m . test ( bloc ) ) continue ;
for ( const [ , code ] of bloc . matchAll ( /^[ \t]*\*[ \t]{3}([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*)(?=\s|$)/gm ) ) codes . push ( code ) ;
}
return codes ;
}
// Modules d'un arbre, épreuves et table des textes exclues, triés.
function modulesDe ( arbre ) {
return readdirSync ( join ( RACINE , arbre ) , { recursive : true } )
. map ( ( nom ) => join ( arbre , nom ) )
. filter ( ( chemin ) => chemin . endsWith ( '.js' ) && ! /\.test\.js$/ . test ( chemin ) && chemin !== TABLE _DES _TEXTES )
. sort ( ) ;
}
// Relevé de l'arbre réel : par arbre, les codes émis et le fichier qui émet
// chacun en premier ; puis ceux des tables de contrat.
function releverArbre ( ) {
const parArbre = new Map ( ) ;
const emetteurs = new Map ( ) ;
for ( const arbre of ARBRES ) {
const codes = new Set ( ) ;
for ( const chemin of modulesDe ( arbre ) ) {
for ( const code of codesEmis ( readFileSync ( join ( RACINE , chemin ) , 'utf8' ) ) ) {
codes . add ( code ) ;
if ( ! emetteurs . has ( code ) ) emetteurs . set ( code , relative ( '.' , chemin ) ) ;
}
}
parArbre . set ( arbre , codes ) ;
}
const tables = new Set ( TABLES . flatMap ( ( chemin ) => codesDesTables ( readFileSync ( join ( RACINE , chemin ) , 'utf8' ) ) ) ) ;
for ( const code of tables ) if ( ! emetteurs . has ( code ) ) emetteurs . set ( code , 'table de contrat' ) ;
return { parArbre , tables , emetteurs } ;
}
describe ( 'libellés : le balayage des codes (§ 14.6, § 14.2)' , ( ) => {
test ( "le relevé lit le premier argument d'un appel, une clé code, un code de détail, et rien d'un commentaire" , ( ) => {
const source = [
"// throw new ErreurStockage('DANS_UN_COMMENTAIRE');" ,
"/** refuser('DANS_UN_BLOC') */" ,
"throw new ErreurStockage('ABSENT', { chemin });" ,
"const refus = refuser(\n 'UTF8_INVALIDE');" ,
"avertissements.push({ code: 'DERIVE', details });" ,
"throw new ErreurStockage('LECTURE_SEULE', { raison: 'BLOQUE' });" ,
"const texte = brut.normalize('NFC');" ,
"const url = 'http://exemple.test'; ajouter('APRES_UNE_ADRESSE');" ,
"if (erreur.code === 'ECARTE') return 'PAS_UN_APPEL';" ,
"const minuscule = verifier('minuscule');" ,
"const RELU = 'CONSTANTE_CODE'; const motif = raisonLue ?? 'PAS_UNE_CONSTANTE';" ,
] . join ( '\n' ) ;
assert . deepEqual ( codesEmis ( source ) , [
'ABSENT' ,
'UTF8_INVALIDE' ,
'DERIVE' ,
'LECTURE_SEULE' ,
'BLOQUE' ,
'APRES_UNE_ADRESSE' ,
'CONSTANTE_CODE' ,
] ) ;
} ) ;
test ( "le relevé des tables lit chaque ligne de code d'un bloc « Codes d… », et ignore les autres blocs" , ( ) => {
const source = [
'/**' ,
" * Codes d'une erreur d'essai :" ,
' *' ,
' * PREMIER {chemin} un premier code' ,
' * qui continue ici' ,
' * SECOND_CODE' ,
' * {} un second' ,
' */' ,
'/**' ,
' * Autre chose :' ,
' * IGNORE {}' ,
' */' ,
] . join ( '\n' ) ;
assert . deepEqual ( codesDesTables ( source ) , [ 'PREMIER' , 'SECOND_CODE' ] ) ;
} ) ;
[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
test ( "un motif de lecture seule ou d'illisibilité ne fait jamais lever le texte ni n'y laisse « undefined », quel que soit le code qui le porte" , ( ) => {
assert . ok ( CODES . length > 0 , 'table vide' ) ;
for ( const code of [ 'LECTURE_SEULE' , 'ETAT_ILLISIBLE' , 'JOURNAL_ILLISIBLE' ] ) {
for ( const raison of [ ... CODES , 'RAISON_INVENTEE' ] ) {
const texte = libelle ( code , { raison , base : 'Soirée des Lucioles' , chemin : null } ) ;
assert . equal ( typeof texte , 'string' , ` ${ code } / ${ raison } ` ) ;
assert . ok ( ! texte . includes ( 'undefined' ) , ` ${ code } / ${ raison } : ${ texte } ` ) ;
}
}
} ) ;
[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
test ( 'chaque arbre émet des codes, et la table de contrat du stockage en porte' , ( ) => {
const { parArbre , tables } = releverArbre ( ) ;
for ( const arbre of ARBRES ) assert . ok ( parArbre . get ( arbre ) . size > 0 , ` ${ arbre } : balayage vide ` ) ;
assert . ok ( tables . size > 0 , 'tables de contrat : balayage vide' ) ;
assert . ok ( tables . has ( 'VERROU_PRIS' ) && tables . has ( 'IDENTIFIANT_HORS_COMPTEUR' ) ) ;
} ) ;
test ( 'chaque code que src/stockage, src/csv et src/application émettent a son texte' , ( ) => {
const { parArbre , tables , emetteurs } = releverArbre ( ) ;
const releves = new Set ( [ ... tables , ... ARBRES . flatMap ( ( arbre ) => [ ... parArbre . get ( arbre ) ] ) ] ) ;
assert . ok ( releves . size > 0 , 'balayage vide' ) ;
const connus = new Set ( CODES ) ;
const sansTexte = [ ... releves ] . filter ( ( code ) => ! connus . has ( code ) ) . sort ( ) ;
assert . deepEqual (
sansTexte . map ( ( code ) => ` ${ code } ( ${ emetteurs . get ( code ) } ) ` ) ,
[ ] ,
'codes émis sans texte dans src/application/libelles.js' ,
) ;
} ) ;
test ( 'chaque texte de la table a un émetteur : aucune entrée morte' , ( ) => {
const { parArbre , tables } = releverArbre ( ) ;
const releves = new Set ( [ ... tables , ... ARBRES . flatMap ( ( arbre ) => [ ... parArbre . get ( arbre ) ] ) ] ) ;
assert . ok ( CODES . length > 0 , 'table vide' ) ;
assert . deepEqual ( CODES . filter ( ( code ) => ! releves . has ( code ) ) , [ ] , 'textes sans émetteur' ) ;
} ) ;
} ) ;
// --- Les textes -----------------------------------------------------------
// Détails d'exemple de chaque code de la table : chaque texte s'y compose une
// fois. Un code ajouté à la table sans exemple fait échouer l'épreuve.
const EXEMPLES = new Map ( [
// Système de fichiers et dépôt
[ 'ABSENT' , { chemin : 'Soirée des Lucioles.gtt.json' } ] ,
[ 'ECRITURE' , { chemin : 'Soirée des Lucioles.gtt.json' , dossier : 'E:\\data' , cause : 'EROFS' } ] ,
[ 'LECTURE' , { chemin : 'Soirée des Lucioles.gtt.json' , dossier : 'E:\\data' , cause : 'EACCES' } ] ,
[ 'EXISTE' , { chemin : 'Soirée des Lucioles.gtt.json' } ] ,
[ 'CHEMIN_REFUSE' , { chemin : '../ailleurs.gtt.json' } ] ,
[ 'ETAT_ILLISIBLE' , { base : 'Soirée des Lucioles' , raison : 'JSON' , chemin : null , secours : { precedent : 4 , journal : 5 } } ] ,
[ 'CORRECTIF' , { rang : 2 } ] ,
[ 'CHEMIN_TROP_LONG' , { racine : 'E:\\un\\dossier\\profond' } ] ,
[ 'IDENTIFIANT_PRESENT' , { id : 'evt-lucioles' , base : 'Soirée des Lucioles' } ] ,
[ 'VERROU_PRIS' , { seance : 'seance-autre' , depuis : '2031-05-17T08:05:00-04:00' , vivant : true } ] ,
[ 'LECTURE_SEULE' , { raison : 'BLOQUE' } ] ,
[ 'ETAT_NON_ECRIT' , { chemin : 'Soirée des Lucioles.gtt.json' , dossier : 'E:\\data' , cause : 'EIO' } ] ,
[ 'CORBEILLE_SATUREE' , { dossier : 'corbeille/2031-05-17_08-05-00_99' } ] ,
[ 'COMPTEUR_SATURE' , { compteur : 'proposition' } ] ,
[ 'NON_DISPONIBLE' , { primitive : 'ouvrirDansExplorateur' } ] ,
// Raisons d'un état illisible
[ 'VIDE' , { } ] ,
[ 'JSON' , { } ] ,
[ 'FORME' , { chemin : 'charge.participants[2].nom' } ] ,
[ 'COMPTES' , { chemin : 'entete.comptes.tables' } ] ,
[ 'REFERENCE' , { chemin : 'charge.reservations[0].table' } ] ,
[ 'FORMAT_INCONNU' , { chemin : 'entete.format' } ] ,
[ 'FORMAT_PLUS_RECENT' , { format : 2 } ] ,
[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
[ 'ENCODAGE' , { } ] ,
[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
// Fautes d'un placement
[ 'LONGUEUR' , { tour : 2 , table : 4 , declare : 8 , ecrit : 9 } ] ,
[ 'DOUBLON' , { tour : 1 , participant : 3 } ] ,
[ 'INCONNU' , { tour : 1 , participant : 12 } ] ,
[ 'MANQUANT' , { tour : 2 , participant : 7 } ] ,
[ 'IDENTIFIANT_REPETE' , { } ] ,
[ 'IDENTIFIANT_HORS_COMPTEUR' , { } ] ,
// Raisons de dérive
[ 'PARTICIPANT_EXCLU' , { participant : 3 } ] ,
[ 'PARTICIPANT_SUPPRIME' , { participant : 4 } ] ,
[ 'PARTICIPANT_NON_PLACE' , { participant : 9 } ] ,
[ 'TABLE_SUPPRIMEE' , { table : 2 } ] ,
[ 'TABLE_AJOUTEE' , { table : 5 } ] ,
[ 'CAPACITE_CHANGEE' , { table : 1 , avant : 8 , maintenant : 6 } ] ,
[ 'TOURS_CHANGES' , { avant : 4 , maintenant : 3 } ] ,
// Avertissements de l'ouverture
[ 'RETENU_FAUTIF' , { } ] ,
[ 'JOURNAL_REPRIS' , { } ] ,
[ 'JOURNAL_EN_AVANCE_DE' , { n : 3 } ] ,
[ 'JOURNAL_INCOMPLET' , { etat : 9 , journal : 6 } ] ,
[ 'JOURNAL_ECARTEES' , { n : 64 } ] ,
[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
[ 'JOURNAL_RECREE' , { } ] ,
[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
[ 'JOURNAL_FORMAT_PLUS_RECENT' , { format : 2 } ] ,
[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
[ 'JOURNAL_ILLISIBLE' , { raison : 'ENCODAGE' } ] ,
[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
[ 'JOURNAL_ETRANGER' , { evenement : 'evt-autre' } ] ,
[ 'PROPOSITIONS_FAUTIVES' , { ids : [ 3 , null ] } ] ,
[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
[ 'PROPOSITIONS_MANQUANTES' , { n : 2 } ] ,
[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
[ 'DERIVE' , { ids : [ 1 , 2 ] , retenu : true } ] ,
// Dossier de travail et réglages locaux
[ 'NON_PUBLIE' , { } ] ,
[ 'DONNEES_APPLICATIVES' , { } ] ,
[ 'SONDE_ECHOUEE' , { cause : 'EROFS' } ] ,
[ 'RELECTURE_DIFFERENTE' , { } ] ,
[ 'PORTABLE_NON_INSCRIPTIBLE' , { racine : { id : 'portable' , chemin : 'E:\\data' } , evenements : 3 , cause : 'EROFS' } ] ,
[ 'DOCUMENTS_NON_INSCRIPTIBLE' , { racine : { id : 'documents' , chemin : 'C:\\Documents\\Table' } , evenements : 0 , cause : 'EACCES' } ] ,
[ 'PLATEFORME_WEB' , { } ] ,
[ 'SUPPORT_AMOVIBLE' , { } ] ,
[ 'REGLAGES_ILLISIBLES' , { chemin : 'reglages_locaux.json' , raison : 'FORME' } ] ,
// CSV
[ 'UTF16_SANS_MARQUE' , { remede : 'ENREGISTRER_EN_UTF8' } ] ,
[ 'UTF16_INVALIDE' , { remede : 'ENREGISTRER_EN_UTF8' } ] ,
[ 'UTF8_INVALIDE' , { remede : 'ENREGISTRER_EN_UTF8' } ] ,
[ 'CARACTERE_NUL' , { remede : 'ENREGISTRER_EN_UTF8' } ] ,
[ 'ENREGISTRER_EN_UTF8' , { } ] ,
[ 'SEPARATEUR_INTROUVABLE' , { separateur : ';' , ligne : 3 , cause : 'NOMBRE_DE_CHAMPS' } ] ,
[ 'NOMBRE_DE_CHAMPS' , { ligne : 3 } ] ,
[ 'GUILLEMET_OUVERT' , { ligne : 2 } ] ,
[ 'AUCUN_ENTETE' , { } ] ,
[ 'NOM_NON_ASSOCIE' , { } ] ,
[ 'AUCUNE_LIGNE_VALIDE' , { } ] ,
[ 'CHAMPS_EN_TROP' , { ligne : 4 , valeur : null } ] ,
[ 'NOM_ABSENT' , { ligne : 5 , valeur : null } ] ,
[ 'EXCLU_INCONNU' , { ligne : 6 , valeur : 'peut-être' } ] ,
[ 'PLUSIEURS_CORRESPONDENT' , { ligne : 7 , valeur : null , participants : [ 3 , 8 ] } ] ,
[ 'DEJA_DESIGNE' , { ligne : 9 , valeur : null , participants : [ 3 ] , premiereLigne : 4 } ] ,
[ 'PLAN_BLOQUE' , { remede : { geste : 'debloquer' } } ] ,
[ 'AMBIGUITE' , { champs : [ 'appartenance' , 'courriel' ] } ] ,
// Séance
[ 'AUCUN_EVENEMENT' , { } ] ,
[ 'MODE_LECTURE' , { } ] ,
[ 'FORMAT' , { } ] ,
[ 'BLOQUE' , { } ] ,
[ 'SUPPORT_ROMPU' , { echec : 'ECRITURE' , chemin : 'Soirée des Lucioles.gtt-journal.jsonl' , dossier : 'E:\\data' , cause : 'EROFS' } ] ,
[ 'SUPPORT_INTACT' , { } ] ,
[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
[ 'SUPPORT_ROMPU_NON_SAUVE' , { echec : 'ETAT_NON_ECRIT' , chemin : 'Soirée des Lucioles.gtt.json' , dossier : 'E:\\data' , cause : 'EIO' } ] ,
[ 'EVENEMENT_CHANGE' , { revision : 7 } ] ,
[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
[ 'VERROU_REPRIS' , { seance : 'seance-morte' , depuis : '2031-05-01T21:40:00-04:00' } ] ,
[ 'VERROU_NON_RENDU' , { chemin : 'Soirée des Lucioles.gtt.verrou' , dossier : 'E:\\data' , cause : 'EROFS' } ] ,
[ 'RIEN_A_DEFAIRE' , { } ] ,
[ 'RIEN_A_REFAIRE' , { } ] ,
[ 'INSTANT_NON_RESTITUABLE' , { revision : 3 } ] ,
[ 'PLAN_NON_BLOQUE' , { remede : null } ] ,
[ 'JALON_NON_ECRIT' , { chemin : 'Soirée des Lucioles.gtt-journal.jsonl' , dossier : 'E:\\data' , cause : 'EROFS' } ] ,
[ 'SANS_EFFET' , { commande : 'modifierParticipant' , remede : null } ] ,
// Refus et avertissements des commandes
[ 'NOM_REQUIS' , { remede : null } ] ,
[ 'DATE_INVALIDE' , { date : '2031-02-30' , remede : null } ] ,
[ 'SIEGES_INVALIDES' , { sieges : 1 , remede : null } ] ,
[ 'TOURS_INVALIDES' , { tours : 0 , remede : null } ] ,
[ 'PARTICIPANT_INCONNU' , { id : 42 , remede : null } ] ,
[ 'PROPOSITION_INCONNUE' , { id : 17 , remede : null } ] ,
[ 'AUCUNE_PROPOSITION' , { remede : { geste : 'ouvrirSection' , section : 'generation' } } ] ,
[ 'AUCUN_RETENU' , { remede : { geste : 'ouvrirSection' , section : 'propositions' } } ] ,
[ 'RESERVATIONS_SUSPENDUES' , { participant : 1 , personne : 'Iris Ombrelle' , reservations : 2 , titres : 1 } ] ,
[ 'RESERVATIONS_RETABLIES' , { participant : 1 , personne : 'Iris Ombrelle' , reservations : 2 , titres : 1 } ] ,
[ 'RESERVATIONS_RETIREES' , { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } ] ,
[ 'IMPORT_EN_RETENU' , { } ] ,
// Libellés figés du journal
[ 'ENTREE_CREATION' , { nom : "Soirée d'essai" } ] ,
[ 'JALON_CREATION' , { } ] ,
[ 'JALON_ETAT' , { etat : 'retenu' } ] ,
[ 'JALON_AVANT_GENERATION' , { } ] ,
[ 'ENTREE_REPRISE' , { } ] ,
[ 'ENTREE_DEFAIRE' , { libelle : 'Ajouté Théo Pervenche' } ] ,
[ 'ENTREE_REFAIRE' , { libelle : 'Ajouté Théo Pervenche' } ] ,
[ 'ENTREE_REVENIR' , { libelle : 'Ajouté Iris Ombrelle' } ] ,
[ 'ENTREE_AJOUTER_PARTICIPANT' , { personne : 'Iris Ombrelle' } ] ,
[ 'ENTREE_MODIFIER_PARTICIPANT' , { personne : 'Iris Ombrelle' , champs : [ 'courriel' ] , devenu : null } ] ,
[ 'ENTREE_EXCLURE_PARTICIPANT' , { personne : 'Iris Ombrelle' } ] ,
[ 'ENTREE_REINTEGRER_PARTICIPANT' , { personne : 'Iris Ombrelle' } ] ,
[ 'ENTREE_SUPPRIMER_PARTICIPANT' , { personne : 'Iris Ombrelle' } ] ,
[ 'ENTREE_IMPORTER_PARTICIPANTS' , { mode : 'ajouter' , ajoutes : 12 , misAJour : 0 , retires : 0 } ] ,
[ 'ENTREE_CHANGER_ETAT' , { etat : 'bloque' } ] ,
[ 'ENTREE_DEBLOQUER' , { etat : 'retenu' } ] ,
[ 'ENTREE_ENREGISTRER_GENERATION' , { n : 5 , premier : 3 , dernier : 7 } ] ,
[ 'ENTREE_RETENIR_PROPOSITION' , { id : 3 } ] ,
[ 'ENTREE_EFFACER_PROPOSITIONS' , { n : 4 , gardee : 2 } ] ,
] ) ;
// Un texte composé ne laisse paraître ni valeur absente, ni booléen, ni
// objet : chaque trace est un mot entier, « nulle part » n'en est pas une.
const TRACES = [ /\bundefined\b/ , /\bnull\b/ , /\bNaN\b/ , /\b(?:true|false)\b/ , /\[object / ] ;
describe ( 'libellés : les textes (§ 14.6, § 8.2)' , ( ) => {
test ( 'un code inconnu lève, et le message nomme le code' , ( ) => {
assert . throws ( ( ) => libelle ( 'CODE_QUI_N_EXISTE_PAS' , { } ) , ( erreur ) => {
assert . ok ( erreur instanceof RangeError ) ;
assert . match ( erreur . message , /CODE_QUI_N_EXISTE_PAS/ ) ;
return true ;
} ) ;
assert . throws ( ( ) => libelle ( undefined ) , RangeError ) ;
} ) ;
test ( "chaque texte de la table se compose de ses détails d'exemple" , ( ) => {
assert . ok ( CODES . length > 0 , 'table vide' ) ;
assert . deepEqual ( CODES . filter ( ( code ) => ! EXEMPLES . has ( code ) ) , [ ] , "codes de la table sans détails d'exemple" ) ;
assert . deepEqual ( [ ... EXEMPLES . keys ( ) ] . filter ( ( code ) => ! CODES . includes ( code ) ) , [ ] , 'exemples sans code dans la table' ) ;
for ( const code of CODES ) {
const texte = libelle ( code , EXEMPLES . get ( code ) ) ;
assert . equal ( typeof texte , 'string' , code ) ;
assert . ok ( texte . trim ( ) . length > 0 , ` ${ code } : texte vide ` ) ;
assert . notEqual ( texte , code , ` ${ code } : le code brut tient lieu de texte ` ) ;
for ( const trace of TRACES ) assert . doesNotMatch ( texte , trace , ` ${ code } : « ${ texte } » ` ) ;
}
} ) ;
test ( 'la table est triée, sans doublon, et figée' , ( ) => {
assert . deepEqual ( [ ... CODES ] , [ ... new Set ( CODES ) ] . sort ( ( a , b ) => ( a < b ? - 1 : a > b ? 1 : 0 ) ) ) ;
assert . ok ( Object . isFrozen ( CODES ) ) ;
} ) ;
test ( 'les libellés figés des exemples du contrat, détails interpolés' , ( ) => {
assert . equal ( libelle ( 'ENTREE_CREATION' , { nom : "Soirée d'essai" } ) , "Création de l'événement « Soirée d'essai »" ) ;
assert . equal ( libelle ( 'JALON_CREATION' ) , 'Création' ) ;
assert . equal ( libelle ( 'ENTREE_AJOUTER_PARTICIPANT' , { personne : 'Iris Ombrelle' } ) , 'Ajouté Iris Ombrelle' ) ;
assert . equal (
libelle ( 'ENTREE_MODIFIER_PARTICIPANT' , { personne : 'Ondine Lacasse' , champs : [ 'courriel' ] , devenu : null } ) ,
'Modifié Ondine Lacasse : courriel' ,
) ;
assert . equal (
libelle ( 'ENTREE_MODIFIER_PARTICIPANT' , { personne : 'Iris Ombrelle' , champs : [ 'nom' , 'titrePressenti' ] , devenu : 'Iris Parasol' } ) ,
'Modifié Iris Ombrelle : nom, titre pressenti (désormais Iris Parasol)' ,
) ;
assert . equal ( libelle ( 'ENTREE_REVENIR' , { libelle : 'Ajouté Iris Ombrelle' } ) , 'Retour à « Ajouté Iris Ombrelle »' ) ;
assert . equal ( libelle ( 'ENTREE_DEFAIRE' , { libelle : 'Ajouté Théo Pervenche' } ) , 'Défait : « Ajouté Théo Pervenche »' ) ;
assert . equal ( libelle ( 'ENTREE_REFAIRE' , { libelle : 'Ajouté Théo Pervenche' } ) , 'Refait : « Ajouté Théo Pervenche »' ) ;
assert . equal ( libelle ( 'ENTREE_REPRISE' ) , "Reprise de l'état trouvé sur le disque" ) ;
assert . equal ( libelle ( 'JALON_ETAT' , { etat : 'propose' } ) , 'État « Proposé »' ) ;
assert . equal ( libelle ( 'ENTREE_CHANGER_ETAT' , { etat : 'bloque' } ) , "Plan passé à l'état « Bloqué »" ) ;
assert . equal ( libelle ( 'ENTREE_ENREGISTRER_GENERATION' , { n : 5 , premier : 3 , dernier : 7 } ) , 'Généré 5 propositions (n° 3 à 7)' ) ;
assert . equal ( libelle ( 'ENTREE_ENREGISTRER_GENERATION' , { n : 1 , premier : 4 , dernier : 4 } ) , 'Généré 1 proposition (n° 4)' ) ;
assert . equal ( libelle ( 'ENTREE_IMPORTER_PARTICIPANTS' , { mode : 'ajouter' , ajoutes : 1 , misAJour : 0 , retires : 0 } ) , 'Importé 1 participant' ) ;
assert . equal (
libelle ( 'ENTREE_IMPORTER_PARTICIPANTS' , { mode : 'remplacer' , ajoutes : 250 , misAJour : 0 , retires : 260 } ) ,
'Remplacé la liste par import : 260 retirés, 250 ajoutés' ,
) ;
} ) ;
test ( 'le séparateur introuvable nomme la ligne et la cause ; le guillemet ouvert tombe dès la ligne 2' , ( ) => {
const nombre = libelle ( 'SEPARATEUR_INTROUVABLE' , { separateur : ';' , ligne : 3 , cause : 'NOMBRE_DE_CHAMPS' } ) ;
assert . match ( nombre , /la ligne 3 n'a pas le nombre de champs de l'en-tête/ ) ;
assert . match ( libelle ( 'SEPARATEUR_INTROUVABLE' , { ligne : 3 } ) , /la ligne 3 n'a pas le nombre de champs de l'en-tête/ ) ;
assert . match ( libelle ( 'SEPARATEUR_INTROUVABLE' , { separateur : ',' , ligne : 2 , cause : 'GUILLEMET_OUVERT' } ) , /ligne 2/ ) ;
assert . match ( libelle ( 'GUILLEMET_OUVERT' , { ligne : 2 } ) , /ligne 2/ ) ;
assert . doesNotMatch ( libelle ( 'SEPARATEUR_INTROUVABLE' , { } ) , /ligne/ ) ;
} ) ;
test ( "les quatre refus d'encodage nomment le même remède : réenregistrer en UTF-8" , ( ) => {
const remede = libelle ( 'ENREGISTRER_EN_UTF8' ) ;
assert . match ( remede , /UTF-8/ ) ;
for ( const code of [ 'UTF16_SANS_MARQUE' , 'UTF16_INVALIDE' , 'UTF8_INVALIDE' , 'CARACTERE_NUL' ] ) {
const texte = libelle ( code , { remede : 'ENREGISTRER_EN_UTF8' } ) ;
assert . ok ( texte . endsWith ( remede ) , ` ${ code } : « ${ texte } » ` ) ;
}
} ) ;
[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
test ( "le support amovible nomme le dossier quand il est donné ; l'événement changé nomme la révision adoptée" , ( ) => {
assert . doesNotMatch ( libelle ( 'SUPPORT_AMOVIBLE' , { } ) , /«/ ) ;
assert . match ( libelle ( 'SUPPORT_AMOVIBLE' , { dossier : 'F:\\soirees' } ) , /« F:\\soirees »/ ) ;
assert . match ( libelle ( 'EVENEMENT_CHANGE' , { revision : 7 } ) , /\b7\b/ ) ;
} ) ;
test ( "quitter un événement au support rompu nomme « Écrire ailleurs », et ce qui serait perdu" , ( ) => {
const details = { chemin : 'Soirée des Lucioles.gtt-journal.jsonl' , dossier : 'E:\\data' , cause : 'EROFS' } ;
for ( const echec of [ 'ECRITURE' , 'ETAT_NON_ECRIT' , 'JALON_NON_ECRIT' , 'VERROU_NON_ECRIT' ] ) {
assert . match ( libelle ( 'SUPPORT_ROMPU_NON_SAUVE' , { echec , ... details } ) , /« Écrire ailleurs »/ , echec ) ;
}
} ) ;
[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
test ( 'le plan bloqué nomme « Débloquer », le mode lecture nomme « Modifier »' , ( ) => {
assert . match ( libelle ( 'LECTURE_SEULE' , { raison : 'BLOQUE' } ) , /« Débloquer »/ ) ;
assert . match ( libelle ( 'PLAN_BLOQUE' , { remede : { geste : 'debloquer' } } ) , /« Débloquer »/ ) ;
assert . match ( libelle ( 'MODE_LECTURE' ) , /« Modifier »/ ) ;
assert . doesNotMatch ( libelle ( 'LECTURE_SEULE' , { raison : 'FORMAT' } ) , /Débloquer/ ) ;
for ( const trace of TRACES ) assert . doesNotMatch ( libelle ( 'LECTURE_SEULE' , { raison : 'FORMAT' } ) , trace ) ;
assert . equal ( libelle ( 'LECTURE_SEULE' , { raison : 'FORMAT' } ) , libelle ( 'LECTURE_SEULE' , { raison : 'FORMAT_PLUS_RECENT' } ) ) ;
} ) ;
test ( "un état illisible nomme sa raison et ses secours ; chaque raison du contrat a son texte" , ( ) => {
const texte = libelle ( 'ETAT_ILLISIBLE' , { base : 'Soirée des Lucioles' , raison : 'VIDE' , chemin : null , secours : { precedent : 4 , journal : null } } ) ;
assert . match ( texte , /« Soirée des Lucioles »/ ) ;
assert . match ( texte , new RegExp ( libelle ( 'VIDE' ) ) ) ;
assert . match ( texte , /révision 4/ ) ;
const sansSecours = libelle ( 'ETAT_ILLISIBLE' , { base : 'Soirée des Lucioles' , raison : 'JSON' , chemin : null , secours : { precedent : null , journal : null } } ) ;
assert . match ( sansSecours , /Aucune version de secours/ ) ;
for ( const raison of [ 'ABSENT' , 'VIDE' , 'JSON' , 'FORME' , 'COMPTES' , 'REFERENCE' , 'FORMAT_INCONNU' , 'FORMAT_PLUS_RECENT' ] ) {
const details = { base : 'Soirée des Lucioles' , raison , chemin : raison === 'FORME' ? 'charge.tables[0].forme' : null , secours : { precedent : null , journal : 2 } } ;
assert . match ( libelle ( 'ETAT_ILLISIBLE' , details ) , /révision 2/ , raison ) ;
}
[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 encodage = libelle ( 'ETAT_ILLISIBLE' , { base : 'Soirée des Lucioles' , raison : 'ENCODAGE' , chemin : null , secours : { precedent : 4 , journal : null } } ) ;
assert . ok ( encodage . includes ( libelle ( 'ENCODAGE' ) ) , encodage ) ;
} ) ;
test ( "une raison qu'un texte ne connaît pas ne lève jamais : une phrase générique la nomme, par son texte quand la table en porte un, telle quelle sinon" , ( ) => {
const inventee = libelle ( 'ETAT_ILLISIBLE' , { base : 'x' , raison : 'RAISON_INVENTEE' , chemin : null } ) ;
assert . match ( inventee , /« RAISON_INVENTEE »/ ) ;
const seule = libelle ( 'LECTURE_SEULE' , { raison : 'RAISON_INVENTEE' } ) ;
assert . match ( seule , /« RAISON_INVENTEE »/ ) ;
assert . match ( seule , /lecture seule/ ) ;
const parSonTexte = libelle ( 'LECTURE_SEULE' , { raison : 'VIDE' } ) ;
assert . ok ( parSonTexte . includes ( libelle ( 'VIDE' ) ) , parSonTexte ) ;
assert . doesNotMatch ( parSonTexte , /VIDE/ ) ;
assert . match ( libelle ( 'JOURNAL_ILLISIBLE' , { raison : 'RAISON_INVENTEE' } ) , /« RAISON_INVENTEE »/ ) ;
for ( const texte of [ inventee , seule , parSonTexte ] ) for ( const trace of TRACES ) assert . doesNotMatch ( texte , trace ) ;
} ) ;
test ( "un état aux octets hors UTF-8 se dit par son encodage : en lecture seule, il nomme le secours qui le met de côté ; le journal illisible, l'historique laissé tel quel" , ( ) => {
const seule = libelle ( 'LECTURE_SEULE' , { raison : 'ENCODAGE' } ) ;
assert . ok ( seule . includes ( libelle ( 'ENCODAGE' ) ) , seule ) ;
assert . match ( seule , /secours/ ) ;
assert . match ( seule , /corbeille/ ) ;
assert . notEqual ( seule , libelle ( 'LECTURE_SEULE' , { raison : 'FORMAT' } ) ) ;
const journal = libelle ( 'JOURNAL_ILLISIBLE' , { raison : 'ENCODAGE' } ) ;
assert . ok ( journal . includes ( libelle ( 'ENCODAGE' ) ) , journal ) ;
assert . match ( libelle ( 'JOURNAL_RECREE' ) , /historique/ ) ;
} ) ;
test ( 'les propositions manquantes se disent par leur nombre, au singulier comme au pluriel, sans jamais les énumérer' , ( ) => {
assert . match ( libelle ( 'PROPOSITIONS_MANQUANTES' , { n : 1 } ) , /^1 proposition que l'en-tête annonce manque/ ) ;
const enorme = libelle ( 'PROPOSITIONS_MANQUANTES' , { n : 2 * * 40 } ) ;
assert . match ( enorme , new RegExp ( ` ^ ${ 2 * * 40 } propositions que l'en-tête annonce manquent ` ) ) ;
assert . ok ( enorme . length < 200 , enorme ) ;
[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
} ) ;
test ( "une cause que la table porte se lit par son texte ; une cause du système s'affiche comme il la nomme" , ( ) => {
const avecCause = CODES . filter ( ( code ) => Object . hasOwn ( EXEMPLES . get ( code ) ? ? { } , 'cause' ) ) ;
assert . ok ( avecCause . length > 0 , 'aucun texte ne reçoit de cause' ) ;
for ( const code of avecCause ) {
const texte = libelle ( code , { ... EXEMPLES . get ( code ) , cause : 'RELECTURE_DIFFERENTE' } ) ;
assert . doesNotMatch ( texte , /RELECTURE_DIFFERENTE/ , ` ${ code } : « ${ texte } » ` ) ;
}
const relue = libelle ( 'RELECTURE_DIFFERENTE' ) ;
for ( const code of [ 'ECRITURE' , 'SONDE_ECHOUEE' , 'PORTABLE_NON_INSCRIPTIBLE' , 'DOCUMENTS_NON_INSCRIPTIBLE' ] ) {
const details = EXEMPLES . get ( code ) ;
assert . ok ( libelle ( code , { ... details , cause : 'RELECTURE_DIFFERENTE' } ) . includes ( ` ( ${ relue } ) ` ) , code ) ;
assert . ok ( libelle ( code , { ... details , cause : 'TypeMismatchError' } ) . includes ( '(TypeMismatchError)' ) , code ) ;
}
} ) ;
test ( "le support rompu dit ce que l'écriture refusée laisse sur le disque ; un échec inconnu lève" , ( ) => {
const refus = { chemin : 'Soirée des Lucioles.gtt-journal.jsonl' , dossier : 'E:\\data' , cause : 'EROFS' } ;
const textes = [ 'ECRITURE' , 'ETAT_NON_ECRIT' , 'JALON_NON_ECRIT' , 'VERROU_NON_ECRIT' ] . map ( ( echec ) =>
libelle ( 'SUPPORT_ROMPU' , { echec , ... refus } ) ,
) ;
for ( const texte of textes ) {
assert . ok ( texte . includes ( '« Soirée des Lucioles.gtt-journal.jsonl » dans le dossier « E:\\data » (EROFS)' ) , texte ) ;
assert . match ( texte , /« Écrire ailleurs »/ ) ;
}
assert . equal ( new Set ( textes ) . size , 4 ) ;
const [ ecriture , etat , jalon , verrou ] = textes ;
assert . match ( ecriture , /le dernier geste n'est pas enregistré/ ) ;
assert . match ( etat , /le dernier geste est inscrit à l'historique, mais pas dans l'état/ ) ;
assert . match ( jalon , /le dernier geste est enregistré, mais pas le jalon qui le nomme/i ) ;
for ( const texte of [ jalon , verrou ] ) assert . doesNotMatch ( texte , /n'est pas enregistré|ne se correspondent plus/ ) ;
assert . throws ( ( ) => libelle ( 'SUPPORT_ROMPU' , { echec : 'ECHEC_INVENTE' , ... refus } ) , RangeError ) ;
} ) ;
test ( 'un verrou pris dit depuis quand, et distingue la séance vivante, inconnue ou morte' , ( ) => {
const vivant = libelle ( 'VERROU_PRIS' , { seance : 's' , depuis : '2031-05-01T21:40:00-04:00' , vivant : true } ) ;
assert . match ( vivant , /1er mai 2031 à 21 h 40/ ) ;
const inconnu = libelle ( 'VERROU_PRIS' , { seance : null , depuis : null , vivant : null } ) ;
assert . notEqual ( inconnu , vivant ) ;
assert . notEqual ( libelle ( 'VERROU_PRIS' , { seance : 's' , depuis : '2031-05-01T21:40:00-04:00' , vivant : false } ) , vivant ) ;
} ) ;
test ( 'un état, un champ, un mode inconnus lèvent au lieu de se taire' , ( ) => {
assert . throws ( ( ) => libelle ( 'JALON_ETAT' , { etat : 'archive' } ) , RangeError ) ;
assert . throws ( ( ) => libelle ( 'ENTREE_MODIFIER_PARTICIPANT' , { personne : 'x' , champs : [ 'age' ] , devenu : null } ) , RangeError ) ;
assert . throws ( ( ) => libelle ( 'ENTREE_IMPORTER_PARTICIPANTS' , { mode : 'fusionner' , ajoutes : 1 , misAJour : 0 , retires : 0 } ) , RangeError ) ;
} ) ;
test ( 'les comptes accordent le singulier et le pluriel' , ( ) => {
assert . match ( libelle ( 'JOURNAL_ECARTEES' , { n : 1 } ) , /^1 entrée de l'historique est écartée/ ) ;
assert . match ( libelle ( 'JOURNAL_ECARTEES' , { n : 64 } ) , /^64 entrées de l'historique sont écartées/ ) ;
assert . equal ( libelle ( 'DERIVE' , { ids : [ ] , retenu : true } ) . startsWith ( 'Le placement retenu ne décrit plus' ) , true ) ;
assert . match ( libelle ( 'DERIVE' , { ids : [ 1 , 2 ] , retenu : true } ) , /^Le placement retenu et les propositions n° 1 et 2 ne décrivent plus/ ) ;
assert . match ( libelle ( 'PROPOSITIONS_FAUTIVES' , { ids : [ 3 , null ] } ) , /n° 3/ ) ;
} ) ;
} ) ;