gestion_table_tournante_libre/src/application/libelles.test.js

528 lines
29 KiB
JavaScript
Raw Normal View History

[ADD] application: session, named commands, French label table The session holds one event, opens it read-only and writes only after "Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a double click writes two entries in order. Every change is a pure named command with a frozen French label (§ 8.2); undo, redo and return write entries and never truncate the journal (§ 8.3). A failed write — a gesture, or the lock on a removed key — breaks the medium until the event is written elsewhere (§ 8.6). The label table covers every code storage, CSV and the session emit, and its test sweeps the sources so a new code needs a text. Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps. --- FR --- [ADD] application : séance, commandes nommées, table des libellés La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que « Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification est une commande nommée pure au libellé français figé (§ 8.2) ; défaire, refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre chaque code du stockage, du CSV et de la séance ; son épreuve balaie les sources, et un code neuf exige un texte. Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// É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`\bcode\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/);
});
});