[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 séance (§ 2.1, § 8.1 à § 8.6, § 8.8, § 9, § 10.1), toutes sur
// le système de fichiers d'épreuve, qui compte ses écritures, avec une
// horloge et un planificateur factices : rien n'attend le temps réel. Une
// ouverture n'écrit rien ; un geste écrit une entrée puis un état ; un refus
// n'écrit rien ; le verrou se prend à l'entrée en écriture et se rend au
// retour en lecture, explicite ou après l'inactivité ; le support qui rompt
// se remplace par un autre dossier. Les noms d'épreuve sont inventés ; la
// version vient du module engendré.
import assert from 'node:assert/strict' ;
import { readFileSync } from 'node:fs' ;
[FIX] local settings: an unreadable or unwritable file no longer blocks
An unreadable reglages_locaux.json, or a folder that refused to write it,
refused the paper choice, so no badge sheet or preview could be produced
on that machine. The chosen paper now holds for the session on that
working folder, in memory, and the file is never overwritten. On that
folder it wins over a stale paper still in the file; the first settings
write that succeeds there retains it and withdraws the warning. The
warning names the file and no longer promises defaults never written.
Checked: node 3918, browser 773; each fix red under the previous code.
--- FR ---
[FIX] réglages locaux : un fichier illisible ou non écrit ne bloque plus
Un reglages_locaux.json illisible, ou un dossier qui refusait de l'écrire,
refusait le choix du papier : ni planche ni aperçu sur cette machine. Le
papier choisi vaut désormais pour la séance sur ce dossier de travail, en
mémoire, et le fichier n'est jamais écrasé. Sur ce dossier, il l'emporte
sur un papier périmé du fichier ; la première écriture des réglages qui y
réussit le retient et retire l'avertissement. L'avertissement nomme le
fichier et ne promet plus de valeurs par défaut jamais écrites.
Vérifié : node 3918, navigateur 773 ; chaque correction rouge avant elle.
Assisted-by: Claude Opus 5.5
2026-10-10 20:20:04 -04:00
import { PanneSimulee , creerFichiersSimules } from '../../test/fichiers_simules.js' ;
[ADD] badges: prepare, produce and save the sheet from the session
The operator needs the badge sheet as a file next to the event, built
from what the session shows at that moment. The session prepares an
announcement (sheets per paper, counts, namesakes, missing glyphs, draft),
then produces the PDF through a single deferred import of the PDF code and
writes it atomically; under the web build it downloads instead. A gesture
started during production waits its turn. A proposal sheet is always a
draft, and an unwritable file or a missing paper choice gives its refusal
with a remedy.
Checked: red first; 3670 node, 707 browser and 101 long tests green from
the index alone.
--- FR ---
[ADD] badges : préparer, produire et déposer la planche depuis la séance
L'opérateur a besoin de la planche en fichier, à côté de l'événement, tirée
de ce que la séance montre à cet instant. La séance prépare une annonce
(planches par papier, comptes, homonymes, glyphes absents, brouillon), puis
produit le PDF par un seul import différé du code PDF et l'écrit de façon
atomique ; sous web, elle le télécharge. Un geste lancé pendant la
production attend son tour. Une planche tirée d'une proposition est
toujours un brouillon, et un fichier non écrit ou un papier non choisi
donnent leur refus avec un remède.
Vérifié : rouge d'abord ; 3670 node, 707 navigateur et 101 longues vertes
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 17:11:48 -04:00
import { H _RETENU , personne as personneBadge , soiree , table as tableBadge } from '../../test/fixtures/badges/cas.js' ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
import { describe , test } from '../../test/lanceur.js' ;
[ADD] badges: prepare, produce and save the sheet from the session
The operator needs the badge sheet as a file next to the event, built
from what the session shows at that moment. The session prepares an
announcement (sheets per paper, counts, namesakes, missing glyphs, draft),
then produces the PDF through a single deferred import of the PDF code and
writes it atomically; under the web build it downloads instead. A gesture
started during production waits its turn. A proposal sheet is always a
draft, and an unwritable file or a missing paper choice gives its refusal
with a remedy.
Checked: red first; 3670 node, 707 browser and 101 long tests green from
the index alone.
--- FR ---
[ADD] badges : préparer, produire et déposer la planche depuis la séance
L'opérateur a besoin de la planche en fichier, à côté de l'événement, tirée
de ce que la séance montre à cet instant. La séance prépare une annonce
(planches par papier, comptes, homonymes, glyphes absents, brouillon), puis
produit le PDF par un seul import différé du code PDF et l'écrit de façon
atomique ; sous web, elle le télécharge. Un geste lancé pendant la
production attend son tour. Une planche tirée d'une proposition est
toujours un brouillon, et un fichier non écrit ou un papier non choisi
donnent leur refus avec un remède.
Vérifié : rouge d'abord ; 3670 node, 707 navigateur et 101 longues vertes
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 17:11:48 -04:00
import { lirePolices } from '../../test/polices.js' ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
import { apercevoir } from '../csv/apercu.js' ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
import { nombrePlacesManquantes , normaliser } from '../moteur/configuration.js' ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
import { rechercher } from '../moteur/recherche.js' ;
import { serialiser } from '../stockage/canonique.js' ;
import { creer as creerDansLeDepot , ouvrir as ouvrirDansLeDepot } from '../stockage/depot.js' ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
import { FORMAT , analyser , configurationDepuisCharge , creerCharge , etatDeduit } from '../stockage/document.js' ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
import { ErreurStockage } from '../stockage/erreurs.js' ;
import { ligneEntree , ligneOuverture , lireJournal , reconstruire } from '../stockage/journal.js' ;
[ADD] badges: prepare, produce and save the sheet from the session
The operator needs the badge sheet as a file next to the event, built
from what the session shows at that moment. The session prepares an
announcement (sheets per paper, counts, namesakes, missing glyphs, draft),
then produces the PDF through a single deferred import of the PDF code and
writes it atomically; under the web build it downloads instead. A gesture
started during production waits its turn. A proposal sheet is always a
draft, and an unwritable file or a missing paper choice gives its refusal
with a remedy.
Checked: red first; 3670 node, 707 browser and 101 long tests green from
the index alone.
--- FR ---
[ADD] badges : préparer, produire et déposer la planche depuis la séance
L'opérateur a besoin de la planche en fichier, à côté de l'événement, tirée
de ce que la séance montre à cet instant. La séance prépare une annonce
(planches par papier, comptes, homonymes, glyphes absents, brouillon), puis
produit le PDF par un seul import différé du code PDF et l'écrit de façon
atomique ; sous web, elle le télécharge. Un geste lancé pendant la
production attend son tour. Une planche tirée d'une proposition est
toujours un brouillon, et un fichier non écrit ou un papier non choisi
donnent leur refus avec un remède.
Vérifié : rouge d'abord ; 3670 node, 707 navigateur et 101 longues vertes
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 17:11:48 -04:00
import { SUFFIXES , nomPlanche } from '../stockage/noms.js' ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
import { FICHIER _REGLAGES } from '../stockage/reglages_locaux.js' ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
import { VERSION } from '../version.genere.js' ;
import { libelle } from './libelles.js' ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
import { DELAI _REGLAGES _MS , INACTIVITE _MS , creerSeance } from './seance.js' ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
const V1 = VERSION . affichee ;
const DOCUMENTS = 'C:\\Users\\Exemple\\Documents\\Gestion table tournante Libre' ;
const DOC = { id : 'documents' , chemin : DOCUMENTS } ;
const PORTABLE = { id : 'portable' , chemin : 'E:\\soirees\\data' } ;
const BASE = 'Soirée des Lucioles' ;
const ETAT = ` ${ BASE } ${ SUFFIXES . etat } ` ;
const JOURNAL = ` ${ BASE } ${ SUFFIXES . journal } ` ;
const VERROU = ` ${ BASE } ${ SUFFIXES . verrou } ` ;
const CREATION = "Création de l'événement « Soirée des Lucioles »" ;
// --- Le montage -------------------------------------------------------------
const deux = ( valeur ) => String ( valeur ) . padStart ( 2 , '0' ) ;
// Horloge d'épreuve : une minute de plus à chaque lecture, à partir de 8 h le
// 17 mai 2031, à l'heure de l'Est.
function creerHorloge ( ) {
let n = 0 ;
return {
horodatage ( ) {
const minutes = 8 * 60 + n ;
n += 1 ;
return ` 2031-05-17T ${ deux ( Math . floor ( minutes / 60 ) ) } : ${ deux ( minutes % 60 ) } :00-04:00 ` ;
} ,
} ;
}
// Identifiants d'épreuve, numérotés dans l'ordre des tirages : le premier est
// celui de la séance. tirages les garde.
function creerIdentifiants ( prefixe ) {
const tirages = [ ] ;
const identifiant = ( ) => {
tirages . push ( ` ${ prefixe } - ${ tirages . length + 1 } ` ) ;
return tirages . at ( - 1 ) ;
} ;
identifiant . tirages = tirages ;
return identifiant ;
}
// Planificateur d'épreuve : rien ne part seul. avancer(ms) déclenche les
// minuteries échues, dans l'ordre de leur échéance puis de leur création, et
// attend ce que chacune rend.
function creerPlanificateur ( ) {
let maintenant = 0 ;
let suivant = 1 ;
const minuteries = new Map ( ) ;
return {
planifier ( fn , ms ) {
const id = suivant ;
suivant += 1 ;
minuteries . set ( id , { fn , echeance : maintenant + ms } ) ;
return id ;
} ,
annuler ( id ) {
minuteries . delete ( id ) ;
} ,
async avancer ( ms ) {
maintenant += ms ;
const echues = [ ... minuteries ]
. filter ( ( [ , { echeance } ] ) => echeance <= maintenant )
. sort ( ( [ a , m ] , [ b , n ] ) => m . echeance - n . echeance || a - b ) ;
for ( const [ id , { fn } ] of echues ) {
if ( ! minuteries . has ( id ) ) continue ;
minuteries . delete ( id ) ;
await fn ( ) ;
}
} ,
get actives ( ) {
return minuteries . size ;
} ,
} ;
}
// Une séance sur le système d'épreuve, sa racine créée : documents par
// défaut.
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// options : les paramètres de creerSeance qu'une épreuve règle en plus —
// formatCourant, delaiReglagesMs. Une racine null crée la séance sans
// dossier de travail.
function monter ( { fs = creerFichiersSimules ( ) , racine = DOC , prefixe = 'seance' , inactiviteMs , ... options } = { } ) {
if ( racine !== null ) fs . deposerDossier ( racine . id ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
const planificateur = creerPlanificateur ( ) ;
const identifiant = creerIdentifiants ( prefixe ) ;
const seance = creerSeance ( {
fs ,
racine ,
horloge : creerHorloge ( ) ,
identifiant ,
produitVersion : V1 ,
planifier : planificateur . planifier ,
annuler : planificateur . annuler ,
... ( inactiviteMs === undefined ? { } : { inactiviteMs } ) ,
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
... options ,
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
} ) ;
return { fs , seance , planificateur , identifiant } ;
}
// Crée l'événement d'essai par la séance, puis passe en écriture.
async function enEcriture ( montage , champs = { } ) {
await montage . seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 , ... champs } ) ;
await montage . seance . passerEnEcriture ( ) ;
}
const personne = ( id , nom , prenom = null , appartenance = null ) => ( {
id ,
nom ,
prenom ,
appartenance ,
courriel : null ,
titrePressenti : null ,
notes : null ,
exclu : false ,
} ) ;
// Quatre personnes, deux tables de deux places, deux tours ; Iris Ombrelle
// anime la table 1 : une réservation « tous » au siège 1 et le titre de ce
// siège.
function chargeEssai ( ) {
const charge = creerCharge ( { id : 'evt-lucioles' , nom : BASE , siegesParDefaut : 2 , tours : 2 } ) ;
charge . prochainsIds = { participant : 5 , table : 3 , proposition : 1 } ;
charge . participants = [
personne ( 1 , 'Ombrelle' , 'Iris' , 'Club des Merles' ) ,
personne ( 2 , 'Grisaille' , null , 'Club des Merles' ) ,
personne ( 3 , 'Pervenche' , 'Théo' ) ,
personne ( 4 , 'Lacasse' , 'Ondine' , 'Société Alpha' ) ,
] ;
charge . tables = [ 1 , 2 ] . map ( ( id ) => ( { id , numero : id , sieges : null , forme : 'ronde' , position : { x : 250 * ( id - 1 ) , y : 0 } } ) ) ;
charge . reservations = [ { participant : 1 , table : 1 , siege : 1 , portee : 'tous' , tour : null } ] ;
charge . titres = [ { table : 1 , siege : 1 , libelle : 'animation' } ] ;
return charge ;
}
// Pose sur le disque, par le dépôt, l'événement de la charge d'essai.
async function deposerEssai ( fs , charge = chargeEssai ( ) ) {
fs . deposerDossier ( 'documents' ) ;
await creerDansLeDepot ( fs , DOC , charge , {
libelle : libelle ( 'ENTREE_CREATION' , { nom : charge . evenement . nom } ) ,
nomJalon : libelle ( 'JALON_CREATION' ) ,
horodatage : '2031-05-17T07:00:00-04:00' ,
produitVersion : V1 ,
} ) ;
}
// Les arguments d'enregistrerGeneration : une génération courte du moteur.
function generer ( charge , nombre = 2 , graine = 7 ) {
const configuration = configurationDepuisCharge ( charge ) ;
return { propositions : rechercher ( configuration , { graine , arret : 40 , nombre } ) , configuration , produitVersion : V1 } ;
}
// Le journal et l'état que porte le disque.
const journalDe = ( fs , racine = 'documents' , base = BASE ) => lireJournal ( fs . contenu ( racine , ` ${ base } ${ SUFFIXES . journal } ` ) ) ;
const etatDe = ( fs , racine = 'documents' , base = BASE ) => analyser ( fs . contenu ( racine , ` ${ base } ${ SUFFIXES . etat } ` ) ) ;
const libellesDe = ( journal ) => journal . entrees . map ( ( entree ) => entree . libelle ) ;
const verrouDe = ( fs , racine = 'documents' ) => {
const texte = fs . contenu ( racine , VERROU ) ;
return texte === null ? null : JSON . parse ( texte ) ;
} ;
const noms = ( charge ) => charge . participants . map ( ( { nom } ) => nom ) ;
// Ouvre base par la séance d'un montage qui ne tient aucun verrou, et
// constate qu'ouvrir n'écrit rien (§ 8.4).
async function ouvrirSansEcrire ( { fs , seance } , base = BASE ) {
const avant = fs . compteurs . ecritures ;
const rendu = await seance . ouvrir ( base ) ;
assert . equal ( fs . compteurs . ecritures , avant , 'ouvrir écrit' ) ;
assert . equal ( seance . mode , 'lecture' ) ;
return rendu ;
}
// Le refus attendu : le code, et les détails quand ils sont donnés. Rend
// l'erreur.
async function refus ( promesse , code , details ) {
let erreur ;
await assert . rejects ( promesse , ( raison ) => {
erreur = raison ;
return true ;
} ) ;
assert . equal ( erreur . code , code , ` ${ erreur } ` ) ;
if ( details !== undefined ) assert . deepEqual ( erreur . details , details ) ;
return erreur ;
}
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
// Les refus de lecture seule de la séance, remède compris.
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const FORMAT _SEUL = { raison : 'FORMAT' , remede : null } ;
const BLOQUE _SEUL = { raison : 'BLOQUE' , remede : { geste : 'debloquer' } } ;
const ENCODAGE _SEUL = { raison : 'ENCODAGE' , remede : { geste : 'ouvrirSecours' , source : 'precedent' } } ;
// Le remède d'un support rompu : écrire ailleurs, dans la racine proposée.
const versRacine = ( racine ) => ( { geste : 'ecrireAilleurs' , racine } ) ;
[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
// --- Les épreuves -----------------------------------------------------------
describe ( 'séance : créer et ouvrir (§ 2.1, § 8.1, § 8.4)' , ( ) => {
test ( "créer écrit le journal puis l'état, pose le jalon « Création », et laisse l'événement ouvert en lecture" , async ( ) => {
const { fs , seance , identifiant } = monter ( ) ;
const avant = fs . compteurs . ecritures ;
const { base } = await seance . creer ( { nom : ' Soirée des Lucioles ' , date : '2031-05-17' , siegesParDefaut : 8 , tours : 3 } ) ;
assert . equal ( base , BASE ) ;
assert . equal ( fs . compteurs . ecritures - avant , 2 ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . deepEqual ( identifiant . tirages , [ 'seance-1' , 'seance-2' ] ) ;
assert . deepEqual ( seance . charge . evenement , { ... creerCharge ( { id : 'seance-2' , nom : BASE , date : '2031-05-17' , siegesParDefaut : 8 , tours : 3 } ) . evenement } ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION ] ) ;
assert . deepEqual ( journal . jalons . map ( ( { revision , nom } ) => [ revision , nom ] ) , [ [ 1 , 'Création' ] ] ) ;
assert . equal ( etatDe ( fs ) . entete . revision , 1 ) ;
} ) ;
test ( "une création refusée n'écrit rien et ne tire aucun identifiant" , async ( ) => {
const { fs , seance , identifiant } = monter ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . creer ( { nom : ' ' , date : null , siegesParDefaut : 8 , tours : 3 } ) , 'NOM_REQUIS' ) ;
await refus ( seance . creer ( { nom : BASE , date : null , siegesParDefaut : 1 , tours : 3 } ) , 'SIEGES_INVALIDES' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( identifiant . tirages , [ 'seance-1' ] ) ;
assert . equal ( seance . ouvert , null ) ;
} ) ;
test ( "créer ferme l'événement en écriture : son verrou est rendu, et le nouveau s'ouvre en lecture, sans verrou" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance , planificateur } = montage ;
await enEcriture ( montage ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
await seance . creer ( { nom : 'Soirée des Hiboux' , date : null , siegesParDefaut : 2 , tours : 1 } ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . nom , planificateur . actives ] , [ 'lecture' , 'Soirée des Hiboux' , 0 ] ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . equal ( fs . contenu ( 'documents' , ` Soirée des Hiboux ${ SUFFIXES . verrou } ` ) , null ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'MODE_LECTURE' ) ;
} ) ;
test ( "une création refusée laisse l'événement ouvert, son mode et son verrou, sans rien écrire" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . creer ( { nom : ' ' , date : null , siegesParDefaut : 2 , tours : 2 } ) , 'NOM_REQUIS' ) ;
await refus ( seance . creer ( { nom : 'Soirée des Hiboux' , date : '2031-02-30' , siegesParDefaut : 2 , tours : 2 } ) , 'DATE_INVALIDE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . nom , verrouDe ( fs ) ? . seance ] , [ 'ecriture' , BASE , seance . id ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' ] ) ;
} ) ;
test ( "ouvrir n'écrit rien et laisse en lecture ; en lecture, un geste, défaire et nommer sont refusés MODE_LECTURE" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const { seance } = monter ( { fs } ) ;
const avant = fs . compteurs . ecritures ;
const { base , avertissements } = await seance . ouvrir ( BASE ) ;
assert . deepEqual ( [ base , avertissements ] , [ BASE , [ ] ] ) ;
assert . equal ( fs . compteurs . ecritures , avant , 'ouvrir écrit' ) ;
assert . equal ( seance . mode , 'lecture' ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'MODE_LECTURE' , { remede : { geste : 'modifier' } } ) ;
[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
await refus ( seance . defaire ( ) , 'MODE_LECTURE' ) ;
await refus ( seance . revenir ( 1 ) , 'MODE_LECTURE' ) ;
await refus ( seance . nommer ( 'Avant le dessert' ) , 'MODE_LECTURE' ) ;
assert . equal ( fs . compteurs . ecritures , avant , 'un refus écrit' ) ;
assert . match ( libelle ( 'MODE_LECTURE' ) , /« Modifier »/ ) ;
} ) ;
test ( "ouvrir un secours n'écrit rien et laisse en lecture" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) } ) ;
await enEcriture ( premier ) ;
await premier . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
await premier . seance . fermer ( ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
const avant = second . fs . compteurs . ecritures ;
await second . seance . ouvrirSecours ( BASE , 'precedent' ) ;
assert . equal ( second . fs . compteurs . ecritures , avant , 'ouvrirSecours écrit' ) ;
assert . equal ( second . seance . mode , 'lecture' ) ;
assert . deepEqual ( [ second . seance . ouvert . revision , noms ( second . seance . charge ) ] , [ 1 , [ ] ] ) ;
} ) ;
test ( "ouvrir ferme le précédent sans question : son verrou est rendu, et seul ce rendu s'écrit" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await creerDansLeDepot ( fs , DOC , creerCharge ( { id : 'evt-hiboux' , nom : 'Soirée des Hiboux' , siegesParDefaut : 2 , tours : 1 } ) , {
libelle : 'Création' ,
nomJalon : 'Création' ,
horodatage : '2031-05-17T07:00:00-04:00' ,
produitVersion : V1 ,
} ) ;
assert . notEqual ( verrouDe ( fs ) , null ) ;
const avant = fs . compteurs . ecritures ;
await seance . ouvrir ( 'Soirée des Hiboux' ) ;
assert . equal ( fs . compteurs . ecritures - avant , 1 ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . nom ] , [ 'lecture' , 'Soirée des Hiboux' ] ) ;
} ) ;
test ( "un événement qui ne s'ouvre pas laisse la séance sans événement, le précédent fermé" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
const avant = montage . fs . compteurs . ecritures ;
await refus ( montage . seance . ouvrir ( 'Soirée absente' ) , 'ABSENT' ) ;
assert . equal ( montage . fs . compteurs . ecritures - avant , 1 , "seul le rendu du verrou s'écrit" ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . deepEqual ( [ montage . seance . ouvert , montage . seance . charge , montage . seance . mode ] , [ null , null , 'lecture' ] ) ;
} ) ;
test ( "sans événement ouvert, chaque geste est refusé AUCUN_EVENEMENT, sans écriture" , async ( ) => {
const { fs , seance } = monter ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . passerEnEcriture ( ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . defaire ( ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . debloquer ( ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . nommer ( 'Jalon' ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'AUCUN_EVENEMENT' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( seance . retours , { defaire : false , refaire : false } ) ;
} ) ;
} ) ;
describe ( "séance : le verrou, pris à l'entrée en écriture (§ 8.8)" , ( ) => {
test ( "passer en écriture prend le verrou de la séance ; le redemander ne refait rien" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
assert . equal ( montage . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( montage . fs ) . seance , montage . seance . id ) ;
assert . equal ( montage . seance . id , 'seance-1' ) ;
const avant = montage . fs . compteurs . ecritures ;
assert . deepEqual ( await montage . seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . equal ( montage . fs . compteurs . ecritures , avant ) ;
} ) ;
test ( 'une seconde séance sur le même disque ouvre en lecture, et son passage en écriture est refusé VERROU_PRIS' , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
const details = { seance : 'premiere-1' , depuis , vivant : true } ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { ... details , remede : null } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
assert . equal ( second . seance . mode , 'lecture' ) ;
assert . deepEqual ( second . seance . verrouAutre , details ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'premiere-1' ) ;
await refus ( second . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'MODE_LECTURE' ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
test ( "le verrou porte l'instant de l'horloge de la séance : VERROU_PRIS le rend à une seconde séance" , async ( ) => {
const disque = new Map ( ) ;
const pilote = '2031-05-17T19:42:00-04:00' ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' , horloge : { horodatage : ( ) => pilote } } ) ;
await enEcriture ( premier ) ;
assert . equal ( verrouDe ( premier . fs ) . depuis , pilote ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : 'premiere-1' , depuis : pilote , vivant : true , remede : null } ) ;
} ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
test ( "le verrou d'une séance morte du même poste se reprend, et l'avertissement le dit" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
premier . fs . pannes . couperApres ( 0 ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : 'premiere-1' , depuis } } ] ;
assert . deepEqual ( await second . seance . passerEnEcriture ( ) , { avertissements : repris } ) ;
assert . equal ( second . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'seconde-1' ) ;
assert . deepEqual ( second . seance . avertissements , repris ) ;
assert . equal ( second . seance . verrouAutre , null ) ;
} ) ;
test ( "le verrou d'un autre poste, ou illisible, ne se reprend pas" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const lointain = monter ( { fs : creerFichiersSimules ( { disque , hote : 'autre-poste' } ) , prefixe : 'lointaine' } ) ;
await ouvrirSansEcrire ( lointain ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( lointain . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : 'premiere-1' , depuis , vivant : null , remede : null } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
premier . fs . deposer ( 'documents' , VERROU , 'pas un verrou\n' ) ;
const voisin = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'voisine' } ) ;
await ouvrirSansEcrire ( voisin ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( voisin . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : null , depuis : null , vivant : null , remede : null } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
assert . equal ( premier . fs . contenu ( 'documents' , VERROU ) , 'pas un verrou\n' ) ;
} ) ;
test ( 'sans verrou disponible, passer en écriture ne pose aucun fichier' , async ( ) => {
const montage = monter ( { fs : creerFichiersSimules ( { verrouDisponible : false } ) } ) ;
await enEcriture ( montage ) ;
assert . equal ( montage . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
await montage . seance . passerEnLecture ( ) ;
assert . equal ( montage . seance . mode , 'lecture' ) ;
} ) ;
test ( "fermer rend le verrou et oublie l'événement" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
await montage . seance . fermer ( ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . deepEqual ( [ montage . seance . ouvert , montage . seance . mode , montage . planificateur . actives ] , [ null , 'lecture' , 0 ] ) ;
} ) ;
} ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
describe ( 'séance : reprendre le verrou que la séance ne reprend pas seule (§ 8.8)' , ( ) => {
// Un verrou posé sur le disque partagé par une séance d'un autre poste, à
// l'instant depuis.
const verrouLointain = ( seance , depuis ) => ` ${ JSON . stringify ( { seance , pid : 41 , hote : 'poste-lointain' , depuis } )} \n ` ;
// Une séance d'épreuve qui ouvre en lecture l'événement d'essai, créé par
// une première séance, déjà fermée, sur le même disque.
async function secondeSeance ( disque , hote = 'autre-poste' ) {
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'createur' } ) ;
await premier . seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) ;
await premier . seance . fermer ( ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque , hote } ) , prefixe : 'reprise' } ) ;
await ouvrirSansEcrire ( second ) ;
return second ;
}
test ( "le verrou d'un autre poste, refusé puis repris : il s'efface, la séance prend le sien et passe en écriture, VERROU_REPRIS le dit" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const lointain = monter ( { fs : creerFichiersSimules ( { disque , hote : 'autre-poste' } ) , prefixe : 'lointaine' } ) ;
await ouvrirSansEcrire ( lointain ) ;
await refus ( lointain . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : 'premiere-1' , depuis , vivant : null , remede : null } ) ;
assert . deepEqual ( lointain . seance . etat ( ) . verrouAutre , { seance : 'premiere-1' , depuis , vivant : null } ) ;
const journal = lointain . fs . contenu ( 'documents' , JOURNAL ) ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : 'premiere-1' , depuis } } ] ;
assert . deepEqual ( await lointain . seance . reprendreVerrou ( ) , { avertissements : repris } ) ;
assert . equal ( lointain . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( lointain . fs ) . seance , 'lointaine-1' ) ;
assert . equal ( lointain . fs . contenu ( 'documents' , JOURNAL ) , journal , 'reprendre n\u{2019}écrit aucune entrée' ) ;
const etat = lointain . seance . etat ( ) ;
assert . equal ( etat . verrouAutre , null ) ;
assert . deepEqual ( etat . avertissements , repris ) ;
assert . equal ( etat . refus , null ) ;
await lointain . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ombrelle' , prenom : 'Iris' } } ) ;
assert . deepEqual ( noms ( etatDe ( lointain . fs ) . charge ) , [ 'Ombrelle' ] ) ;
} ) ;
test ( 'un verrou illisible se reprend de même, sous VERROU_REPRIS {seance: null, depuis: null}' , async ( ) => {
const disque = new Map ( ) ;
const second = await secondeSeance ( disque , 'poste-epreuve' ) ;
second . fs . deposer ( 'documents' , VERROU , 'pas un verrou\n' ) ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : null , depuis : null , vivant : null , remede : null } ) ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : null , depuis : null } } ] ;
assert . deepEqual ( await second . seance . reprendreVerrou ( ) , { avertissements : repris } ) ;
assert . equal ( second . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'reprise-1' ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
test ( "un verrou illisible remplacé par un valide entre sa relecture et son effacement : le valide reste, et VERROU_PRIS le nomme" , async ( ) => {
const disque = new Map ( ) ;
const second = await secondeSeance ( disque , 'poste-epreuve' ) ;
second . fs . deposer ( 'documents' , VERROU , 'pas un verrou\n' ) ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : null , depuis : null , vivant : null , remede : null } ) ;
// Le système de la séance pose un verrou valide juste après avoir rendu
// l'illisible à la reprise, comme une autre séance qui le prendrait
// entre deux appels.
const pose = '2031-05-17T09:15:00-04:00' ;
const valide = verrouLointain ( 'lointaine-6' , pose ) ;
const verrouiller = second . fs . verrouiller ;
let substituer = true ;
second . fs . verrouiller = async ( ... parametres ) => {
const rendu = await verrouiller ( ... parametres ) ;
if ( substituer && rendu . pris === false && rendu . seance === null ) {
substituer = false ;
second . fs . deposer ( 'documents' , VERROU , valide ) ;
}
return rendu ;
} ;
const details = { seance : 'lointaine-6' , depuis : pose , vivant : null } ;
await refus ( second . seance . reprendreVerrou ( ) , 'VERROU_PRIS' , { ... details , remede : null } ) ;
assert . equal ( second . fs . contenu ( 'documents' , VERROU ) , valide ) ;
assert . equal ( second . seance . mode , 'lecture' ) ;
assert . deepEqual ( second . seance . etat ( ) . verrouAutre , details ) ;
} ) ;
test ( 'un verrou illisible disparu entre le refus et sa relecture : la relecture prend le verrou, la séance le reconnaît comme le sien, VERROU_REPRIS puis écriture' , async ( ) => {
const disque = new Map ( ) ;
const second = await secondeSeance ( disque , 'poste-epreuve' ) ;
second . fs . deposer ( 'documents' , VERROU , 'pas un verrou\n' ) ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : null , depuis : null , vivant : null , remede : null } ) ;
// Le fichier disparaît juste avant le deuxième appel de la reprise, la
// relecture d'effacerVerrou, qui pose alors le verrou de la séance.
const verrouiller = second . fs . verrouiller ;
let appels = 0 ;
second . fs . verrouiller = async ( racine , chemin , ... reste ) => {
appels += 1 ;
if ( appels === 2 ) await second . fs . supprimer ( racine , chemin ) ;
return verrouiller ( racine , chemin , ... reste ) ;
} ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : null , depuis : null } } ] ;
assert . deepEqual ( await second . seance . reprendreVerrou ( ) , { avertissements : repris } ) ;
assert . equal ( second . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'reprise-1' ) ;
assert . equal ( second . seance . etat ( ) . verrouAutre , null ) ;
await second . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ombrelle' , prenom : 'Iris' } } ) ;
assert . deepEqual ( noms ( etatDe ( second . fs ) . charge ) , [ 'Ombrelle' ] ) ;
} ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
test ( "un verrou qui a changé depuis le refus ne s'efface pas : VERROU_PRIS nomme le nouveau, que verrouAutre garde" , async ( ) => {
const disque = new Map ( ) ;
const second = await secondeSeance ( disque ) ;
const vu = '2031-05-17T09:00:00-04:00' ;
second . fs . deposer ( 'documents' , VERROU , verrouLointain ( 'lointaine-1' , vu ) ) ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : 'lointaine-1' , depuis : vu , vivant : null , remede : null } ) ;
const pose = '2031-05-17T09:30:00-04:00' ;
const nouveau = verrouLointain ( 'lointaine-2' , pose ) ;
second . fs . deposer ( 'documents' , VERROU , nouveau ) ;
const details = { seance : 'lointaine-2' , depuis : pose , vivant : null } ;
await refus ( second . seance . reprendreVerrou ( ) , 'VERROU_PRIS' , { ... details , remede : null } ) ;
assert . equal ( second . fs . contenu ( 'documents' , VERROU ) , nouveau ) ;
assert . equal ( second . seance . mode , 'lecture' ) ;
assert . deepEqual ( second . seance . etat ( ) . verrouAutre , details ) ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : 'lointaine-2' , depuis : pose } } ] ;
assert . deepEqual ( await second . seance . reprendreVerrou ( ) , { avertissements : repris } ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'reprise-1' ) ;
} ) ;
// Le verrou vu, puis remplacé par celui que décrit change : la reprise
// le refuse VERROU_PRIS avec les détails du nouveau, ne touche pas au
// fichier, et verrouAutre garde le nouveau.
async function repriseRefusee ( seance , depuis ) {
const second = await secondeSeance ( new Map ( ) ) ;
const vu = '2031-05-17T09:00:00-04:00' ;
second . fs . deposer ( 'documents' , VERROU , verrouLointain ( 'lointaine-1' , vu ) ) ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : 'lointaine-1' , depuis : vu , vivant : null , remede : null } ) ;
const nouveau = verrouLointain ( seance , depuis ) ;
second . fs . deposer ( 'documents' , VERROU , nouveau ) ;
const details = { seance , depuis , vivant : null } ;
await refus ( second . seance . reprendreVerrou ( ) , 'VERROU_PRIS' , { ... details , remede : null } ) ;
assert . equal ( second . fs . contenu ( 'documents' , VERROU ) , nouveau ) ;
assert . equal ( second . seance . mode , 'lecture' ) ;
assert . deepEqual ( second . seance . etat ( ) . verrouAutre , details ) ;
}
test ( "la même séance reprise à un instant plus tard : le verrou n'est plus celui que l'opérateur a vu, il ne s'efface pas" , async ( ) => {
await repriseRefusee ( 'lointaine-1' , '2031-05-17T09:45:00-04:00' ) ;
} ) ;
test ( "une autre séance au même instant : le verrou n'est plus celui que l'opérateur a vu, il ne s'efface pas" , async ( ) => {
await repriseRefusee ( 'lointaine-5' , '2031-05-17T09:00:00-04:00' ) ;
} ) ;
test ( "le verrou d'une séance vivante de ce poste ne se reprend jamais" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const voisin = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'voisine' } ) ;
await ouvrirSansEcrire ( voisin ) ;
const details = { seance : 'premiere-1' , depuis , vivant : true } ;
await refus ( voisin . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { ... details , remede : null } ) ;
await refus ( voisin . seance . reprendreVerrou ( ) , 'VERROU_PRIS' , { ... details , remede : null } ) ;
assert . equal ( verrouDe ( voisin . fs ) . seance , 'premiere-1' ) ;
assert . equal ( voisin . seance . mode , 'lecture' ) ;
} ) ;
test ( "sans verrou refusé, reprendre est passer en écriture : un verrou posé depuis n'est pas repris" , async ( ) => {
const second = await secondeSeance ( new Map ( ) ) ;
const pose = '2031-05-17T10:00:00-04:00' ;
second . fs . deposer ( 'documents' , VERROU , verrouLointain ( 'lointaine-3' , pose ) ) ;
await refus ( second . seance . reprendreVerrou ( ) , 'VERROU_PRIS' , { seance : 'lointaine-3' , depuis : pose , vivant : null , remede : null } ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'lointaine-3' ) ;
const libre = await secondeSeance ( new Map ( ) ) ;
assert . deepEqual ( await libre . seance . reprendreVerrou ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ libre . seance . mode , verrouDe ( libre . fs ) . seance ] , [ 'ecriture' , 'reprise-1' ] ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
test ( 'sans verrou refusé, reprendre sur un plan bloqué lève LECTURE_SEULE BLOQUE comme passer en écriture, sans rien écrire' , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seance . passerEnLecture ( ) ;
assert . equal ( seance . etat ( ) . verrouAutre , null ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . reprendreVerrou ( ) , 'LECTURE_SEULE' , BLOQUE _SEUL ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat , verrouDe ( fs ) ] , [ 'lecture' , 'bloque' , null ] ) ;
} ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
test ( 'sur un plan bloqué, la reprise rend aussitôt le verrou : la séance reste en lecture, et débloquer passe ensuite' , async ( ) => {
const montage = monter ( { prefixe : 'bloqueuse' } ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const depuis = '2031-05-17T11:00:00-04:00' ;
fs . deposer ( 'documents' , VERROU , verrouLointain ( 'lointaine-4' , depuis ) ) ;
await refus ( seance . debloquer ( ) , 'VERROU_PRIS' , { seance : 'lointaine-4' , depuis , vivant : null , remede : null } ) ;
const journal = fs . contenu ( 'documents' , JOURNAL ) ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : 'lointaine-4' , depuis } } ] ;
assert . deepEqual ( await seance . reprendreVerrou ( ) , { avertissements : repris } ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat ] , [ 'lecture' , 'bloque' ] ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . equal ( fs . contenu ( 'documents' , JOURNAL ) , journal ) ;
assert . deepEqual ( seance . etat ( ) . avertissements , repris ) ;
assert . deepEqual ( seance . etat ( ) . refus , { code : 'LECTURE_SEULE' , details : BLOQUE _SEUL } ) ;
await seance . debloquer ( ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat ] , [ 'ecriture' , 'brouillon' ] ) ;
} ) ;
test ( "en écriture, reprendre ne fait rien ; un format plus récent et un support rompu le refusent sans rien écrire" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
const avant = montage . fs . compteurs . ecritures ;
assert . deepEqual ( await montage . seance . reprendreVerrou ( ) , { avertissements : [ ] } ) ;
assert . equal ( montage . fs . compteurs . ecritures , avant ) ;
montage . fs . pannes . ecriture ( 'documents' ) ;
await refus ( montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Pervenche' } } ) , 'ECRITURE' ) ;
await montage . seance . passerEnLecture ( ) ;
const rompu = montage . fs . compteurs . ecritures ;
await refus ( montage . seance . reprendreVerrou ( ) , 'SUPPORT_ROMPU' ) ;
assert . equal ( montage . fs . compteurs . ecritures , rompu ) ;
const fsRecent = creerFichiersSimules ( ) ;
deposerAuFormat ( fsRecent , FORMAT + 1 ) ;
const recent = monter ( { fs : fsRecent } ) ;
await ouvrirSansEcrire ( recent ) ;
await refus ( recent . seance . reprendreVerrou ( ) , 'LECTURE_SEULE' , FORMAT _SEUL ) ;
} ) ;
} ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
describe ( "séance : l'inactivité (§ 8.4)" , ( ) => {
test ( 'au terme du délai, la séance revient seule en lecture et rend son verrou' , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
await montage . planificateur . avancer ( INACTIVITE _MS - 1 ) ;
assert . equal ( montage . seance . mode , 'ecriture' ) ;
await montage . planificateur . avancer ( 1 ) ;
assert . equal ( montage . seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . equal ( montage . planificateur . actives , 0 ) ;
assert . equal ( INACTIVITE _MS , 10 * 60 * 1000 ) ;
} ) ;
test ( "pendant un geste ouvert, rien ne tombe ; les gestes s'emboîtent ; le dernier fini, le compte repart" , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
assert . equal ( planificateur . actives , 1 ) ;
seance . debuterGeste ( ) ;
seance . debuterGeste ( ) ;
assert . equal ( planificateur . actives , 0 , 'un geste ouvert laisse une minuterie armée' ) ;
await planificateur . avancer ( 3 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
seance . finirGeste ( ) ;
assert . equal ( planificateur . actives , 0 , 'le geste encore ouvert laisse une minuterie armée' ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . equal ( planificateur . actives , 0 , 'un geste achevé arme le compte malgré le geste ouvert' ) ;
await planificateur . avancer ( 2 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
seance . finirGeste ( ) ;
assert . equal ( planificateur . actives , 1 ) ;
await planificateur . avancer ( INACTIVITE _MS - 1 ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
await planificateur . avancer ( 1 ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
} ) ;
test ( "pendant un calcul ouvert, rien ne tombe ; fini, le compte repart ; un calcul fini de trop ne compte pas" , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
seance . debuterCalcul ( ) ;
assert . equal ( seance . calculEnCours , true ) ;
assert . equal ( planificateur . actives , 0 , 'un calcul ouvert laisse une minuterie armée' ) ;
await planificateur . avancer ( 2 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
seance . finirCalcul ( ) ;
seance . finirCalcul ( ) ;
assert . equal ( seance . calculEnCours , false ) ;
seance . debuterCalcul ( ) ;
await planificateur . avancer ( 2 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' , 'le calcul fini de trop a compté contre le suivant' ) ;
seance . finirCalcul ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
test ( 'chaque geste achevé relance le compte ; le délai se règle' , async ( ) => {
const montage = monter ( { inactiviteMs : 5000 } ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
await planificateur . avancer ( 4000 ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
await planificateur . avancer ( 4000 ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : ' ' } } ) , 'NOM_REQUIS' ) ;
await planificateur . avancer ( 4000 ) ;
assert . equal ( seance . mode , 'ecriture' , 'un geste refusé est une activité' ) ;
await planificateur . avancer ( 1000 ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
test ( 'passer en lecture annule le geste en cours et rend le verrou ; repassé en écriture, le compte court' , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
seance . debuterGeste ( ) ;
await seance . passerEnLecture ( ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . equal ( planificateur . actives , 0 ) ;
await seance . passerEnEcriture ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
} ) ;
describe ( 'séance : un geste, une entrée, un état (§ 2.1, § 8.2, § 10.1)' , ( ) => {
test ( "un geste écrit le journal puis l'état ; son libellé reste figé quand on renomme la personne" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const avant = fs . compteurs . ecritures ;
const rendu = await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ombrelle' , prenom : 'Iris' } } ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// L'événement neuf n'a aucune table : la première personne manque d'une place.
const manque = [ { code : 'PLACES_MANQUANTES' , details : { placesManquantes : 1 } } ] ;
assert . deepEqual ( rendu , { libelle : 'Ajouté Iris Ombrelle' , avertissements : manque } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
assert . equal ( fs . compteurs . ecritures - avant , 3 , "la ligne, le .precedent, l'état" ) ;
await seance . executer ( 'modifierParticipant' , { id : 1 , champs : { nom : 'Parasol' } } ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION , 'Ajouté Iris Ombrelle' , 'Modifié Iris Ombrelle : nom (désormais Iris Parasol)' ] ) ;
assert . deepEqual ( etatDe ( fs ) . charge , reconstruire ( journal , 3 ) ) ;
assert . equal ( etatDe ( fs ) . entete . revision , 3 ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Parasol' ] ) ;
} ) ;
test ( "deux gestes lancés sans attendre écrivent deux entrées, dans l'ordre" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
const premier = montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const second = montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
await Promise . all ( [ premier , second ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( montage . fs ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
assert . deepEqual ( montage . seance . charge . participants . map ( ( { id , nom } ) => [ id , nom ] ) , [ [ 1 , 'Ancolie' ] , [ 2 , 'Bruyère' ] ] ) ;
} ) ;
test ( "un geste sans effet, ou refusé par sa commande, n'écrit rien" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . executer ( 'modifierParticipant' , { id : 1 , champs : { nom : 'Ancolie' } } ) , 'SANS_EFFET' ) ;
await refus ( seance . executer ( 'exclureParticipant' , { id : 7 } ) , 'PARTICIPANT_INCONNU' ) ;
await refus ( seance . executer ( 'changerEtat' , { etat : 'retenu' } ) , 'AUCUN_RETENU' ) ;
await assert . rejects ( seance . executer ( 'commandeInventee' , { } ) , TypeError ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
test ( "nommer l'instant courant pose un jalon, sans entrée" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
await montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const avant = montage . fs . compteurs . ecritures ;
await montage . seance . nommer ( ' Avant le dessert ' ) ;
assert . equal ( montage . fs . compteurs . ecritures - avant , 1 ) ;
const journal = journalDe ( montage . fs ) ;
assert . equal ( journal . entrees . length , 2 ) ;
assert . deepEqual ( journal . jalons . at ( - 1 ) , { type : 'jalon' , revision : 2 , nom : 'Avant le dessert' , horodatage : journal . jalons . at ( - 1 ) . horodatage } ) ;
await refus ( montage . seance . nommer ( ' ' ) , 'NOM_REQUIS' ) ;
} ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "un import est une seule entrée ; en brouillon, il ne dit que le manque de places, et en retenu l'import et la dérive (§ 10.1, § 5.9)" , async ( ) => {
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
const csv = 'nom;prenom\nAncolie;Bastien\nBruyère;Capucine\nCerfeuil;Damien\n' ;
const entrees = journalDe ( fs ) . entrees . length ;
const rendu = await seance . executer ( 'importerParticipants' , { apercu : apercevoir ( csv , { participants : seance . charge . participants } ) , mode : 'ajouter' } ) ;
assert . equal ( rendu . libelle , 'Importé 3 participants' ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . deepEqual ( rendu . avertissements , [ { code : 'PLACES_MANQUANTES' , details : { placesManquantes : 3 } } ] ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
assert . equal ( rendu . resume . ajoutes , 3 ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees + 1 ) ;
assert . equal ( seance . charge . participants . length , 7 ) ;
await seance . executer ( 'supprimerParticipant' , { id : 7 } ) ;
await seance . executer ( 'supprimerParticipant' , { id : 6 } ) ;
await seance . executer ( 'supprimerParticipant' , { id : 5 } ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
const plus = apercevoir ( 'nom\nEpicéa\n' , { participants : seance . charge . participants } ) ;
const enRetenu = await seance . executer ( 'importerParticipants' , { apercu : plus , mode : 'ajouter' } ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
const nonPlace = [ { code : 'PARTICIPANT_NON_PLACE' , participant : 8 , personne : 'Epicéa' } ] ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . deepEqual ( enRetenu . avertissements , [
{ code : 'IMPORT_EN_RETENU' , details : { } } ,
{ code : 'PLACES_MANQUANTES' , details : { placesManquantes : 1 } } ,
{
code : 'DERIVE_NOUVELLE' ,
details : { touchees : [ { id : 1 , raisons : nonPlace } , { id : 2 , raisons : nonPlace } , { id : 'retenu' , raisons : nonPlace } ] } ,
} ,
] ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
// La séance montre aussi la dérive que l'Ouvert écrit porte désormais.
assert . deepEqual ( seance . avertissements , [ ... seance . ouvert . avertissements , ... enRetenu . avertissements ] ) ;
assert . deepEqual ( seance . ouvert . avertissements . map ( ( { code } ) => code ) , [ 'DERIVE' ] ) ;
[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 ( "importer sur un plan bloqué est refusé, et le texte du refus nomme « Débloquer »" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const avant = fs . compteurs . ecritures ;
const apercu = apercevoir ( 'nom\nAncolie\n' , { participants : [ ] } ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const erreur = await refus ( seance . executer ( 'importerParticipants' , { apercu , mode : 'ajouter' } ) , 'LECTURE_SEULE' , BLOQUE _SEUL ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
assert . match ( libelle ( erreur . code , erreur . details ) , /« Débloquer »/ ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
test ( "exclure une personne réservée suspend ses réservations, son titre n'est plus pourvu, et l'avertissement le dit ; la réintégrer les rend (§ 4.4)" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
const exclusion = await seance . executer ( 'exclureParticipant' , { id : 1 } ) ;
assert . deepEqual ( exclusion , {
libelle : 'Exclu Iris Ombrelle' ,
avertissements : [ { code : 'RESERVATIONS_SUSPENDUES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ] ,
} ) ;
assert . deepEqual ( [ seance . charge . participants [ 0 ] . exclu , seance . charge . reservations . length ] , [ true , 1 ] ) ;
assert . deepEqual ( seance . avertissements , exclusion . avertissements ) ;
const retour = await seance . executer ( 'reintegrerParticipant' , { id : 1 } ) ;
assert . deepEqual ( retour . avertissements , [
{ code : 'RESERVATIONS_RETABLIES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ,
] ) ;
assert . equal ( seance . charge . participants [ 0 ] . exclu , false ) ;
} ) ;
} ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
describe ( "séance : ce qu'un import dit, et ce que la séance montre (§ 4.4, § 9, § 10.1)" , ( ) => {
// L'événement d'essai, ouvert depuis le disque, en écriture.
async function essaiEnEcriture ( ) {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
await ouvrirSansEcrire ( montage ) ;
await montage . seance . passerEnEcriture ( ) ;
return montage ;
}
const mettreAJour = ( seance , csv ) =>
seance . executer ( 'importerParticipants' , {
apercu : apercevoir ( csv , { participants : seance . charge . participants , mode : 'mettreAJour' } ) ,
mode : 'mettreAJour' ,
} ) ;
test ( "un import qui exclut l'animatrice réservée dit ses réservations suspendues et son titre non pourvu, comme exclure (§ 4.4)" , async ( ) => {
const { seance } = await essaiEnEcriture ( ) ;
const rendu = await mettreAJour ( seance , 'nom;prenom;exclu\nOmbrelle;Iris;oui\n' ) ;
const suspendues = { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } ;
assert . equal ( rendu . libelle , 'Mis à jour la liste par import : 0 ajouté, 1 modifié' ) ;
assert . deepEqual ( rendu . avertissements , [ { code : 'RESERVATIONS_SUSPENDUES' , details : suspendues } ] ) ;
assert . equal ( seance . charge . participants [ 0 ] . exclu , true ) ;
const retour = await mettreAJour ( seance , 'nom;prenom;exclu\nOmbrelle;Iris;non\n' ) ;
assert . deepEqual ( retour . avertissements , [ { code : 'RESERVATIONS_RETABLIES' , details : suspendues } ] ) ;
} ) ;
test ( "un import dont les lignes valides ne changent rien, mais qui refuse des lignes, n'écrit rien et rend son résumé" , async ( ) => {
const { fs , seance } = await essaiEnEcriture ( ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Lacasse' , prenom : 'Ondine' } } ) ;
const avant = fs . compteurs . ecritures ;
const entrees = journalDe ( fs ) . entrees . length ;
const rendu = await mettreAJour ( seance , 'nom;prenom;courriel\nLacasse;Ondine;o@exemple.test\nPervenche;Théo;\n' ) ;
assert . equal ( rendu . libelle , null ) ;
assert . deepEqual ( rendu . avertissements , [ ] ) ;
assert . deepEqual ( rendu . resume . refusees . map ( ( { ligne , code } ) => [ ligne , code ] ) , [ [ 2 , 'PLUSIEURS_CORRESPONDENT' ] ] ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
assert . deepEqual ( seance . avertissements , [ ] ) ;
} ) ;
test ( "après un geste, la séance montre les avertissements de l'Ouvert écrit : la dérive paraît et s'en va (§ 9)" , async ( ) => {
const { seance } = await essaiEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
assert . deepEqual ( seance . avertissements , [ ] ) ;
const exclusion = await seance . executer ( 'exclureParticipant' , { id : 2 } ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . deepEqual ( codesDe ( exclusion . avertissements ) , [ 'DERIVE_NOUVELLE' ] ) ;
assert . deepEqual ( seance . avertissements . map ( ( { code } ) => code ) , [ 'DERIVE' , 'DERIVE_NOUVELLE' ] ) ;
assert . deepEqual ( seance . avertissements , [ ... seance . ouvert . avertissements , ... exclusion . avertissements ] ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
assert . equal ( seance . avertissements [ 0 ] . details . retenu , true ) ;
await seance . executer ( 'reintegrerParticipant' , { id : 2 } ) ;
assert . deepEqual ( seance . avertissements , [ ] ) ;
} ) ;
test ( "le journal incomplet de l'ouverture ne s'affiche plus une fois la reprise écrite" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const plus = chargeEssai ( ) ;
plus . participants . push ( personne ( 5 , 'Ancolie' ) ) ;
plus . prochainsIds . participant = 6 ;
fs . deposer ( 'documents' , ETAT , serialiser ( plus , { revision : 4 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
const incomplet = [ { code : 'JOURNAL_INCOMPLET' , details : { etat : 4 , journal : 1 } } ] ;
assert . deepEqual ( seance . avertissements , incomplet ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( seance . avertissements , incomplet ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// Cinq personnes déjà pour quatre places : seul le manque, qui croît, se dit.
assert . deepEqual ( seance . avertissements , [ { code : 'PLACES_MANQUANTES' , details : { placesManquantes : 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
} ) ;
} ) ;
describe ( 'séance : la forme nommée se lit sans rien écrire (§ 8.9, point 4)' , ( ) => {
test ( "la forme nommée d'une proposition ou du retenu se lit en lecture, sans entrée ni écriture" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 2 } ) ;
await seance . passerEnLecture ( ) ;
const avant = fs . compteurs . ecritures ;
const entrees = journalDe ( fs ) . entrees . length ;
const proposition = await seance . formeNommee ( { proposition : 1 } ) ;
assert . deepEqual ( proposition . fautes , [ ] ) ;
assert . deepEqual (
proposition . lignes . filter ( ( { tour } ) => tour === 1 ) . map ( ( { nom } ) => nom ) . sort ( ) ,
[ 'Grisaille' , 'Lacasse' , 'Ombrelle' , 'Pervenche' ] ,
) ;
const retenu = await seance . formeNommee ( { retenu : true } ) ;
assert . equal ( retenu . lignes . length , 8 ) ;
await refus ( seance . formeNommee ( { proposition : 7 } ) , 'PROPOSITION_INCONNUE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
test ( "un retenu fautif sur le disque se lit sous forme nommée par la séance : ses lignes et ses fautes (RETENU_FAUTIF, § 8.9)" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
await seance . passerEnLecture ( ) ;
const { entete , charge } = etatDe ( fs ) ;
const premier = charge . retenu . placement [ 0 ] ;
const assis = premier . sieges . find ( ( liste ) => liste . length > 0 ) [ 0 ] ;
premier . reserve = [ ... premier . reserve , assis ] . sort ( ( a , b ) => a - b ) ;
fs . deposer ( 'documents' , ETAT , serialiser ( charge , { revision : entete . revision , produitVersion : V1 } ) ) ;
await ouvrirSansEcrire ( montage ) ;
assert . equal ( seance . avertissements [ 0 ] . code , 'RETENU_FAUTIF' ) ;
const lue = await seance . formeNommee ( { retenu : true } ) ;
assert . deepEqual ( lue . fautes , [ { code : 'DOUBLON' , tour : 1 , participant : assis } ] ) ;
assert . equal ( lue . lignes . length , 9 ) ;
assert . equal ( lue . lignes . filter ( ( { tour , participant } ) => tour === 1 && participant === assis ) . length , 2 ) ;
} ) ;
test ( 'sans événement ouvert, la forme nommée est refusée AUCUN_EVENEMENT' , async ( ) => {
await refus ( monter ( ) . seance . formeNommee ( { retenu : true } ) , 'AUCUN_EVENEMENT' ) ;
} ) ;
} ) ;
describe ( "séance : une autre séance a écrit entre-temps (§ 8.2, § 8.8)" , ( ) => {
// Deux processus d'un même poste sur un même disque : la première séance
// crée l'événement d'essai, puis la seconde l'ouvre en lecture.
async function deuxSeances ( ) {
const disque = new Map ( ) ;
const premiere = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await premiere . seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) ;
const seconde = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( seconde ) ;
return { premiere : premiere . seance , seconde : seconde . seance , fs : seconde . fs , fsPremiere : premiere . fs , disque } ;
}
test ( "passer en écriture relit l'événement : le travail de l'autre séance est adopté, EVENEMENT_CHANGE le dit, et le geste suivant s'écrit à sa suite" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'ajouterParticipant' , { champs : { nom : 'Xavier' } } ) ;
await premiere . passerEnLecture ( ) ;
const change = [ { code : 'EVENEMENT_CHANGE' , details : { revision : 2 } } ] ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , { avertissements : change } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) . seance ] , [ 'ecriture' , seconde . id ] ) ;
assert . deepEqual ( noms ( seconde . charge ) , [ 'Xavier' ] ) ;
assert . deepEqual ( seconde . avertissements , change ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION , 'Ajouté Xavier' , 'Ajouté Yvette' ] ) ;
assert . equal ( journal . ecartees , 0 ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Xavier' , 'Yvette' ] ) ;
assert . deepEqual ( etatDe ( fs ) . charge , reconstruire ( journal , 3 ) ) ;
} ) ;
test ( "sans geste de l'autre séance, rien n'avertit ; son jalon est adopté avec le journal relu" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . nommer ( 'Avant les invitations' ) ;
await premiere . passerEnLecture ( ) ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( seconde . ouvert . journal . jalons . map ( ( { nom } ) => nom ) , [ 'Création' , 'Avant les invitations' ] ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) ;
assert . deepEqual ( journalDe ( fs ) . jalons . map ( ( { nom } ) => nom ) , [ 'Création' , 'Avant les invitations' ] ) ;
} ) ;
test ( "un événement qui a quitté le disque entre-temps, renommé ailleurs : le passage en écriture est refusé, le verrou rendu, rien ne se recrée" , async ( ) => {
const { seconde , fs , disque } = await deuxSeances ( ) ;
const autre = creerFichiersSimules ( { disque } ) ;
for ( const fichier of [ ETAT , JOURNAL ] ) await autre . supprimer ( DOC , fichier ) ;
await refus ( seconde . passerEnEcriture ( ) , 'ABSENT' ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) ] , [ 'lecture' , null ] ) ;
assert . deepEqual ( ( await fs . lister ( DOC , '' ) ) . map ( ( { nom } ) => nom ) , [ ] ) ;
await refus ( seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) , 'MODE_LECTURE' ) ;
} ) ;
test ( "un plan que l'autre séance a bloqué entre-temps : adopté, EVENEMENT_CHANGE le dit, et le passage est refusé BLOQUE, le verrou rendu" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , BLOQUE _SEUL ) ;
[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
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . charge . evenement . etat ] , [ 'lecture' , null , 'bloque' ] ) ;
assert . deepEqual ( seconde . avertissements , [ { code : 'EVENEMENT_CHANGE' , details : { revision : 2 } } ] ) ;
} ) ;
test ( "débloquer relit l'événement sous le verrou : le déblocage s'écrit à la suite du travail de l'autre séance" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seconde . ouvrir ( BASE ) ;
await premiere . debloquer ( ) ;
await premiere . executer ( 'ajouterParticipant' , { champs : { nom : 'Xavier' } } ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . equal ( premiere . mode , 'lecture' ) ;
const rendu = await seconde . debloquer ( ) ;
assert . deepEqual ( rendu , {
libelle : 'Plan débloqué, état « Brouillon »' ,
avertissements : [ { code : 'EVENEMENT_CHANGE' , details : { revision : 5 } } ] ,
} ) ;
assert . equal ( seconde . mode , 'ecriture' ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( journal . entrees . map ( ( { revision } ) => revision ) , [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Xavier' ] ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'brouillon' ) ;
} ) ;
test ( "débloquer un plan que l'autre séance a débloqué entre-temps : refusé PLAN_NON_BLOQUE, adopté, le verrou rendu" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seconde . ouvrir ( BASE ) ;
await premiere . debloquer ( ) ;
await premiere . passerEnLecture ( ) ;
const entrees = journalDe ( fs ) . entrees . length ;
await refus ( seconde . debloquer ( ) , 'PLAN_NON_BLOQUE' ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . charge . evenement . etat ] , [ 'lecture' , null , 'brouillon' ] ) ;
assert . deepEqual ( seconde . avertissements , [ { code : 'EVENEMENT_CHANGE' , details : { revision : 3 } } ] ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
} ) ;
test ( "un secours ouvert se relit par la même source : passer en écriture ne bute pas sur l'état illisible" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Xavier' } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'journal' ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Xavier' , 'Yvette' ] ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
} ) ;
// L'événement de la première séance porte Aubépine puis Bourrache, son
// état devient illisible, et chacune des deux ouvre son .precedent : la
// révision 2, Aubépine seule.
async function surPrecedent ( ) {
const deux = await deuxSeances ( ) ;
const { premiere , seconde , fs } = deux ;
await premiere . passerEnEcriture ( ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await premiere . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await premiere . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
for ( const seance of [ premiere , seconde ] ) await seance . ouvrirSecours ( BASE , 'precedent' ) ;
assert . deepEqual ( [ seconde . ouvert . revision , noms ( seconde . charge ) ] , [ 2 , [ 'Aubépine' ] ] ) ;
return deux ;
}
for ( const gestes of [ [ 'Cerfeuil' ] , [ 'Cerfeuil' , 'Dictame' ] ] ) {
test ( ` un secours .precedent se relit par l'état qu'une autre séance a écrit depuis ( ${ gestes . length } geste·s) : adopté, EVENEMENT_CHANGE le dit, rien ne s'efface ` , async ( ) => {
const { premiere , seconde , fs } = await surPrecedent ( ) ;
await premiere . passerEnEcriture ( ) ;
for ( const nom of gestes ) await premiere . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await premiere . passerEnLecture ( ) ;
const { revision } = etatDe ( fs ) . entete ;
const change = [ { code : 'EVENEMENT_CHANGE' , details : { revision } } ] ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , { avertissements : change } ) ;
assert . deepEqual ( [ seconde . ouvert . revision , noms ( seconde . charge ) ] , [ revision , [ 'Aubépine' , ... gestes ] ] ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Estragon' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , ... gestes , 'Estragon' ] ) ;
assert . equal ( journalDe ( fs ) . ecartees , 0 ) ;
} ) ;
}
test ( "un secours .precedent dont l'état reste illisible, mais dont le journal a reçu le geste d'une autre séance : le dernier instant du journal est adopté, EVENEMENT_CHANGE le dit" , async ( ) => {
const { premiere , seconde , fs , fsPremiere } = await surPrecedent ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . nommer ( 'Point' ) ;
fsPremiere . pannes . renommage ( 'documents' ) ;
await refus ( premiere . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'ETAT_NON_ECRIT' ) ;
await premiere . passerEnLecture ( ) ;
assert . equal ( fs . contenu ( 'documents' , ETAT ) , '' ) ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , {
avertissements : [ { code : 'EVENEMENT_CHANGE' , details : { revision : 5 } } ] ,
} ) ;
assert . deepEqual ( noms ( seconde . charge ) , [ 'Aubépine' , 'Cerfeuil' ] ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Estragon' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , 'Cerfeuil' , 'Estragon' ] ) ;
} ) ;
test ( "un secours .precedent dont l'état redevient lisible entre-temps se relit par l'état, sans entrée nouvelle au journal : EVENEMENT_CHANGE le dit" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
const lisible = fs . contenu ( 'documents' , ETAT ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
fs . deposer ( 'documents' , ETAT , lisible ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , {
avertissements : [ { code : 'EVENEMENT_CHANGE' , details : { revision : 3 } } ] ,
} ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Aubépine' , 'Bourrache' ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , 'Bourrache' , 'Cerfeuil' ] ) ;
} ) ;
test ( "un secours .precedent ouvert devant un état lisible se relit par le .precedent tant que l'état ne change pas : le choix de l'opérateur reste (§ 8.9, point 3)" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . ouvert . revision , noms ( seance . charge ) ] , [ 2 , [ 'Aubépine' ] ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , 'Cerfeuil' ] ) ;
} ) ;
test ( "après un geste sur un secours .precedent, la séance se relit par l'état : redevenu illisible, il refuse le passage en écriture, sans revenir au .precedent" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await refus ( seance . passerEnEcriture ( ) , 'ETAT_ILLISIBLE' ) ;
assert . deepEqual ( [ seance . mode , verrouDe ( fs ) , noms ( seance . charge ) ] , [ 'lecture' , null , [ 'Aubépine' , 'Cerfeuil' ] ] ) ;
} ) ;
test ( "après avoir écrit ailleurs un secours .precedent, la séance s'y relit par l'état, que le dossier nouveau porte seul" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
fs . deposerDossier ( 'portable' ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
await seance . passerEnEcriture ( ) ;
fs . pannes . ecriture ( 'documents' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'ECRITURE' ) ;
await seance . ecrireAilleurs ( PORTABLE ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . mode , noms ( seance . charge ) ] , [ 'ecriture' , [ 'Aubépine' ] ] ) ;
} ) ;
test ( "le jalon d'une séance seule sur un secours .precedent pose la reprise : la relecture la retrouve, sans EVENEMENT_CHANGE ni recul de révision" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( await seance . nommer ( 'Point' ) , { revision : 4 } ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . ouvert . revision , noms ( seance . charge ) ] , [ 4 , [ 'Aubépine' ] ] ) ;
const codes = seance . avertissements . map ( ( { code } ) => code ) ;
assert . ok ( ! codes . includes ( 'JOURNAL_EN_AVANCE_DE' ) , codes . join ( ) ) ;
} ) ;
test ( "le jalon d'une séance seule sur un état que le journal devance de deux entrées pose la reprise : la relecture la retrouve, sans EVENEMENT_CHANGE" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const premier = fs . contenu ( 'documents' , ETAT ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , premier ) ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( await seance . nommer ( 'Point' ) , { revision : 4 } ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . ouvert . revision , noms ( seance . charge ) ] , [ 4 , [ ] ] ) ;
} ) ;
// L'événement réécrit par une construction plus récente : l'état, ou la
[ADD] storage: file format 2, tags, goals, converted journal
Tags on people and goals on tables are operator input that an older
build must not silently drop. Files written now carry format 2 with
every new field optional and last; a format-1 file opens with no write,
and its journal is rewritten as format 2 at the first gesture with every
revision rebuilt byte for byte, so undo crosses the conversion. An older
build opens a format-2 pair read-only. Tags follow one shared form and
split rule, which a guard forbids reimplementing.
Checked: node 4215, navigateur 884, node-long 113; a real format-1 event
written by the previous build stays a fixture; depot and journal 100%.
--- FR ---
[ADD] stockage : format 2, étiquettes, objectifs, journal converti
Les étiquettes des personnes et les objectifs des tables sont de la saisie
qu'une version antérieure ne doit pas perdre sans bruit. Les fichiers
écrits portent le format 2, chaque champ nouveau facultatif et en dernier ;
un fichier du format 1 s'ouvre sans écriture, et son journal passe au
format 2 au premier geste, chaque révision reconstruite à l'octet près :
Défaire traverse la conversion. Une version antérieure ouvre un couple du
format 2 en lecture seule. Une forme et une découpe uniques des étiquettes,
qu'une garde interdit de refaire ailleurs.
Vérifié : node 4215, navigateur 884, longues 113 ; un vrai événement du
format 1, écrit par la version précédente, reste en fixture ; 100 %.
Assisted-by: Claude Opus 5.5
2026-10-11 13:55:19 -04:00
// première ligne du journal, déclarent le format qui suit FORMAT.
const versPlusRecent = ( texte ) => texte . replace ( ` "format": ${ FORMAT } ` , ` "format": ${ FORMAT + 1 } ` ) ;
[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 plusRecent = {
[ADD] storage: file format 2, tags, goals, converted journal
Tags on people and goals on tables are operator input that an older
build must not silently drop. Files written now carry format 2 with
every new field optional and last; a format-1 file opens with no write,
and its journal is rewritten as format 2 at the first gesture with every
revision rebuilt byte for byte, so undo crosses the conversion. An older
build opens a format-2 pair read-only. Tags follow one shared form and
split rule, which a guard forbids reimplementing.
Checked: node 4215, navigateur 884, node-long 113; a real format-1 event
written by the previous build stays a fixture; depot and journal 100%.
--- FR ---
[ADD] stockage : format 2, étiquettes, objectifs, journal converti
Les étiquettes des personnes et les objectifs des tables sont de la saisie
qu'une version antérieure ne doit pas perdre sans bruit. Les fichiers
écrits portent le format 2, chaque champ nouveau facultatif et en dernier ;
un fichier du format 1 s'ouvre sans écriture, et son journal passe au
format 2 au premier geste, chaque révision reconstruite à l'octet près :
Défaire traverse la conversion. Une version antérieure ouvre un couple du
format 2 en lecture seule. Une forme et une découpe uniques des étiquettes,
qu'une garde interdit de refaire ailleurs.
Vérifié : node 4215, navigateur 884, longues 113 ; un vrai événement du
format 1, écrit par la version précédente, reste en fixture ; 100 %.
Assisted-by: Claude Opus 5.5
2026-10-11 13:55:19 -04:00
etat : ( fs ) => fs . deposer ( 'documents' , ETAT , versPlusRecent ( fs . contenu ( 'documents' , ETAT ) ) ) ,
journal : ( fs ) => fs . deposer ( 'documents' , JOURNAL , versPlusRecent ( fs . contenu ( 'documents' , JOURNAL ) ) ) ,
[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
} ;
for ( const [ fichier , designe ] of [ [ 'etat' , "l'état" ] , [ 'journal' , 'le journal' ] ] ) {
test ( ` ${ designe } qu'une construction plus récente a réécrit entre-temps : passer en écriture est refusé LECTURE_SEULE FORMAT, le verrou rendu ` , async ( ) => {
const { seconde , fs } = await deuxSeances ( ) ;
const entrees = journalDe ( fs ) . entrees . length ;
plusRecent [ fichier ] ( fs ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , FORMAT _SEUL ) ;
[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
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . ouvert . formatPlusRecent ] , [ 'lecture' , null , true ] ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'LECTURE_SEULE' , FORMAT _SEUL ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
if ( fichier === 'etat' ) assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
} ) ;
test ( ` un secours .precedent dont ${ designe } est réécrit entre-temps par une construction plus récente : passer en écriture est refusé LECTURE_SEULE FORMAT, le verrou rendu ` , async ( ) => {
const { seconde , fs } = await surPrecedent ( ) ;
if ( fichier === 'etat' ) {
const precedent = fs . contenu ( 'documents' , ` ${ BASE } ${ SUFFIXES . precedent } ` ) ;
[ADD] storage: file format 2, tags, goals, converted journal
Tags on people and goals on tables are operator input that an older
build must not silently drop. Files written now carry format 2 with
every new field optional and last; a format-1 file opens with no write,
and its journal is rewritten as format 2 at the first gesture with every
revision rebuilt byte for byte, so undo crosses the conversion. An older
build opens a format-2 pair read-only. Tags follow one shared form and
split rule, which a guard forbids reimplementing.
Checked: node 4215, navigateur 884, node-long 113; a real format-1 event
written by the previous build stays a fixture; depot and journal 100%.
--- FR ---
[ADD] stockage : format 2, étiquettes, objectifs, journal converti
Les étiquettes des personnes et les objectifs des tables sont de la saisie
qu'une version antérieure ne doit pas perdre sans bruit. Les fichiers
écrits portent le format 2, chaque champ nouveau facultatif et en dernier ;
un fichier du format 1 s'ouvre sans écriture, et son journal passe au
format 2 au premier geste, chaque révision reconstruite à l'octet près :
Défaire traverse la conversion. Une version antérieure ouvre un couple du
format 2 en lecture seule. Une forme et une découpe uniques des étiquettes,
qu'une garde interdit de refaire ailleurs.
Vérifié : node 4215, navigateur 884, longues 113 ; un vrai événement du
format 1, écrit par la version précédente, reste en fixture ; 100 %.
Assisted-by: Claude Opus 5.5
2026-10-11 13:55:19 -04:00
fs . deposer ( 'documents' , ETAT , versPlusRecent ( precedent ) ) ;
[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
} else {
plusRecent . journal ( fs ) ;
}
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , FORMAT _SEUL ) ;
[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
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . ouvert . formatPlusRecent ] , [ 'lecture' , null , true ] ) ;
} ) ;
test ( ` un plan bloqué dont ${ designe } est réécrit entre-temps par une construction plus récente : débloquer est refusé LECTURE_SEULE FORMAT, le verrou rendu, sans entrée ` , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seconde . ouvrir ( BASE ) ;
const texte = fs . contenu ( 'documents' , JOURNAL ) ;
plusRecent [ fichier ] ( fs ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seconde . debloquer ( ) , 'LECTURE_SEULE' , FORMAT _SEUL ) ;
[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
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . ouvert . formatPlusRecent ] , [ 'lecture' , null , true ] ) ;
if ( fichier === 'etat' ) assert . equal ( fs . contenu ( 'documents' , JOURNAL ) , texte ) ;
} ) ;
}
// L'état réécrit en octets hors UTF-8 : son premier « é » remplacé par
// l'octet E9 d'un éditeur réglé en windows-1252. Rend ces octets.
function etatMalEncode ( fs ) {
const octets = [ ... new TextEncoder ( ) . encode ( fs . contenu ( 'documents' , ETAT ) ) ] ;
const rang = octets . findIndex ( ( octet , i ) => octet === 0xc3 && octets [ i + 1 ] === 0xa9 ) ;
octets . splice ( rang , 2 , 0xe9 ) ;
fs . deposerOctets ( 'documents' , ETAT , new Uint8Array ( octets ) ) ;
return new Uint8Array ( octets ) ;
}
// Les noms des fichiers de documents et le texte du journal : ce qu'un
// refus laisse tel quel.
const empreinte = async ( fs ) => [ ( await fs . lister ( DOC , '' ) ) . map ( ( { nom } ) => nom ) , fs . contenu ( 'documents' , JOURNAL ) ] ;
test ( "l'état réécrit entre-temps en octets hors UTF-8 : passer en écriture est refusé LECTURE_SEULE {raison: 'ENCODAGE'}, comme un format plus récent, le verrou rendu et rien de l'événement écrit ; le refus a son texte" , async ( ) => {
const { seconde , fs } = await deuxSeances ( ) ;
const octets = etatMalEncode ( fs ) ;
const avant = await empreinte ( fs ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const erreur = await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , ENCODAGE _SEUL ) ;
[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
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) ] , [ 'lecture' , null ] ) ;
assert . deepEqual ( await empreinte ( fs ) , avant ) ;
assert . deepEqual ( fs . octets ( 'documents' , ETAT ) , octets ) ;
assert . match ( libelle ( erreur . code , erreur . details ) , /UTF-8/ ) ;
await refus ( seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'MODE_LECTURE' ) ;
} ) ;
test ( "un plan bloqué dont l'état est réécrit entre-temps en octets hors UTF-8 : débloquer est refusé LECTURE_SEULE {raison: 'ENCODAGE'}, le verrou rendu, sans entrée" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await premiere . passerEnLecture ( ) ;
await seconde . ouvrir ( BASE ) ;
const octets = etatMalEncode ( fs ) ;
const avant = await empreinte ( fs ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seconde . debloquer ( ) , 'LECTURE_SEULE' , ENCODAGE _SEUL ) ;
[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
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) ] , [ 'lecture' , null ] ) ;
assert . deepEqual ( await empreinte ( fs ) , avant ) ;
assert . deepEqual ( fs . octets ( 'documents' , ETAT ) , octets ) ;
} ) ;
for ( const source of [ 'precedent' , 'journal' ] ) {
test ( ` ouvert par son secours ${ source } , un état aux octets hors UTF-8 ne fige rien : passer en écriture réussit, et le premier geste met l'état dans un dossier daté de la corbeille, ses octets intacts, avant d'écrire ` , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
const octets = etatMalEncode ( fs ) ;
const illisible = await refus ( seance . ouvrir ( BASE ) , 'ETAT_ILLISIBLE' ) ;
assert . equal ( illisible . details . raison , 'ENCODAGE' ) ;
assert . match ( libelle ( illisible . code , illisible . details ) , /UTF-8/ ) ;
await seance . ouvrirSecours ( BASE , source ) ;
assert . equal ( seance . ouvert . etatMalEncode , true ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
const [ dossier ] = ( await fs . lister ( DOC , 'corbeille' ) ) . map ( ( { nom } ) => nom ) ;
assert . deepEqual ( fs . octets ( 'documents' , ` corbeille/ ${ dossier } / ${ ETAT } ` ) , octets ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ ... noms ( seance . charge ) ] ) ;
assert . ok ( noms ( seance . charge ) . includes ( 'Cerfeuil' ) ) ;
assert . equal ( seance . ouvert . etatMalEncode , false ) ;
for ( const { code , details } of seance . avertissements ) assert . equal ( typeof libelle ( code , details ) , 'string' ) ;
} ) ;
}
} ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
describe ( 'séance : revenir, défaire, refaire (§ 8.3)' , ( ) => {
// Trois gestes après la création : Ancolie, Bruyère, Capucine, aux
// révisions 2, 3 et 4.
async function troisGestes ( ) {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Ancolie' , 'Bruyère' , 'Capucine' ] ) await montage . seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
return montage ;
}
test ( 'défaire, refaire et revenir écrivent des entrées de retour, et le journal ne se tronque jamais' , async ( ) => {
const montage = await troisGestes ( ) ;
const { fs , seance } = montage ;
assert . deepEqual ( await seance . defaire ( ) , { libelle : 'Défait : « Ajouté Capucine »' , avertissements : [ ] } ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
await seance . refaire ( ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' , 'Capucine' ] ) ;
await seance . revenir ( 2 ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual (
journal . entrees . map ( ( { revision , sens , retour , libelle : texte } ) => [ revision , sens , retour , texte ] ) ,
[
[ 1 , null , null , CREATION ] ,
[ 2 , null , null , 'Ajouté Ancolie' ] ,
[ 3 , null , null , 'Ajouté Bruyère' ] ,
[ 4 , null , null , 'Ajouté Capucine' ] ,
[ 5 , 'defaire' , 3 , 'Défait : « Ajouté Capucine »' ] ,
[ 6 , 'refaire' , 4 , 'Refait : « Ajouté Capucine »' ] ,
[ 7 , 'revenir' , 2 , 'Retour à « Ajouté Ancolie »' ] ,
] ,
) ;
2026-10-06 23:40:40 -04:00
// L'instant 2 revient avec son contenu, et ses compteurs restent où trois
// ajouts les ont portés : aucun identifiant donné ne se redonne (§ 8.9).
const instant2 = reconstruire ( journal , 2 ) ;
const attendu = { ... instant2 , prochainsIds : { ... instant2 . prochainsIds , participant : 4 } } ;
assert . deepEqual ( etatDe ( fs ) . charge , attendu ) ;
assert . deepEqual ( reconstruire ( journal , 7 ) , attendu ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Digitale' } } ) ;
assert . deepEqual (
seance . charge . participants . map ( ( { id , nom } ) => [ id , nom ] ) ,
[ [ 1 , 'Ancolie' ] , [ 4 , 'Digitale' ] ] ,
) ;
[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 ( "refaire n'existe qu'après des retours, tant qu'aucun geste ne les suit" , async ( ) => {
const montage = await troisGestes ( ) ;
const { fs , seance } = montage ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . refaire ( ) , 'RIEN_A_REFAIRE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
await seance . defaire ( ) ;
await seance . defaire ( ) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : true } ) ;
await seance . refaire ( ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Dauphinelle' } } ) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
await refus ( seance . refaire ( ) , 'RIEN_A_REFAIRE' ) ;
} ) ;
test ( "rien à défaire au premier instant" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
assert . deepEqual ( montage . seance . retours , { defaire : false , refaire : false } ) ;
const avant = montage . fs . compteurs . ecritures ;
await refus ( montage . seance . defaire ( ) , 'RIEN_A_DEFAIRE' ) ;
assert . equal ( montage . fs . compteurs . ecritures , avant ) ;
} ) ;
test ( 'après un retour de la révision 6 à la 3, défaire vise le prédécesseur de 3 sur le fil, non la ligne précédente' , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Ancolie' , 'Bruyère' , 'Capucine' , 'Dauphinelle' , 'Églantine' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . revenir ( 3 ) ;
await seance . defaire ( ) ;
const derniere = journalDe ( fs ) . entrees . at ( - 1 ) ;
assert . deepEqual ( [ derniere . revision , derniere . sens , derniere . retour , derniere . libelle ] , [ 8 , 'defaire' , 2 , 'Défait : « Ajouté Bruyère »' ] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
} ) ;
test ( "un journal qui finit avant l'état : défaire vise l'instant où il finit, par-dessus l'entrée de reprise" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
// L'état du disque, à la révision 4, porte une personne que le journal,
// arrêté à la révision 1, n'a pas.
const plus = chargeEssai ( ) ;
plus . participants . push ( personne ( 5 , 'Ancolie' ) ) ;
plus . prochainsIds . participant = 6 ;
fs . deposer ( 'documents' , ETAT , serialiser ( plus , { revision : 4 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
assert . deepEqual ( seance . ouvert . avertissements , [ { code : 'JOURNAL_INCOMPLET' , details : { etat : 4 , journal : 1 } } ] ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
const reprise = "Reprise de l'état trouvé sur le disque" ;
assert . deepEqual ( await seance . defaire ( ) , { libelle : ` Défait : « ${ reprise } » ` , avertissements : [ ] } ) ;
assert . deepEqual (
journalDe ( fs ) . entrees . map ( ( { revision , sens , retour , libelle : texte } ) => [ revision , sens , retour , texte ] ) ,
[
[ 1 , null , null , CREATION ] ,
[ 2 , null , null , reprise ] ,
[ 3 , 'defaire' , 1 , ` Défait : « ${ reprise } » ` ] ,
] ,
) ;
assert . deepEqual ( noms ( seance . charge ) , noms ( chargeEssai ( ) ) ) ;
assert . deepEqual ( seance . retours , { defaire : false , refaire : true } ) ;
await seance . refaire ( ) ;
assert . deepEqual ( noms ( seance . charge ) , noms ( plus ) ) ;
} ) ;
test ( "un état sans journal : rien à défaire ; le premier geste ouvre le journal par l'entrée de reprise" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , ETAT , serialiser ( chargeEssai ( ) , { revision : 3 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( seance . retours , { defaire : false , refaire : false } ) ;
await refus ( seance . defaire ( ) , 'RIEN_A_DEFAIRE' ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual (
journalDe ( fs ) . entrees . map ( ( { revision , libelle : texte } ) => [ revision , texte ] ) ,
[
[ 3 , "Reprise de l'état trouvé sur le disque" ] ,
[ 4 , 'Ajouté Ancolie' ] ,
] ,
) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
} ) ;
test ( "revenir à un instant que le journal ne restitue plus est refusé, sans écriture" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
const charge = chargeEssai ( ) ;
// Un journal élagué : sa première entrée est la révision 10.
const lignes = [
ligneOuverture ( charge . evenement . id ) ,
ligneEntree ( { revision : 10 , libelle : 'Geste 10' , horodatage : '2031-05-17T07:00:00-04:00' , produitVersion : V1 , apres : charge , instantane : true } ) ,
] ;
fs . deposer ( 'documents' , JOURNAL , ` ${ lignes . join ( '\n' ) } \n ` ) ;
fs . deposer ( 'documents' , ETAT , serialiser ( charge , { revision : 10 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
const avant = fs . compteurs . ecritures ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seance . revenir ( 3 ) , 'INSTANT_NON_RESTITUABLE' , { revision : 3 , remede : null } ) ;
await refus ( seance . revenir ( 11 ) , 'INSTANT_NON_RESTITUABLE' , { revision : 11 , remede : null } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
await refus ( seance . defaire ( ) , 'RIEN_A_DEFAIRE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
} ) ;
describe ( 'séance : les jalons automatiques (§ 8.3)' , ( ) => {
test ( "la création, chaque changement d'état, et l'instant qui précède chaque génération sont nommés" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge , 1 , 9 ) ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION , 'Généré 2 propositions (n° 1 à 2)' , 'Retenu la proposition n° 1' , 'Généré 1 proposition (n° 3)' ] ) ;
assert . deepEqual ( journal . jalons . map ( ( { revision , nom } ) => [ revision , nom ] ) , [
[ 1 , 'Création' ] ,
[ 1 , 'Avant la génération' ] ,
[ 2 , 'État « Proposé »' ] ,
[ 3 , 'État « Retenu »' ] ,
[ 3 , 'Avant la génération' ] ,
] ) ;
} ) ;
test ( "un retour qui change l'état du plan pose le jalon qui le nomme : défaire un déblocage rebloque, et la séance revient en lecture" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seance . debloquer ( ) ;
const rendu = await seance . defaire ( ) ;
assert . deepEqual ( rendu , { libelle : 'Défait : « Plan débloqué, état « Brouillon » »' , avertissements : [ ] } ) ;
const journal = journalDe ( fs ) ;
const derniere = journal . entrees . at ( - 1 ) ;
assert . deepEqual ( [ derniere . revision , derniere . sens , derniere . retour ] , [ 4 , 'defaire' , 2 ] ) ;
assert . deepEqual ( journal . jalons . map ( ( { revision , nom } ) => [ revision , nom ] ) , [
[ 1 , 'Création' ] ,
[ 2 , 'État « Bloqué »' ] ,
[ 3 , 'État « Brouillon »' ] ,
[ 4 , 'État « Bloqué »' ] ,
] ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat , verrouDe ( fs ) ] , [ 'lecture' , 'bloque' , null ] ) ;
} ) ;
} ) ;
describe ( 'séance : bloquer et débloquer (§ 8.5, § 9)' , ( ) => {
test ( "bloquer écrit l'entrée et son jalon, puis la séance revient en lecture et rend le verrou" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const rendu = await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . equal ( rendu . libelle , "Plan passé à l'état « Bloqué »" ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . equal ( montage . planificateur . actives , 0 ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( journal . jalons . at ( - 1 ) . nom , 'État « Bloqué »' ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'bloque' ) ;
} ) ;
test ( 'sur un plan bloqué : passer en écriture, un geste, défaire et nommer sont refusés BLOQUE, sans écriture' , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) } ) ;
await enEcriture ( premier ) ;
await premier . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
await premier . seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
for ( const { seance , fs } of [ premier , second ] ) {
const avant = fs . compteurs . ecritures ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const bloque = BLOQUE _SEUL ;
[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
await refus ( seance . passerEnEcriture ( ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . executer ( 'changerEtat' , { etat : 'retenu' } ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . defaire ( ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . nommer ( 'Distribué' ) , 'LECTURE_SEULE' , bloque ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( seance . mode , 'lecture' ) ;
}
assert . match ( libelle ( 'LECTURE_SEULE' , { raison : 'BLOQUE' } ) , /« Débloquer »/ ) ;
} ) ;
test ( "débloquer prend le verrou, écrit une entrée et un jalon, rend l'état que le contenu impose, et laisse la séance en écriture" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const entrees = journalDe ( fs ) . entrees . length ;
const rendu = await seance . debloquer ( ) ;
assert . equal ( rendu . libelle , 'Plan débloqué, état « Brouillon »' ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
assert . equal ( seance . charge . evenement . etat , etatDeduit ( seance . charge ) ) ;
const journal = journalDe ( fs ) ;
assert . equal ( journal . entrees . length , entrees + 1 ) ;
assert . deepEqual ( journal . jalons . at ( - 1 ) , { ... journal . jalons . at ( - 1 ) , revision : journal . entrees . at ( - 1 ) . revision , nom : 'État « Brouillon »' } ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seance . executer ( 'debloquer' , { } ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat ] , [ 'ecriture' , 'brouillon' ] ) ;
} ) ;
test ( "débloquer devant le verrou d'un autre poste est refusé VERROU_PRIS, avant toute entrée" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const depuis = '2031-05-17T09:00:00-04:00' ;
fs . deposer ( 'documents' , VERROU , ` ${ JSON . stringify ( { seance : 'lointaine-1' , pid : 99 , hote : 'autre-poste' , depuis } )} \n ` ) ;
const journal = fs . contenu ( 'documents' , JOURNAL ) ;
const avant = fs . compteurs . ecritures ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seance . debloquer ( ) , 'VERROU_PRIS' , { seance : 'lointaine-1' , depuis , vivant : null , remede : null } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
assert . equal ( fs . compteurs . ecritures - avant , 1 , 'seule la prise du verrou se tente' ) ;
assert . equal ( fs . contenu ( 'documents' , JOURNAL ) , journal ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat ] , [ 'lecture' , 'bloque' ] ) ;
assert . equal ( verrouDe ( fs ) . seance , 'lointaine-1' ) ;
} ) ;
test ( "débloquer un plan qui ne l'est pas : en lecture MODE_LECTURE, en écriture PLAN_NON_BLOQUE" , async ( ) => {
const montage = monter ( ) ;
await montage . seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) ;
await refus ( montage . seance . debloquer ( ) , 'MODE_LECTURE' ) ;
await montage . seance . passerEnEcriture ( ) ;
const avant = montage . fs . compteurs . ecritures ;
await refus ( montage . seance . debloquer ( ) , 'PLAN_NON_BLOQUE' ) ;
assert . equal ( montage . fs . compteurs . ecritures , avant ) ;
} ) ;
} ) ;
describe ( "séance : un fichier d'un format plus récent (§ 8.8)" , ( ) => {
test ( "s'ouvre en lecture sans rien écrire, et tout ce qui écrirait est refusé LECTURE_SEULE FORMAT, avant BLOQUE" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
const charge = chargeEssai ( ) ;
charge . evenement . etat = 'bloque' ;
[ADD] storage: file format 2, tags, goals, converted journal
Tags on people and goals on tables are operator input that an older
build must not silently drop. Files written now carry format 2 with
every new field optional and last; a format-1 file opens with no write,
and its journal is rewritten as format 2 at the first gesture with every
revision rebuilt byte for byte, so undo crosses the conversion. An older
build opens a format-2 pair read-only. Tags follow one shared form and
split rule, which a guard forbids reimplementing.
Checked: node 4215, navigateur 884, node-long 113; a real format-1 event
written by the previous build stays a fixture; depot and journal 100%.
--- FR ---
[ADD] stockage : format 2, étiquettes, objectifs, journal converti
Les étiquettes des personnes et les objectifs des tables sont de la saisie
qu'une version antérieure ne doit pas perdre sans bruit. Les fichiers
écrits portent le format 2, chaque champ nouveau facultatif et en dernier ;
un fichier du format 1 s'ouvre sans écriture, et son journal passe au
format 2 au premier geste, chaque révision reconstruite à l'octet près :
Défaire traverse la conversion. Une version antérieure ouvre un couple du
format 2 en lecture seule. Une forme et une découpe uniques des étiquettes,
qu'une garde interdit de refaire ailleurs.
Vérifié : node 4215, navigateur 884, longues 113 ; un vrai événement du
format 1, écrit par la version précédente, reste en fixture ; 100 %.
Assisted-by: Claude Opus 5.5
2026-10-11 13:55:19 -04:00
fs . deposer (
'documents' ,
ETAT ,
serialiser ( charge , { revision : 1 , produitVersion : V1 } ) . replace ( ` "format": ${ FORMAT } ` , ` "format": ${ FORMAT + 1 } ` ) ,
) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
const { seance } = monter ( { fs } ) ;
const avant = fs . compteurs . ecritures ;
await seance . ouvrir ( BASE ) ;
assert . equal ( seance . ouvert . formatPlusRecent , true ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const format = FORMAT _SEUL ;
[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
await refus ( seance . passerEnEcriture ( ) , 'LECTURE_SEULE' , format ) ;
await refus ( seance . executer ( 'changerEtat' , { etat : 'retenu' } ) , 'LECTURE_SEULE' , format ) ;
await refus ( seance . debloquer ( ) , 'LECTURE_SEULE' , format ) ;
await refus ( seance . defaire ( ) , 'LECTURE_SEULE' , format ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . doesNotMatch ( libelle ( 'LECTURE_SEULE' , format ) , /Débloquer/ ) ;
} ) ;
} ) ;
describe ( 'séance : la perte du support en cours de séance (§ 8.6)' , ( ) => {
// Un événement du disque portable, en écriture, après un premier geste.
[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
async function surPortable ( reglages = { } ) {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' , ... reglages } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
const montage = monter ( { fs , racine : PORTABLE } ) ;
await enEcriture ( montage ) ;
await montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
return montage ;
}
test ( "une écriture refusée rompt le support : le geste est refusé, les suivants aussi, puis écrire ailleurs rétablit" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
const refusSupport = { echec : 'ECRITURE' , chemin : JOURNAL , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
assert . equal ( seance . supportRompu , true ) ;
assert . deepEqual ( seance . refusSupport , refusSupport ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
const avant = fs . compteurs . ecritures ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'SUPPORT_ROMPU' , { ... refusSupport , remede : versRacine ( DOC ) } ) ;
[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
await refus ( seance . defaire ( ) , 'SUPPORT_ROMPU' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . match ( libelle ( 'SUPPORT_ROMPU' , refusSupport ) , /Écrire ailleurs/ ) ;
const { base , avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
] ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport , seance . mode ] , [ false , null , 'ecriture' ] ) ;
assert . deepEqual ( seance . racine , DOC ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
test ( "le cadrage en attente quand le support rompt s'écrit dans les réglages de la racine nouvelle, après écrire ailleurs" , async ( ) => {
const { fs , seance , planificateur } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
seance . demanderCadrage ( { k : 1.25 , tx : 30 , ty : - 12 } ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
await seance . ecrireAilleurs ( DOC ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
const evenement = seance . charge . evenement . id ;
assert . deepEqual ( JSON . parse ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) ) . cadrages , [ { evenement , k : 1.25 , tx : 30 , ty : - 12 } ] ) ;
assert . equal ( fs . contenu ( 'portable' , FICHIER _REGLAGES ) , null ) ;
} ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
test ( "un état qui ne s'écrit pas : le geste est au journal, la séance en montre la charge, et écrire ailleurs la garde" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . renommage ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport . echec ] , [ true , 'ETAT_NON_ECRIT' ] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs , 'portable' ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
await seance . ecrireAilleurs ( DOC ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
assert . equal ( seance . supportRompu , false ) ;
} ) ;
test ( "un jalon d'état qui ne s'écrit pas laisse le geste enregistré : JALON_NON_ECRIT le dit, et le support est rompu" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
// La deuxième ligne ajoutée au journal à partir d'ici, le jalon qui suit
// l'entrée, trouve le support en lecture seule.
const ajouterLigne = fs . ajouterLigne ;
let lignes = 0 ;
fs . ajouterLigne = async ( racine , chemin , ligne ) => {
lignes += 1 ;
if ( lignes === 2 ) fs . pannes . ecriture ( racine . id ) ;
return ajouterLigne ( racine , chemin , ligne ) ;
} ;
const refusSupport = { chemin : JOURNAL , dossier : DOCUMENTS , cause : 'EROFS' } ;
const rendu = await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . deepEqual ( rendu , {
libelle : "Plan passé à l'état « Bloqué »" ,
avertissements : [
{ code : 'JALON_NON_ECRIT' , details : refusSupport } ,
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : DOCUMENTS , cause : 'EROFS' } } ,
] ,
} ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport ] , [ true , { echec : 'JALON_NON_ECRIT' , ... refusSupport } ] ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'bloque' ) ;
const journal = journalDe ( fs ) ;
assert . equal ( journal . entrees . at ( - 1 ) . libelle , "Plan passé à l'état « Bloqué »" ) ;
assert . deepEqual ( journal . jalons . map ( ( { nom } ) => nom ) , [ 'Création' ] ) ;
} ) ;
test ( "une création dont l'état ne s'écrit pas reste ouverte en lecture, le support rompu, et s'écrit ailleurs" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
const montage = monter ( { fs , racine : PORTABLE } ) ;
const { seance } = montage ;
// La deuxième écriture atomique, l'état qui suit le journal, trouve le
// support en lecture seule.
const ecrireAtomique = fs . ecrireAtomique ;
let ecritures = 0 ;
fs . ecrireAtomique = async ( racine , chemin , texte ) => {
ecritures += 1 ;
if ( ecritures === 2 ) fs . pannes . ecriture ( racine . id ) ;
return ecrireAtomique ( racine , chemin , texte ) ;
} ;
await refus ( seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport . echec ] , [ 'lecture' , true , 'ETAT_NON_ECRIT' ] ) ;
assert . equal ( seance . charge . evenement . nom , BASE ) ;
assert . equal ( fs . contenu ( 'portable' , ETAT ) , null ) ;
const { base } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , verrouDe ( fs ) ] , [ false , 'lecture' , null ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION ] ) ;
assert . equal ( etatDe ( fs ) . entete . revision , 1 ) ;
} ) ;
test ( "un verrou que le support refuse rompt le support : « Modifier » est refusé, puis écrire ailleurs rétablit l'écriture" , async ( ) => {
const { fs , seance , planificateur } = await surPortable ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . deepEqual ( [ seance . mode , verrouDe ( fs , 'portable' ) ] , [ 'lecture' , null ] ) ;
fs . pannes . ecriture ( 'portable' ) ;
const details = { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
await refus ( seance . passerEnEcriture ( ) , 'ECRITURE' , details ) ;
const refusSupport = { echec : 'VERROU_NON_ECRIT' , ... details } ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport ] , [ 'lecture' , true , refusSupport ] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
const avant = fs . compteurs . ecritures ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seance . passerEnEcriture ( ) , 'SUPPORT_ROMPU' , { ... refusSupport , remede : versRacine ( DOC ) } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
assert . equal ( fs . compteurs . ecritures , avant ) ;
const { base } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , seance . racine ] , [ false , 'lecture' , DOC ] ) ;
await seance . passerEnEcriture ( ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
} ) ;
test ( "débloquer devant un verrou que le support refuse rompt aussi le support, et le plan se débloque ailleurs" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . equal ( seance . mode , 'lecture' ) ;
fs . pannes . ecriture ( 'portable' ) ;
const details = { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
await refus ( seance . debloquer ( ) , 'ECRITURE' , details ) ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport ] , [ 'lecture' , true , { echec : 'VERROU_NON_ECRIT' , ... details } ] ) ;
assert . equal ( seance . charge . evenement . etat , 'bloque' ) ;
await seance . ecrireAilleurs ( DOC ) ;
const rendu = await seance . debloquer ( ) ;
assert . equal ( rendu . libelle , 'Plan débloqué, état « Brouillon »' ) ;
assert . deepEqual ( [ seance . mode , verrouDe ( fs ) ? . seance ] , [ 'ecriture' , seance . id ] ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'brouillon' ) ;
} ) ;
test ( "écrire ailleurs dont le verrou ne s'écrit pas là-bas : la paire y est, la séance y passe en lecture, son support de nouveau rompu" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
// Les Documents prennent la paire, mais refusent le verrou.
const details = { chemin : VERROU , dossier : DOCUMENTS , cause : 'EACCES' } ;
const verrouiller = fs . verrouiller ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
fs . verrouiller = async ( racine , chemin , jeton , depuis ) => {
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
if ( racine . id === 'documents' ) throw new ErreurStockage ( 'ECRITURE' , details ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
return verrouiller ( racine , chemin , jeton , depuis ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
} ;
const { base , avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( avertissements . map ( ( { code } ) => code ) , [ 'VERROU_NON_RENDU' , 'ECRITURE' ] ) ;
const refusSupport = { echec : 'VERROU_NON_ECRIT' , ... details } ;
assert . deepEqual ( [ seance . racine , seance . mode , seance . supportRompu , seance . refusSupport ] , [ DOC , 'lecture' , true , refusSupport ] ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' ] ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// Le support rompu est celui des Documents : aucune racine à proposer.
await refus ( seance . passerEnEcriture ( ) , 'SUPPORT_ROMPU' , { ... refusSupport , remede : versRacine ( null ) } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
} ) ;
test ( "écrire ailleurs refuse un dossier qui porte déjà l'événement, le sien compris : jamais deux copies d'un identifiant dans un dossier" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
fs . pannes . renommage ( 'documents' , 11 ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ETAT_NON_ECRIT' ) ;
assert . equal ( seance . supportRompu , true ) ;
const avant = fs . compteurs . ecritures ;
const id = seance . charge . evenement . id ;
const erreur = await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , DOC ] ) ;
const etats = ( await fs . lister ( DOC , '' ) ) . filter ( ( { nom } ) => nom . endsWith ( SUFFIXES . etat ) ) . map ( ( { nom } ) => nom ) ;
assert . deepEqual ( etats , [ ETAT ] ) ;
assert . doesNotMatch ( libelle ( erreur . code , erreur . details ) , /import/ ) ;
} ) ;
test ( "un état qu'écrire ailleurs ne pose pas y transporte la séance, avec le journal écrit : un nouvel essai n'y dérive pas une seconde base" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
// La deuxième écriture atomique dans les Documents, l'état qui suit le
// journal, voit refusés tous ses renommages.
const ecrireAtomique = fs . ecrireAtomique ;
let ecritures = 0 ;
fs . ecrireAtomique = async ( racine , chemin , texte ) => {
if ( racine . id === 'documents' ) {
ecritures += 1 ;
if ( ecritures === 2 ) fs . pannes . renommage ( 'documents' , 11 ) ;
}
return ecrireAtomique ( racine , chemin , texte ) ;
} ;
const id = seance . charge . evenement . id ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . racine , seance . ouvert . racine , seance . ouvert . base ] , [ DOC , DOC , BASE ] ) ;
const refusSupport = { echec : 'ETAT_NON_ECRIT' , chemin : ETAT , dossier : DOCUMENTS , cause : 'EBUSY' } ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport ] , [ 'lecture' , true , refusSupport ] ) ;
assert . deepEqual ( seance . avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' ] ) ;
assert . equal ( fs . contenu ( 'documents' , ETAT ) , null ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
await refus ( seance . ecrireAilleurs ( PORTABLE ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
const fichiers = ( await fs . lister ( DOC , '' ) ) . map ( ( { nom } ) => nom ) ;
assert . deepEqual ( fichiers , [ JOURNAL ] ) ;
} ) ;
// L'événement d'essai des Documents, dont l'état devient un JSON tronqué et
// dont le journal reste lisible : ouvrir le refuse, la séance l'ouvre par
// le secours du journal et passe en écriture, puis l'état d'un geste ne
// s'écrit pas — le geste est au journal, le support est rompu.
async function illisibleRompu ( ) {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
await deposerEssai ( fs ) ;
fs . deposer ( 'documents' , ETAT , fs . contenu ( 'documents' , ETAT ) . slice ( 0 , 40 ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
const illisible = await refus ( seance . ouvrir ( BASE ) , 'ETAT_ILLISIBLE' ) ;
assert . deepEqual ( [ illisible . details . raison , illisible . details . secours ] , [ 'JSON' , { precedent : null , journal : 1 } ] ) ;
await seance . ouvrirSecours ( BASE , 'journal' ) ;
await seance . passerEnEcriture ( ) ;
fs . pannes . renommage ( 'documents' , 11 ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , DOC ] ) ;
return montage ;
}
// Les noms des fichiers d'une racine.
const fichiersDe = async ( fs , racine ) => ( await fs . lister ( racine , '' ) ) . map ( ( { nom } ) => nom ) ;
test ( "écrire ailleurs refuse le dossier où l'événement n'a qu'un état illisible, que son journal apparie : aucune seconde paire, et ce journal reste son secours" , async ( ) => {
const { fs , seance } = await illisibleRompu ( ) ;
const id = seance . charge . evenement . id ;
const avant = fs . compteurs . ecritures ;
const fichiers = await fichiersDe ( fs , DOC ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( await fichiersDe ( fs , DOC ) , fichiers ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , DOC ] ) ;
const illisible = await refus ( ouvrirDansLeDepot ( fs , DOC , BASE ) , 'ETAT_ILLISIBLE' ) ;
assert . deepEqual ( illisible . details . secours , { precedent : null , journal : 2 } ) ;
} ) ;
test ( "écrire ailleurs refuse de revenir au dossier d'où l'événement est parti, quand son état y est illisible" , async ( ) => {
const { fs , seance } = await illisibleRompu ( ) ;
const id = seance . charge . evenement . id ;
await seance . ecrireAilleurs ( PORTABLE ) ;
assert . deepEqual ( [ seance . racine , seance . supportRompu , seance . mode ] , [ PORTABLE , false , 'ecriture' ] ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const avant = fs . compteurs . ecritures ;
const fichiers = await fichiersDe ( fs , DOC ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( await fichiersDe ( fs , DOC ) , fichiers ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , PORTABLE ] ) ;
} ) ;
[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 ( "écrire ailleurs sur un support amovible l'annonce, comme la règle du dossier de travail au démarrage (§ 8.8)" , async ( ) => {
const { fs , seance } = await surPortable ( { support : { documents : 'amovible' } } ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const amovible = { code : 'SUPPORT_AMOVIBLE' , details : { dossier : DOCUMENTS } } ;
const { avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . deepEqual ( avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
amovible ,
] ) ;
assert . ok ( seance . avertissements . some ( ( { code } ) => code === 'SUPPORT_AMOVIBLE' ) ) ;
assert . match ( libelle ( amovible . code , amovible . details ) , /Gestion table tournante Libre/ ) ;
} ) ;
const fautesDuPont = [
[ 'stockage' , ( ) => new ErreurStockage ( 'LECTURE' , { chemin : '' , dossier : DOCUMENTS , cause : 'EIO' } ) ] ,
[ 'pont' , ( ) => new TypeError ( 'réponse du pont illisible pour typeSupport' ) ] ,
] ;
for ( const [ nature , faute ] of fautesDuPont ) {
test ( ` écrire ailleurs réussit quand le type du support ne se dit pas (faute ${ nature } ) : le support vaut inconnu, rien n'est annoncé ` , async ( ) => {
const { fs , seance , planificateur } = await surPortable ( { support : { documents : 'amovible' } } ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
fs . typeSupport = async ( ) => {
throw faute ( ) ;
} ;
const { base , avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
] ) ;
assert . deepEqual ( seance . avertissements , avertissements ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , seance . racine , planificateur . actives ] , [ false , 'ecriture' , DOC , 1 ] ) ;
} ) ;
}
test ( "tant que le support est rompu, ouvrir, ouvrir un secours, créer et fermer sont refusés SUPPORT_ROMPU_NON_SAUVE, sans rien changer" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const refusSupport = { echec : 'ECRITURE' , chemin : JOURNAL , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const nonSauve = { ... refusSupport , remede : versRacine ( DOC ) } ;
[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 avant = fs . compteurs . ecritures ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
await refus ( seance . ouvrir ( BASE ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
await refus ( seance . ouvrirSecours ( BASE , 'journal' ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
await refus ( seance . creer ( { nom : 'Soirée des Grillons' , date : null , siegesParDefaut : 2 , tours : 2 } ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
await refus ( seance . fermer ( ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
[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
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport , noms ( seance . charge ) ] , [ true , refusSupport , [ 'Ancolie' ] ] ) ;
assert . match ( libelle ( 'SUPPORT_ROMPU_NON_SAUVE' , refusSupport ) , /« Écrire ailleurs »/ ) ;
await seance . ecrireAilleurs ( DOC ) ;
await seance . fermer ( ) ;
assert . equal ( seance . ouvert , null ) ;
} ) ;
test ( "le support rompu se quitte sur abandon explicite : ouvrir, ou fermer, avec {abandonner: true}" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const rendu = await seance . ouvrir ( BASE , { abandonner : true } ) ;
assert . equal ( rendu . base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , noms ( seance . charge ) ] , [ false , 'lecture' , [ 'Ancolie' ] ] ) ;
await refus ( seance . passerEnEcriture ( ) , 'ECRITURE' ) ;
assert . equal ( seance . supportRompu , true ) ;
await seance . fermer ( { abandonner : true } ) ;
assert . deepEqual ( [ seance . ouvert , seance . supportRompu ] , [ null , false ] ) ;
} ) ;
test ( "le support rompu se quitte sur abandon explicite : créer, ou ouvrir un secours, avec {abandonner: true}" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
// Un état que le support refuse rompt le support ; les renommages
// rétablis ensuite, ce qui suit s'écrit.
const rompre = async ( nom ) => {
await seance . passerEnEcriture ( ) ;
fs . pannes . renommage ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) , 'ETAT_NON_ECRIT' ) ;
fs . pannes . renommage ( 'portable' , 0 ) ;
assert . equal ( seance . supportRompu , true ) ;
} ;
await rompre ( 'Bruyère' ) ;
const grillons = 'Soirée des Grillons' ;
const cree = await seance . creer ( { nom : grillons , date : null , siegesParDefaut : 2 , tours : 2 } , { abandonner : true } ) ;
assert . equal ( cree . base , grillons ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , seance . charge . evenement . nom ] , [ false , 'lecture' , grillons ] ) ;
await rompre ( 'Cerfeuil' ) ;
const secours = await seance . ouvrirSecours ( BASE , 'journal' , { abandonner : true } ) ;
assert . equal ( secours . base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , noms ( seance . charge ) ] , [ false , 'lecture' , [ 'Ancolie' , 'Bruyère' ] ] ) ;
} ) ;
[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 ( "écrire ailleurs qui échoue laisse le support rompu ; sans support rompu, il est refusé SUPPORT_INTACT" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'SUPPORT_INTACT' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
fs . pannes . ecriture ( 'documents' ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'ECRITURE' ) ;
assert . equal ( seance . supportRompu , true ) ;
assert . deepEqual ( seance . racine , PORTABLE ) ;
} ) ;
} ) ;
describe ( "séance : ni horloge ni minuterie lues en direct (§ 14.7)" , ( ) => {
// Commentaires et chaînes blanchis : seul le code compte.
const JETONS = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\[\s\S]|[^`\\])*`/g ;
const code = ( texte ) => texte . replace ( JETONS , ( jeton ) => jeton . replace ( /[^\n]/g , ' ' ) ) ;
test ( "ni la séance, ni les commandes, ni la table des libellés ne lisent l'horloge, un aléa ou une minuterie" , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
for ( const fichier of [ 'seance.js' , 'avertissements.js' , 'commandes.js' , 'libelles.js' ] ) {
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
const texte = code ( readFileSync ( new URL ( ` ./ ${ fichier } ` , import . meta . url ) , 'utf8' ) ) ;
assert . ok ( texte . trim ( ) . length > 0 , ` ${ fichier } vide ` ) ;
for ( const motif of [ /\bDate\b/ , /\bsetTimeout\b/ , /\bsetInterval\b/ , /\bclearTimeout\b/ , /\bperformance\b/ , /\bMath\s*\.\s*random\b/ , /\bcrypto\b/ ] ) {
assert . doesNotMatch ( texte , motif , ` ${ fichier } : ${ motif } ` ) ;
}
}
} ) ;
} ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
// --- La séance observable ----------------------------------------------------
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// Laisse passer les tâches en attente : l'écrivain différé des réglages
// écrit à la suite de sa minuterie, sans que celle-ci rende sa promesse.
[ADD] tests: guards for layers, clock, sources, colours, watch patterns
Guards scan the tree file by file and line by line, fail on an empty scan,
and are each shown red on a temporary tree. They cover the interface import
graph, the Electron bridge, one clock, refused dependencies, how tests are
written, hard-coded strings, translation keys, forbidden words, colours and
the conflict hue, markup, pruned selectors and watch patterns. Coverage
keeps the engine integration test on a small machine. Fixes: delays in
tests, the conflict hatch class; « la rend évitable » is a named exception
(§ 5.6). Checked: 2587 node, 465 browser tests, from the index alone.
--- FR ---
[ADD] épreuves : gardes des couches, horloge, sources, couleurs, motifs
Les gardes relèvent par fichier et par ligne, échouent sur un balayage vide
et sont chacune montrées rouges sur un arbre temporaire. Elles couvrent le
graphe d'imports de l'interface, le pont d'Electron, l'horloge unique, les
dépendances refusées, l'écriture des épreuves, les chaînes en dur, les clés
de traduction, les mots interdits, les couleurs et la teinte de conflit, le
balisage, les sélecteurs élagués et les motifs de surveillance. Corrigés :
délais d'épreuves, classe de la trame de conflit ; « la rend évitable » est
une exception nommée (§ 5.6). Vérifié : 2587 node, 465 navigateur.
Assisted-by: Claude Opus 5.5
2026-10-07 13:21:21 -04:00
// setImmediate rappelle une fois vidées les microtâches, où s'enchaînent
// les écritures du système de fichiers d'épreuve ; il n'attend aucune durée.
const laisser = ( ) => new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// Compte les objets atteints depuis valeur, chacun une fois, en affirmant
// que chacun est figé.
function compterFiges ( valeur , vus = new Set ( ) ) {
if ( valeur === null || typeof valeur !== 'object' || vus . has ( valeur ) ) return 0 ;
vus . add ( valeur ) ;
assert . ok ( Object . isFrozen ( valeur ) , ` objet non figé : ${ JSON . stringify ( valeur ) . slice ( 0 , 80 ) } ` ) ;
return 1 + Object . keys ( valeur ) . reduce ( ( n , cle ) => n + compterFiges ( valeur [ cle ] , vus ) , 0 ) ;
}
const codesDe = ( avertissements ) => avertissements . map ( ( { code } ) => code ) ;
// La charge d'essai, d'un plan bloqué.
const chargeBloquee = ( ) => {
const charge = chargeEssai ( ) ;
charge . evenement . etat = 'bloque' ;
return charge ;
} ;
// Le retenu de la proposition 1 posé sur la charge d'essai : Iris et
// Grisaille à la table 1 au premier tour, Iris et Théo au second.
function avecRetenu ( charge ) {
charge . retenu = {
proposition : 1 ,
siegesAttribues : false ,
tables : [ 1 , 2 ] ,
capacites : [ 2 , 2 ] ,
tours : 2 ,
participants : [ 1 , 2 , 3 , 4 ] ,
placement : [
{ sieges : [ [ 1 , 2 ] , [ 3 , 4 ] ] , reserve : [ ] } ,
{ sieges : [ [ 1 , 3 ] , [ 2 , 4 ] ] , reserve : [ ] } ,
] ,
} ;
charge . prochainsIds . proposition = 2 ;
charge . evenement . etat = 'retenu' ;
return charge ;
}
// Grisaille réservée à la table 2 au premier tour, où le retenu l'assoit à
// la table 1.
const reserverGrisaille = ( charge ) => {
charge . reservations . push ( { participant : 2 , table : 2 , siege : null , portee : 'tour' , tour : 1 } ) ;
return charge ;
} ;
// Un montage dont l'événement d'essai, de cette charge, est ouvert.
async function ouvertSur ( charge = chargeEssai ( ) , options = { } ) {
const fs = options . fs ? ? creerFichiersSimules ( ) ;
await deposerEssai ( fs , charge ) ;
const montage = monter ( { ... options , fs } ) ;
await montage . seance . ouvrir ( BASE ) ;
return montage ;
}
// Un fichier d'état de la charge d'essai, au format donné.
function deposerAuFormat ( fs , format , charge = chargeEssai ( ) ) {
fs . deposerDossier ( 'documents' ) ;
const texte = serialiser ( charge , { revision : 1 , produitVersion : V1 } ) ;
fs . deposer ( 'documents' , ETAT , texte . replace ( ` "format": ${ FORMAT } ` , ` "format": ${ format } ` ) ) ;
}
const MODIFIER = { geste : 'modifier' } ;
const DEBLOQUER = { geste : 'debloquer' } ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : etat() et subscribe (§ 8.4)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( 'subscribe appelle aussitôt, puis après ouvrir, passer en écriture, un geste et passer en lecture ; désabonné, plus rien' , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const { seance } = monter ( { fs } ) ;
const recus = [ ] ;
const desabonner = seance . subscribe ( ( etat ) => recus . push ( etat ) ) ;
assert . equal ( recus . length , 1 ) ;
assert . equal ( recus [ 0 ] , seance . etat ( ) ) ;
assert . equal ( recus [ 0 ] . evenement , null ) ;
await seance . ouvrir ( BASE ) ;
assert . deepEqual ( [ recus . at ( - 1 ) . evenement . base , recus . at ( - 1 ) . mode ] , [ BASE , 'lecture' ] ) ;
await seance . passerEnEcriture ( ) ;
assert . equal ( recus . at ( - 1 ) . mode , 'ecriture' ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual ( noms ( recus . at ( - 1 ) . charge ) , [ 'Ombrelle' , 'Grisaille' , 'Pervenche' , 'Lacasse' , 'Ancolie' ] ) ;
await seance . passerEnLecture ( ) ;
assert . equal ( recus . at ( - 1 ) . mode , 'lecture' ) ;
assert . ok ( recus . length >= 5 , ` ${ recus . length } appels ` ) ;
assert . equal ( new Set ( recus . slice ( 1 ) ) . size , recus . length - 1 , 'un état rendu deux fois' ) ;
const n = recus . length ;
desabonner ( ) ;
await seance . passerEnEcriture ( ) ;
seance . debuterCalcul ( ) ;
assert . equal ( recus . length , n ) ;
} ) ;
test ( "l'état rendu n'est jamais muté : figé en profondeur, et une copie d'avant compare égale après les gestes suivants" , async ( ) => {
const { seance } = await ouvertSur ( ) ;
await seance . passerEnEcriture ( ) ;
const etat = seance . etat ( ) ;
const copie = structuredClone ( etat ) ;
assert . ok ( compterFiges ( etat ) > 20 ) ;
assert . throws ( ( ) => etat . charge . participants . push ( personne ( 9 , 'Ancolie' ) ) , TypeError ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
await seance . executer ( 'exclureParticipant' , { id : 3 } ) ;
await seance . defaire ( ) ;
assert . deepEqual ( etat , copie ) ;
assert . notEqual ( seance . etat ( ) , etat ) ;
assert . equal ( seance . etat ( ) , seance . etat ( ) , "deux lectures sans changement rendent le même état" ) ;
compterFiges ( seance . etat ( ) ) ;
} ) ;
[IMP] proposal evaluation: cached per frozen charge, validated once
The Génération and Propositions tabs re-evaluated every proposal on each
visit, about 1 ms per proposal, with no bound on how many accumulate.
The evaluation table is now kept per frozen event charge, which every
write replaces, and both tabs share it. Each placement is validated once
instead of three times; the public derive and planDepuisFichier still
validate, and a guard keeps the unchecked variants to evaluer alone.
At 200 proposals: Génération 214 to 17 ms on revisit, Propositions 310
to 118 ms. Checked: results deep-equal; node 4019, browser 873.
--- FR ---
[IMP] évaluation des propositions : gardée par charge, validée une fois
Les onglets Génération et Propositions réévaluaient toutes les
propositions à chaque visite, environ 1 ms chacune, sans borne sur leur
nombre. La table d'évaluation se garde par charge figée, qu'une écriture
remplace, et les deux onglets la partagent. Chaque placement se valide
une fois au lieu de trois ; derive et planDepuisFichier publics valident
toujours, une garde réserve les variantes sans contrôle à evaluer.
À 200 propositions : Génération 214 → 17 ms au retour, Propositions
310 → 118 ms. Vérifié : résultats identiques ; node 4019, nav. 873.
Assisted-by: Claude Opus 5.5
2026-10-11 03:43:47 -04:00
test ( "la charge de l'état garde son identité tant que l'événement ne change pas, et chaque geste en rend une neuve, figée" , async ( ) => {
const { seance } = await ouvertSur ( ) ;
await seance . passerEnEcriture ( ) ;
const etat = seance . etat ( ) ;
const { charge } = etat ;
seance . debuterCalcul ( ) ;
seance . finirCalcul ( ) ;
assert . notEqual ( seance . etat ( ) , etat ) ;
assert . equal ( seance . etat ( ) . charge , charge ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const apres = seance . etat ( ) . charge ;
assert . notEqual ( apres , charge ) ;
compterFiges ( apres ) ;
await seance . executer ( 'exclureParticipant' , { id : 3 } ) ;
assert . notEqual ( seance . etat ( ) . charge , apres ) ;
compterFiges ( seance . etat ( ) . charge ) ;
} ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "l'état porte l'événement, la charge, l'examen, les retours, le calcul en cours ; sans événement, rien" , async ( ) => {
const vide = monter ( ) . seance . etat ( ) ;
assert . deepEqual (
[ vide . evenement , vide . charge , vide . examen , vide . fautives , vide . avertissements , vide . refus , vide . supportRompu , vide . verrouAutre ] ,
[ null , null , null , [ ] , [ ] , null , null , null ] ,
) ;
assert . deepEqual ( [ vide . mode , vide . retours , vide . calculEnCours , vide . racine ] , [ 'lecture' , { defaire : false , refaire : false } , false , DOC ] ) ;
const { seance } = await ouvertSur ( ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const etat = seance . etat ( ) ;
assert . deepEqual ( etat . evenement , { base : BASE , id : 'evt-lucioles' , nom : BASE , etat : 'brouillon' , revision : 2 , annonceFormat : null } ) ;
assert . deepEqual ( etat . charge , seance . charge ) ;
assert . deepEqual ( etat . examen , seance . ouvert . examen ) ;
assert . deepEqual ( etat . retours , { defaire : true , refaire : false } ) ;
const recus = [ ] ;
seance . subscribe ( ( suivant ) => recus . push ( suivant ) ) ;
seance . debuterCalcul ( ) ;
assert . deepEqual ( [ seance . etat ( ) . calculEnCours , recus . at ( - 1 ) . calculEnCours ] , [ true , true ] ) ;
seance . finirCalcul ( ) ;
assert . deepEqual ( [ seance . etat ( ) . calculEnCours , recus . at ( - 1 ) . calculEnCours ] , [ false , false ] ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : la racine (§ 8.6)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "une séance créée sans racine n'en a pas ; changerRacine la pose sans rien écrire" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
const { seance } = monter ( { fs , racine : null } ) ;
assert . equal ( seance . etat ( ) . racine , null ) ;
const avant = fs . compteurs . ecritures ;
const recus = [ ] ;
seance . subscribe ( ( etat ) => recus . push ( etat ) ) ;
await seance . changerRacine ( DOC ) ;
assert . deepEqual ( [ seance . etat ( ) . racine , recus . at ( - 1 ) . racine ] , [ DOC , DOC ] ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
await seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) ;
assert . equal ( seance . etat ( ) . evenement . base , BASE ) ;
} ) ;
test ( "changerRacine ferme l'événement ouvert en écriture : son verrou quitte le disque, et la séance ouvre désormais là" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
fs . deposerDossier ( 'portable' ) ;
const montage = monter ( { fs } ) ;
await enEcriture ( montage ) ;
assert . equal ( verrouDe ( fs ) . seance , montage . seance . id ) ;
await montage . seance . changerRacine ( PORTABLE ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . deepEqual ( [ montage . seance . etat ( ) . evenement , montage . seance . etat ( ) . racine , montage . seance . etat ( ) . mode ] , [ null , PORTABLE , 'lecture' ] ) ;
await refus ( montage . seance . ouvrir ( BASE ) , 'ABSENT' ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : la version de format (§ 8.8, § 18.6)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "un fichier d'un format plus récent s'annonce 'plusRecent' ; passer en écriture et un geste sont refusés LECTURE_SEULE FORMAT, remède null, jamais MODE_LECTURE" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
deposerAuFormat ( fs , FORMAT + 1 ) ;
const { seance } = monter ( { fs } ) ;
const avant = fs . compteurs . ecritures ;
await seance . ouvrir ( BASE ) ;
assert . equal ( seance . etat ( ) . evenement . annonceFormat , 'plusRecent' ) ;
const format = { raison : 'FORMAT' , remede : null } ;
await refus ( seance . passerEnEcriture ( ) , 'LECTURE_SEULE' , format ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) , 'LECTURE_SEULE' , format ) ;
assert . deepEqual ( seance . etat ( ) . refus , { code : 'LECTURE_SEULE' , details : format } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
test ( "au format courant, aucune annonce ; formatCourant au-delà, 'plusAncien' sans rien écrire, puis le premier geste écrit au format FORMAT et l'annonce tombe" , async ( ) => {
const courant = await ouvertSur ( ) ;
assert . equal ( courant . seance . etat ( ) . evenement . annonceFormat , null ) ;
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const { seance } = monter ( { fs , formatCourant : FORMAT + 1 } ) ;
const avant = fs . compteurs . ecritures ;
await seance . ouvrir ( BASE ) ;
assert . equal ( seance . etat ( ) . evenement . annonceFormat , 'plusAncien' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
await seance . passerEnEcriture ( ) ;
assert . equal ( seance . etat ( ) . evenement . annonceFormat , 'plusAncien' , 'le passage en écriture ne convertit rien' ) ;
const entrees = journalDe ( fs ) . entrees . length ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees + 1 ) ;
assert . equal ( etatDe ( fs ) . entete . format , FORMAT ) ;
assert . equal ( seance . etat ( ) . evenement . annonceFormat , null ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : bloquer et débloquer (§ 8.5, § 9)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "sur un plan bloqué, passer en écriture, changer l'état et défaire sont refusés BLOQUE, remède debloquer, jamais MODE_LECTURE, sans écriture" , async ( ) => {
const montage = await ouvertSur ( chargeBloquee ( ) ) ;
const { fs , seance } = montage ;
const avant = fs . compteurs . ecritures ;
const bloque = { raison : 'BLOQUE' , remede : DEBLOQUER } ;
await refus ( seance . passerEnEcriture ( ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . executer ( 'changerEtat' , { etat : 'retenu' } ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . defaire ( ) , 'LECTURE_SEULE' , bloque ) ;
assert . deepEqual ( seance . etat ( ) . refus , { code : 'LECTURE_SEULE' , details : bloque } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
test ( "débloquer prend le verrou, écrit une entrée et un jalon, rend l'état que le contenu impose, et laisse en écriture" , async ( ) => {
const { fs , seance } = await ouvertSur ( chargeBloquee ( ) ) ;
const entrees = journalDe ( fs ) . entrees . length ;
await seance . debloquer ( ) ;
assert . equal ( seance . etat ( ) . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
const journal = journalDe ( fs ) ;
assert . equal ( journal . entrees . length , entrees + 1 ) ;
assert . equal ( journal . jalons . at ( - 1 ) . revision , journal . entrees . at ( - 1 ) . revision ) ;
assert . equal ( seance . etat ( ) . evenement . etat , etatDeduit ( seance . charge ) ) ;
assert . equal ( seance . etat ( ) . refus , null ) ;
} ) ;
test ( 'sur un plan non bloqué ouvert en lecture, un geste est refusé MODE_LECTURE, remède modifier' , async ( ) => {
const { fs , seance } = await ouvertSur ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) , 'MODE_LECTURE' , { remede : MODIFIER } ) ;
await refus ( seance . nommer ( 'Avant le dessert' ) , 'MODE_LECTURE' , { remede : MODIFIER } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
test ( 'bloquer écrit une entrée et un jalon, revient en lecture, rend le verrou ; une seconde séance du même disque ouvre un plan bloqué' , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const entrees = journalDe ( premier . fs ) . entrees . length ;
await premier . seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . equal ( journalDe ( premier . fs ) . entrees . length , entrees + 1 ) ;
assert . equal ( journalDe ( premier . fs ) . jalons . at ( - 1 ) . nom , 'État « Bloqué »' ) ;
assert . deepEqual ( [ premier . seance . etat ( ) . mode , verrouDe ( premier . fs ) ] , [ 'lecture' , null ] ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
assert . equal ( second . seance . etat ( ) . evenement . etat , 'bloque' ) ;
assert . deepEqual ( second . seance . etat ( ) . refus , { code : 'LECTURE_SEULE' , details : { raison : 'BLOQUE' , remede : DEBLOQUER } } ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( "séance observable : l'annulation et le retour automatique (§ 8.4)" , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( 'surAnnulation est appelé au passage en lecture explicite pendant un geste, au retour automatique et à la fermeture ; désabonné, plus rien' , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
let appels = 0 ;
const desabonner = seance . surAnnulation ( ( ) => {
appels += 1 ;
} ) ;
seance . debuterGeste ( ) ;
await seance . passerEnLecture ( ) ;
assert . equal ( appels , 1 ) ;
await seance . passerEnEcriture ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . deepEqual ( [ seance . mode , appels ] , [ 'lecture' , 2 ] ) ;
await seance . fermer ( ) ;
assert . equal ( appels , 3 ) ;
desabonner ( ) ;
await seance . ouvrir ( BASE ) ;
await seance . passerEnEcriture ( ) ;
await seance . passerEnLecture ( ) ;
await seance . fermer ( ) ;
assert . equal ( appels , 3 ) ;
} ) ;
test ( "le retour automatique ne tombe jamais pendant un geste ouvert ; il pose RETOUR_AUTOMATIQUE à la suite des avertissements, que le passage en écriture suivant retire" , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
const ajout = await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual ( codesDe ( ajout . avertissements ) , [ 'PLACES_MANQUANTES' ] ) ;
let appels = 0 ;
seance . surAnnulation ( ( ) => {
appels += 1 ;
} ) ;
seance . debuterGeste ( ) ;
await planificateur . avancer ( 3 * INACTIVITE _MS ) ;
assert . deepEqual ( [ seance . mode , appels ] , [ 'ecriture' , 0 ] ) ;
assert . ok ( ! codesDe ( seance . etat ( ) . avertissements ) . includes ( 'RETOUR_AUTOMATIQUE' ) ) ;
seance . finirGeste ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . deepEqual ( [ seance . mode , appels ] , [ 'lecture' , 1 ] ) ;
const retour = { code : 'RETOUR_AUTOMATIQUE' , details : { } } ;
assert . deepEqual ( seance . etat ( ) . avertissements , [ ... seance . ouvert . avertissements , ... ajout . avertissements , retour ] ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( codesDe ( seance . etat ( ) . avertissements ) , [ ] ) ;
await seance . passerEnLecture ( ) ;
assert . ok ( ! codesDe ( seance . etat ( ) . avertissements ) . includes ( 'RETOUR_AUTOMATIQUE' ) , 'le retour explicite se tait' ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : le cadrage (§ 8.5)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const REGLAGES = ` {"cadrages":[{"evenement":"evt-lucioles","k":2,"tx":10,"ty":-5}],"format":1,"papier":null} \n ` ;
const lireCadrages = ( fs ) => JSON . parse ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) ) . cadrages ;
for ( const [ cas , charge , ecriture ] of [
[ 'en lecture' , chargeEssai , false ] ,
[ 'sur un plan bloqué' , chargeBloquee , false ] ,
[ 'en écriture' , chargeEssai , true ] ,
] ) {
test ( ` ${ cas } : ouvrir relit le cadrage ; vingt demandes en 500 ms font une seule écriture des réglages, ${ DELAI _REGLAGES _MS } ms après la première, le dernier cadrage, sans toucher l'état ni le journal ` , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs , charge ( ) ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , REGLAGES ) ;
const montage = monter ( { fs } ) ;
const { seance , planificateur } = montage ;
await seance . ouvrir ( BASE ) ;
if ( ecriture ) await seance . passerEnEcriture ( ) ;
assert . deepEqual ( seance . cadrage ( ) , { k : 2 , tx : 10 , ty : - 5 } ) ;
const avant = fs . compteurs . ecritures ;
const revision = seance . etat ( ) . evenement . revision ;
const textes = [ fs . contenu ( 'documents' , ETAT ) , fs . contenu ( 'documents' , JOURNAL ) ] ;
for ( let rang = 0 ; rang < 20 ; rang += 1 ) {
seance . demanderCadrage ( { k : 1 + rang / 10 , tx : rang , ty : - rang } ) ;
await planificateur . avancer ( 25 ) ;
}
assert . deepEqual ( seance . cadrage ( ) , { k : 2.9 , tx : 19 , ty : - 19 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS - 500 - 1 ) ;
await laisser ( ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
await planificateur . avancer ( 1 ) ;
await laisser ( ) ;
assert . equal ( fs . compteurs . ecritures , avant + 1 ) ;
assert . deepEqual ( lireCadrages ( fs ) , [ { evenement : 'evt-lucioles' , k : 2.9 , tx : 19 , ty : - 19 } ] ) ;
assert . deepEqual ( [ fs . contenu ( 'documents' , ETAT ) , fs . contenu ( 'documents' , JOURNAL ) ] , textes ) ;
assert . equal ( seance . etat ( ) . evenement . revision , revision ) ;
} ) ;
}
test ( "une vue de k nul ou non fini n'est pas demandée ; une écriture refusée se tait" , async ( ) => {
const { fs , seance , planificateur } = await ouvertSur ( ) ;
const avant = fs . compteurs . ecritures ;
for ( const vue of [ { k : 0 , tx : 0 , ty : 0 } , { k : Number . NaN , tx : 0 , ty : 0 } , { k : Infinity , tx : 0 , ty : 0 } , { k : 1 , tx : Number . NaN , ty : 0 } ] ) {
seance . demanderCadrage ( vue ) ;
}
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( seance . cadrage ( ) , null ) ;
fs . pannes . ecriture ( 'documents' ) ;
seance . demanderCadrage ( { k : 1.5 , tx : 3 , ty : 4 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , null ) ;
assert . deepEqual ( seance . cadrage ( ) , { k : 1.5 , tx : 3 , ty : 4 } ) ;
await seance . fermer ( ) ;
} ) ;
test ( "fermer écrit aussitôt une demande en attente ; rouvrir la rend ; défaire ne la change pas" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance , planificateur } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
seance . demanderCadrage ( { k : 0.5 , tx : - 40 , ty : 12 } ) ;
await seance . fermer ( ) ;
assert . equal ( planificateur . actives , 0 ) ;
const { id } = etatDe ( fs ) . charge . evenement ;
assert . deepEqual ( lireCadrages ( fs ) , [ { evenement : id , k : 0.5 , tx : - 40 , ty : 12 } ] ) ;
await seance . ouvrir ( BASE ) ;
assert . deepEqual ( seance . cadrage ( ) , { k : 0.5 , tx : - 40 , ty : 12 } ) ;
await seance . passerEnEcriture ( ) ;
await seance . defaire ( ) ;
assert . deepEqual ( seance . cadrage ( ) , { k : 0.5 , tx : - 40 , ty : 12 } ) ;
const ailleurs = monter ( { fs : creerFichiersSimules ( { disque : new Map ( ) } ) } ) ;
assert . equal ( ailleurs . seance . cadrage ( ) , null , 'sans événement, aucun cadrage' ) ;
} ) ;
test ( "une lecture des réglages qui lève ne retient pas l'ouverture : cadrage() null" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const lireTexte = fs . lireTexte ;
fs . lireTexte = async ( racine , chemin ) => {
if ( chemin === FICHIER _REGLAGES ) throw new TypeError ( 'réponse illisible du système' ) ;
return lireTexte ( racine , chemin ) ;
} ;
const { seance } = monter ( { fs } ) ;
await seance . ouvrir ( BASE ) ;
assert . deepEqual ( [ seance . etat ( ) . evenement . base , seance . cadrage ( ) ] , [ BASE , null ] ) ;
} ) ;
test ( "des réglages que la lecture ne prend pas ne s'écrasent pas : le cadrage attend, le papier et les autres cadrages restent" , async ( ) => {
const AUTRES = ` {"cadrages":[{"evenement":"evt-voisin","k":3,"tx":1,"ty":2}],"format":1,"papier":"A4"} \n ` ;
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , AUTRES ) ;
let occupe = false ;
const lireTexte = fs . lireTexte ;
fs . lireTexte = async ( racine , chemin ) => {
if ( occupe && chemin === FICHIER _REGLAGES ) throw new ErreurStockage ( 'LECTURE' , { chemin , cause : 'EBUSY' } ) ;
return lireTexte ( racine , chemin ) ;
} ;
const montage = monter ( { fs } ) ;
const { seance , planificateur } = montage ;
await seance . ouvrir ( BASE ) ;
occupe = true ;
seance . demanderCadrage ( { k : 1 , tx : 0 , ty : 0 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , AUTRES , 'une lecture refusée laisse le fichier' ) ;
assert . deepEqual ( seance . cadrage ( ) , { k : 1 , tx : 0 , ty : 0 } ) ;
occupe = false ;
await seance . fermer ( ) ;
const relus = JSON . parse ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) ) ;
assert . equal ( relus . papier , 'A4' ) ;
assert . deepEqual ( relus . cadrages , [
{ evenement : 'evt-lucioles' , k : 1 , tx : 0 , ty : 0 } ,
{ evenement : 'evt-voisin' , k : 3 , tx : 1 , ty : 2 } ,
] ) ;
for ( const corrompu of [ 'pas du json\n' , '{"cadrages":[],"format":0,"papier":"A4"}\n' ] ) {
fs . deposer ( 'documents' , FICHIER _REGLAGES , corrompu ) ;
await seance . ouvrir ( BASE ) ;
seance . demanderCadrage ( { k : 2 , tx : 5 , ty : 5 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
await seance . fermer ( ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , corrompu , corrompu ) ;
}
} ) ;
test ( "des réglages corrompus : cadrage() null, et aucun avertissement de la séance ne le dit" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , 'pas du json\n' ) ;
const { seance } = monter ( { fs } ) ;
await seance . ouvrir ( BASE ) ;
assert . equal ( seance . cadrage ( ) , null ) ;
assert . ok ( ! codesDe ( seance . etat ( ) . avertissements ) . includes ( 'REGLAGES_ILLISIBLES' ) ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( "séance observable : les avertissements d'un geste (§ 5.9, § 9)" , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// Deux tables de huit et seize convives : la salle est pleine.
function sallePleine ( ) {
const charge = creerCharge ( { id : 'evt-lucioles' , nom : BASE , siegesParDefaut : 8 , tours : 2 } ) ;
charge . tables = [ 1 , 2 ] . map ( ( id ) => ( { id , numero : id , sieges : null , forme : 'ronde' , position : { x : 300 * ( id - 1 ) , y : 0 } } ) ) ;
charge . participants = Array . from ( { length : 16 } , ( _ , rang ) => personne ( rang + 1 , ` Convive ${ rang + 1 } ` ) ) ;
charge . prochainsIds = { participant : 17 , table : 3 , proposition : 1 } ;
return charge ;
}
const manqueDe = ( charge ) => nombrePlacesManquantes ( normaliser ( configurationDepuisCharge ( charge ) ) ) ;
test ( "la dix-septième personne avertit PLACES_MANQUANTES {1}, trois de plus {4}, le nombre du moteur ; un geste qui ne fait pas croître le manque n'en dit rien" , async ( ) => {
const { seance } = await ouvertSur ( sallePleine ( ) ) ;
await seance . passerEnEcriture ( ) ;
const ajout = await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual ( ajout . avertissements , [ { code : 'PLACES_MANQUANTES' , details : { placesManquantes : 1 } } ] ) ;
assert . equal ( manqueDe ( seance . charge ) , 1 ) ;
const apercu = apercevoir ( 'nom\nBruyère\nCerfeuil\nDahlia\n' , { participants : seance . charge . participants } ) ;
const import3 = await seance . executer ( 'importerParticipants' , { apercu , mode : 'ajouter' } ) ;
assert . deepEqual ( import3 . avertissements , [ { code : 'PLACES_MANQUANTES' , details : { placesManquantes : 4 } } ] ) ;
assert . equal ( manqueDe ( seance . charge ) , 4 ) ;
assert . deepEqual ( seance . etat ( ) . avertissements , [ ... seance . ouvert . avertissements , ... import3 . avertissements ] ) ;
const renomme = await seance . executer ( 'modifierParticipant' , { id : 1 , champs : { nom : 'Parasol' } } ) ;
assert . deepEqual ( renomme . avertissements , [ ] ) ;
const retire = await seance . executer ( 'supprimerParticipant' , { id : 2 } ) ;
assert . deepEqual ( retire . avertissements , [ ] ) ;
} ) ;
test ( "après la retenue, exclure une personne : DERIVE_NOUVELLE nomme le retenu et PARTICIPANT_EXCLU, sans lever ; le geste suivant passe sans redire cette raison" , async ( ) => {
const { seance } = await ouvertSur ( ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
const exclusion = await seance . executer ( 'exclureParticipant' , { id : 3 } ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
const raisons = [ { code : 'PARTICIPANT_EXCLU' , participant : 3 , personne : 'Théo Pervenche' } ] ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . deepEqual ( exclusion . avertissements , [
{ code : 'DERIVE_NOUVELLE' , details : { touchees : [ { id : 1 , raisons } , { id : 2 , raisons } , { id : 'retenu' , raisons } ] } } ,
] ) ;
const suivant = await seance . executer ( 'modifierParticipant' , { id : 4 , champs : { nom : 'Lacasse-Ombrelle' } } ) ;
assert . deepEqual ( suivant . avertissements , [ ] ) ;
const autre = await seance . executer ( 'exclureParticipant' , { id : 2 } ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
const nouvelle = [ { code : 'PARTICIPANT_EXCLU' , participant : 2 , personne : 'Grisaille' } ] ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . deepEqual ( autre . avertissements , [
{ code : 'DERIVE_NOUVELLE' , details : { touchees : [ { id : 1 , raisons : nouvelle } , { id : 2 , raisons : nouvelle } , { id : 'retenu' , raisons : nouvelle } ] } } ,
] ) ;
} ) ;
test ( "un fichier au retenu fautif : le geste passe, et ni DERIVE_NOUVELLE ni RESERVATION_NON_HONOREE ne vient du retenu" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
const charge = avecRetenu ( chargeEssai ( ) ) ;
charge . retenu . placement [ 0 ] . reserve = [ 2 ] ;
await deposerEssai ( fs , reserverGrisaille ( charge ) ) ;
const montage = monter ( { fs } ) ;
await ouvrirSansEcrire ( montage ) ;
const { seance } = montage ;
assert . equal ( seance . etat ( ) . avertissements [ 0 ] . code , 'RETENU_FAUTIF' ) ;
await seance . passerEnEcriture ( ) ;
const exclusion = await seance . executer ( 'exclureParticipant' , { id : 3 } ) ;
assert . deepEqual ( exclusion . avertissements , [ ] ) ;
assert . ok ( ! codesDe ( seance . etat ( ) . avertissements ) . includes ( 'INVARIANT_VIOLE' ) ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( "séance observable : le verrou d'une autre séance (§ 8.8)" , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( 'la seconde séance ouvre en lecture sans refus ; son passage en écriture est refusé VERROU_PRIS, remède null, et verrouAutre porte la séance et depuis' , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
assert . equal ( second . seance . etat ( ) . verrouAutre , null ) ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : 'premiere-1' , depuis , vivant : true , remede : null } ) ;
assert . deepEqual ( second . seance . etat ( ) . verrouAutre , { seance : 'premiere-1' , depuis , vivant : true } ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'premiere-1' ) ;
} ) ;
test ( "le verrou d'une séance morte se reprend seul, et VERROU_REPRIS le dit" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
premier . fs . pannes . couperApres ( 0 ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : 'premiere-1' , depuis } } ] ;
assert . deepEqual ( await second . seance . passerEnEcriture ( ) , { avertissements : repris } ) ;
assert . deepEqual ( second . seance . etat ( ) . avertissements , repris ) ;
} ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
test ( "le verrou d'une instance d'un autre hôte (vivant null) n'est jamais repris : VERROU_PRIS, remède null, le fichier inchangé, et aucun geste ne le reprend seul : seul reprendreVerrou, que l'opérateur confirme, le reprend" , async ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const texte = premier . fs . contenu ( 'documents' , VERROU ) ;
const lointain = monter ( { fs : creerFichiersSimules ( { disque , hote : 'autre-poste' } ) , prefixe : 'lointaine' } ) ;
await ouvrirSansEcrire ( lointain ) ;
const { seance } = lointain ;
const pris = { seance : 'premiere-1' , depuis , vivant : null , remede : null } ;
await refus ( seance . passerEnEcriture ( ) , 'VERROU_PRIS' , pris ) ;
await refus ( seance . passerEnEcriture ( ) , 'VERROU_PRIS' , pris ) ;
assert . equal ( premier . fs . contenu ( 'documents' , VERROU ) , texte ) ;
assert . deepEqual ( seance . etat ( ) . verrouAutre , { seance : 'premiere-1' , depuis , vivant : null } ) ;
assert . equal ( seance . etat ( ) . mode , 'lecture' ) ;
} ) ;
test ( "un verrou illisible n'est jamais repris : VERROU_PRIS, remède null, le fichier inchangé" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
fs . deposer ( 'documents' , VERROU , 'pas un verrou\n' ) ;
const montage = monter ( { fs } ) ;
await ouvrirSansEcrire ( montage ) ;
const illisible = { seance : null , depuis : null , vivant : null , remede : null } ;
await refus ( montage . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , illisible ) ;
assert . equal ( fs . contenu ( 'documents' , VERROU ) , 'pas un verrou\n' ) ;
} ) ;
test ( "sur un plan bloqué, débloquer devant le verrou d'un autre hôte est refusé VERROU_PRIS, remède null, sans entrée, le verrou inchangé" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs , chargeBloquee ( ) ) ;
const depuis = '2031-05-17T09:00:00-04:00' ;
const texte = ` ${ JSON . stringify ( { seance : 'lointaine-1' , pid : 99 , hote : 'autre-poste' , depuis } )} \n ` ;
fs . deposer ( 'documents' , VERROU , texte ) ;
const montage = monter ( { fs } ) ;
await ouvrirSansEcrire ( montage ) ;
const { seance } = montage ;
const entrees = journalDe ( fs ) . entrees . length ;
await refus ( seance . debloquer ( ) , 'VERROU_PRIS' , { seance : 'lointaine-1' , depuis , vivant : null , remede : null } ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
assert . deepEqual ( [ seance . etat ( ) . mode , seance . etat ( ) . evenement . etat ] , [ 'lecture' , 'bloque' ] ) ;
assert . equal ( fs . contenu ( 'documents' , VERROU ) , texte ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : le support rompu (§ 8.6, § 8.2)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
// Un événement du disque portable, en écriture, après un premier geste.
async function surPortable ( ) {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
const montage = monter ( { fs , racine : PORTABLE } ) ;
await enEcriture ( montage ) ;
await montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
return montage ;
}
const rompu = { echec : 'ECRITURE' , chemin : JOURNAL , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
const versDocuments = { geste : 'ecrireAilleurs' , racine : DOC } ;
test ( "une écriture refusée remonte ECRITURE ; supportRompu en garde les champs et la racine documents ; le refus courant et le geste suivant sont SUPPORT_ROMPU, remède écrire ailleurs ; écrire ailleurs rétablit" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
const erreur = await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
assert . deepEqual ( erreur . details , { chemin : JOURNAL , dossier : PORTABLE . chemin , cause : 'EROFS' } ) ;
assert . deepEqual ( seance . etat ( ) . supportRompu , { ... rompu , ailleurs : DOC } ) ;
const attendu = { code : 'SUPPORT_ROMPU' , details : { ... rompu , remede : versDocuments } } ;
assert . deepEqual ( seance . etat ( ) . refus , attendu ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'SUPPORT_ROMPU' , attendu . details ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
await seance . ecrireAilleurs ( DOC ) ;
assert . deepEqual ( [ seance . etat ( ) . supportRompu , seance . etat ( ) . racine , seance . etat ( ) . refus ] , [ null , DOC , null ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' ] ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' ] ) ;
} ) ;
test ( "un état qui ne s'écrit pas : ETAT_NON_ECRIT, le geste au journal, l'état du disque intact ; écrire ailleurs écrit là-bas l'état de ce geste" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . renommage ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ETAT_NON_ECRIT' ) ;
assert . equal ( seance . etat ( ) . supportRompu . echec , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs , 'portable' ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
assert . deepEqual ( noms ( etatDe ( fs , 'portable' ) . charge ) , [ 'Ancolie' ] ) ;
await seance . ecrireAilleurs ( DOC ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
} ) ;
test ( 'sous un support rompu, ouvrir, créer, fermer et changerRacine sont refusés SUPPORT_ROMPU_NON_SAUVE, remède écrire ailleurs, sans écriture ; fermer({abandonner: true}) ferme' , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const nonSauve = { ... rompu , remede : versDocuments } ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . ouvrir ( BASE ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
await refus ( seance . creer ( { nom : 'Soirée des Grillons' , date : null , siegesParDefaut : 2 , tours : 2 } ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
await refus ( seance . fermer ( ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
await refus ( seance . changerRacine ( DOC ) , 'SUPPORT_ROMPU_NON_SAUVE' , nonSauve ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( seance . etat ( ) . racine , PORTABLE ) ;
await seance . fermer ( { abandonner : true } ) ;
assert . deepEqual ( [ seance . etat ( ) . evenement , seance . etat ( ) . supportRompu ] , [ null , null ] ) ;
} ) ;
test ( "écrire ailleurs sans racine demande le dossier par fs.choisirDossier : son annulation ne change rien, son choix y écrit" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
let appels = 0 ;
fs . choisirDossier = async ( ) => {
appels += 1 ;
return null ;
} ;
const avant = fs . compteurs . ecritures ;
assert . equal ( await seance . ecrireAilleurs ( null ) , null ) ;
assert . equal ( appels , 1 ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . etat ( ) . supportRompu , seance . etat ( ) . racine ] , [ { ... rompu , ailleurs : DOC } , PORTABLE ] ) ;
fs . deposerDossier ( 'documents' ) ;
fs . choisirDossier = async ( ) => DOC ;
const { base } = await seance . ecrireAilleurs ( null ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( [ seance . etat ( ) . supportRompu , seance . etat ( ) . racine ] , [ null , DOC ] ) ;
} ) ;
test ( "un système qui ne dit pas ses racines : aucun ailleurs n'est proposé" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
fs . racines = async ( ) => {
throw new TypeError ( 'réponse illisible du système' ) ;
} ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
assert . deepEqual ( seance . etat ( ) . supportRompu , { ... rompu , ailleurs : null } ) ;
} ) ;
test ( "le support rompu de la racine documents n'a pas d'ailleurs : le remède laisse l'opérateur désigner le dossier" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
fs . pannes . ecriture ( 'documents' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
assert . equal ( seance . etat ( ) . supportRompu . ailleurs , null ) ;
assert . deepEqual ( seance . etat ( ) . refus . details . remede , { geste : 'ecrireAilleurs' , racine : null } ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : le refus courant (§ 2.1, garantie 3)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "null sans événement et en écriture ; sinon, en profondeur, le code et les détails qu'executer lève, remède compris" , async ( ) => {
assert . equal ( monter ( ) . seance . etat ( ) . refus , null ) ;
const enEcritureMontage = await ouvertSur ( ) ;
await enEcritureMontage . seance . passerEnEcriture ( ) ;
assert . equal ( enEcritureMontage . seance . etat ( ) . refus , null ) ;
const lecture = await ouvertSur ( ) ;
const bloque = await ouvertSur ( chargeBloquee ( ) ) ;
const fsFormat = creerFichiersSimules ( ) ;
deposerAuFormat ( fsFormat , FORMAT + 1 ) ;
const format = monter ( { fs : fsFormat } ) ;
await format . seance . ouvrir ( BASE ) ;
const portable = monter ( { fs : creerFichiersSimules ( { executable : 'E:\\soirees' } ) , racine : PORTABLE } ) ;
await enEcriture ( portable ) ;
portable . fs . pannes . ecriture ( 'portable' ) ;
await refus ( portable . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const cas = [
[ lecture , 'MODE_LECTURE' , { remede : MODIFIER } ] ,
[ bloque , 'LECTURE_SEULE' , { raison : 'BLOQUE' , remede : DEBLOQUER } ] ,
[ format , 'LECTURE_SEULE' , { raison : 'FORMAT' , remede : null } ] ,
[ portable , 'SUPPORT_ROMPU' , { ... { echec : 'ECRITURE' , chemin : JOURNAL , dossier : PORTABLE . chemin , cause : 'EROFS' } , remede : { geste : 'ecrireAilleurs' , racine : DOC } } ] ,
] ;
assert . equal ( cas . length , 4 ) ;
for ( const [ { seance } , code , details ] of cas ) {
assert . deepEqual ( seance . etat ( ) . refus , { code , details } , code ) ;
const erreur = await refus ( seance . executer ( 'changerEtat' , { etat : 'brouillon' } ) , code ) ;
assert . deepEqual ( seance . etat ( ) . refus , { code : erreur . code , details : erreur . details } , code ) ;
}
} ) ;
test ( "les autres refus de la séance portent un remède null" , async ( ) => {
await refus ( monter ( ) . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) , 'AUCUN_EVENEMENT' , { remede : null } ) ;
const montage = monter ( ) ;
await enEcriture ( montage ) ;
await refus ( montage . seance . defaire ( ) , 'RIEN_A_DEFAIRE' , { remede : null } ) ;
await refus ( montage . seance . refaire ( ) , 'RIEN_A_REFAIRE' , { remede : null } ) ;
await refus ( montage . seance . nommer ( ' ' ) , 'NOM_REQUIS' , { remede : null } ) ;
await refus ( montage . seance . revenir ( 99 ) , 'INSTANT_NON_RESTITUABLE' , { revision : 99 , remede : null } ) ;
await refus ( montage . seance . ecrireAilleurs ( DOC ) , 'SUPPORT_INTACT' , { remede : null } ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : fautives et examen (§ 8.9)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "les propositions que l'ouverture écarte restent dans fautives après un geste, et partent à la fermeture ; l'examen d'après un geste n'en porte aucune" , async ( ) => {
const charge = chargeEssai ( ) ;
charge . prochainsIds . proposition = 2 ;
charge . propositions = [
{
id : 1 ,
graine : 7 ,
arret : 40 ,
historique : 1 ,
produitVersion : V1 ,
siegesAttribues : false ,
tables : [ 1 , 2 ] ,
capacites : [ 2 , 2 ] ,
tours : 2 ,
participants : [ 1 , 2 , 3 , 4 ] ,
placement : [
{ sieges : [ [ 1 , 2 , 3 ] , [ 4 ] ] , reserve : [ ] } ,
{ sieges : [ [ 1 , 3 ] , [ 2 , 4 ] ] , reserve : [ ] } ,
] ,
} ,
] ;
const { seance } = await ouvertSur ( charge ) ;
const { fautives } = seance . etat ( ) ;
assert . deepEqual ( fautives . map ( ( { rang , id } ) => [ rang , id ] ) , [ [ 0 , 1 ] ] ) ;
assert . equal ( fautives [ 0 ] . fautes [ 0 ] . code , 'LONGUEUR' ) ;
assert . deepEqual ( seance . etat ( ) . examen . fautives , fautives ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual ( seance . etat ( ) . fautives , fautives ) ;
assert . deepEqual ( seance . etat ( ) . examen . fautives , [ ] ) ;
await seance . fermer ( ) ;
assert . deepEqual ( seance . etat ( ) . fautives , [ ] ) ;
} ) ;
} ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
describe ( 'séance : attendre la file (§ 5.10, § 8.4)' , ( ) => {
test ( "attendre se résout quand les appels déjà en file ont fini, réussis ou refusés, sans rien écrire ni publier de plus" , async ( ) => {
const { seance , fs } = await ouvertSur ( ) ;
await seance . passerEnEcriture ( ) ;
const ecrit = seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const refuse = seance . executer ( 'ajouterParticipant' , { champs : { nom : '' } } ) . catch ( ( erreur ) => erreur ) ;
const avant = seance . charge . participants . length ;
const ecritures = fs . compteurs . ecritures ;
let publications = 0 ;
seance . subscribe ( ( ) => {
publications += 1 ;
} ) ;
publications = 0 ;
await seance . attendre ( ) ;
assert . equal ( seance . charge . participants . length , avant + 1 ) ;
assert . equal ( ( await refuse ) . code !== undefined , true ) ;
await ecrit ;
const apres = publications ;
const ecrituresApres = fs . compteurs . ecritures ;
await seance . attendre ( ) ;
assert . equal ( publications , apres , 'attendre ne publie rien' ) ;
assert . equal ( fs . compteurs . ecritures , ecrituresApres , 'attendre n’ écrit rien' ) ;
assert . ok ( ecrituresApres > ecritures ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : la dérive nommée (§ 9)' , ( ) => {
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
test ( "l'examen de l'état nomme chaque raison de dérive — la personne par son nom, la table par son numéro —, quand celui de l'Ouvert garde les identifiants" , async ( ) => {
const charge = avecRetenu ( chargeEssai ( ) ) ;
charge . participants [ 2 ] . exclu = true ;
charge . tables [ 1 ] . numero = 7 ;
charge . tables [ 1 ] . sieges = 3 ;
const { seance } = await ouvertSur ( charge ) ;
const { raisons } = seance . etat ( ) . examen . retenu ;
assert . deepEqual ( raisons , [
{ code : 'CAPACITE_CHANGEE' , table : 2 , avant : 2 , maintenant : 3 , numero : 7 } ,
{ code : 'PARTICIPANT_EXCLU' , participant : 3 , personne : 'Théo Pervenche' } ,
] ) ;
assert . deepEqual (
seance . ouvert . examen . retenu . raisons ,
raisons . map ( ( { personne , numero , ... nue } ) => nue ) ,
) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
describe ( 'séance observable : les invariants du retenu (§ 14.12, § 14.15)' , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( "un retenu qui laisse une réservation d'un tour non honorée s'ouvre avec INVARIANT_VIOLE, sans les violations que la dérive nomme, sans écriture ; de même après défaire, refaire et revenir" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
const charge = reserverGrisaille ( avecRetenu ( chargeEssai ( ) ) ) ;
charge . participants [ 3 ] . exclu = true ;
await deposerEssai ( fs , charge ) ;
const montage = monter ( { fs } ) ;
await ouvrirSansEcrire ( montage ) ;
const { seance } = montage ;
const viole = {
code : 'INVARIANT_VIOLE' ,
details : { proposition : 1 , violations : [ { code : 'RESERVATION' , participant : 2 , table : 2 , tour : 1 } ] } ,
} ;
assert . deepEqual ( seance . etat ( ) . avertissements , [ ... seance . ouvert . avertissements , viole ] ) ;
assert . deepEqual ( codesDe ( seance . ouvert . avertissements ) , [ 'DERIVE' ] ) ;
await seance . passerEnEcriture ( ) ;
const renomme = await seance . executer ( 'modifierParticipant' , { id : 3 , champs : { nom : 'Pervenche-Ombrelle' } } ) ;
assert . deepEqual ( seance . etat ( ) . avertissements , [ ... seance . ouvert . avertissements , viole , ... renomme . avertissements ] ) ;
for ( const retour of [ ( ) => seance . defaire ( ) , ( ) => seance . refaire ( ) , ( ) => seance . revenir ( 1 ) ] ) {
await retour ( ) ;
assert . ok ( seance . etat ( ) . avertissements . some ( ( a ) => a . code === 'INVARIANT_VIOLE' ) , String ( retour ) ) ;
assert . deepEqual ( seance . etat ( ) . avertissements . find ( ( a ) => a . code === 'INVARIANT_VIOLE' ) , viole ) ;
}
const exclusion = await seance . executer ( 'exclureParticipant' , { id : 1 } ) ;
assert . deepEqual ( codesDe ( exclusion . avertissements ) , [ 'RESERVATIONS_SUSPENDUES' , 'DERIVE_NOUVELLE' ] ) ;
assert . deepEqual ( seance . etat ( ) . avertissements , [ ... seance . ouvert . avertissements , viole , ... exclusion . avertissements ] ) ;
} ) ;
test ( "un retenu aux surnuméraires versés en réserve s'ouvre sans INVARIANT_VIOLE" , async ( ) => {
const charge = avecRetenu ( chargeEssai ( ) ) ;
charge . participants . push ( personne ( 5 , 'Ancolie' ) ) ;
charge . prochainsIds . participant = 6 ;
charge . retenu . participants = [ 1 , 2 , 3 , 4 , 5 ] ;
for ( const tour of charge . retenu . placement ) tour . reserve = [ 5 ] ;
const { seance } = await ouvertSur ( charge ) ;
assert . deepEqual ( codesDe ( seance . etat ( ) . avertissements ) , [ ] ) ;
} ) ;
} ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
// Les textes des codes que la séance ajoute se composent, sans trace de
// valeur absente, sous les détails d'exemple de la table que
// libelles.test.js partage pour chaque code ; les épreuves qui suivent
// lisent ce que chaque texte nomme.
describe ( "séance observable : les textes des codes qu'elle ajoute (§ 14.6)" , ( ) => {
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
test ( 'le manque dit son nombre ; la surréservation nomme la table, le tour, les réservées et la capacité' , ( ) => {
assert . match ( libelle ( 'PLACES_MANQUANTES' , { placesManquantes : 1 } ) , /\b1 place\b/ ) ;
assert . match ( libelle ( 'PLACES_MANQUANTES' , { placesManquantes : 4 } ) , /\b4 places\b/ ) ;
const texte = libelle ( 'SURRESERVATION' , { table : 3 , tour : 2 , reservees : 9 , capacite : 8 } ) ;
for ( const motif of [ /identifiant 3\b/ , /tour 2\b/ , /\b9 personnes\b/ , /\b8 places\b/ ] ) assert . match ( texte , motif ) ;
} ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
test ( "la dérive nouvelle nomme le placement et sa raison ; l'invariant nomme la réservation ; la reprise d'un verrou la dit d'une autre séance, d'un poste ou d'un autre" , ( ) => {
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
const derive = libelle ( 'DERIVE_NOUVELLE' , {
touchees : [
{ id : 2 , raisons : [ { code : 'PARTICIPANT_EXCLU' , participant : 3 } ] } ,
{ id : 'retenu' , raisons : [ { code : 'TABLE_AJOUTEE' , table : 5 } ] } ,
] ,
} ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . ok ( derive . includes ( libelle ( 'PARTICIPANT_EXCLU' , { participant : 3 } ) ) , derive ) ;
assert . match ( derive , /n° 2/ ) ;
assert . match ( derive , /retenu/ ) ;
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
const viole = libelle ( 'INVARIANT_VIOLE' , {
proposition : 3 ,
violations : [
{ code : 'RESERVATION' , participant : 2 , table : 2 , tour : 1 } ,
{ code : 'DOUBLE_PLACE' , participant : 4 , tour : 2 } ,
] ,
} ) ;
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . ok ( viole . includes ( libelle ( 'RESERVATION_NON_HONOREE' , { participant : 2 , table : 2 , tour : 1 } ) ) , viole ) ;
assert . match ( viole , /n° 3/ ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
for ( const details of [ { seance : 's' , depuis : null } , { seance : null , depuis : null } ] ) {
const repris = libelle ( 'VERROU_REPRIS' , details ) ;
assert . match ( repris , /une autre séance/ ) ;
assert . doesNotMatch ( repris , /interrompue|ce poste/ ) ;
}
[ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.
--- FR ---
[ADD] application : séance observable, façade des événements, places
Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.
Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00
assert . match ( libelle ( 'CONFIGURATION_INCOHERENTE' , { code : 'CODE_INVENTE' , details : { } } ) , /« CODE_INVENTE »/ ) ;
} ) ;
} ) ;
[ADD] outputs: dated placement, draft rule, page footer, machine paper
A badge or a printed page must say which placement it shows and whether it
is a draft. The retained plan and each proposal carry an optional
timestamp, set when their placement changes and kept when a command rebuilds
it unchanged, with or without an instant; format 1 is unchanged and the
shipped demonstrations stay byte-identical. Anything but the retained plan
under a retained or locked state prints as a draft, proposals included. The
footer names event, date, round, timestamp and visible version.
Checked: red first; 3085 node and 639 browser tests green from the index.
--- FR ---
[ADD] sorties : placement daté, règle du brouillon, pied, papier
Un badge ou une page imprimée doit dire quel placement il montre et s'il
est un brouillon. Le retenu et chaque proposition portent un horodatage
facultatif, posé quand leur placement change et gardé quand une commande le
reconstruit à l'identique, avec ou sans instant ; le format 1 ne change pas
et les démonstrations livrées restent identiques à l'octet. Tout ce qui
n'est pas le retenu sous l'état retenu ou bloqué s'imprime en brouillon,
propositions comprises. Le pied nomme événement, date, tour, horodatage et
version visible.
Vérifié : rouge d'abord ; 3085 node et 639 navigateur verts depuis l'index.
Assisted-by: Claude Opus 5.5
2026-10-09 04:50:31 -04:00
describe ( 'séance : le placement daté (§ 8.9, § 11.8, Q2)' , ( ) => {
// L'horodatage de la dernière entrée du journal sur le disque.
const derniereDate = ( fs ) => journalDe ( fs ) . entrees . at ( - 1 ) . horodatage ;
test ( "chaque geste date le placement de l'instant de son entrée, une lecture d'horloge ; corriger un nom ne date rien ; défaire restitue l'ancien" , async ( ) => {
const { fs , seance } = await ouvertSur ( ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
const generation = derniereDate ( fs ) ;
assert . deepEqual ( seance . charge . propositions . map ( ( p ) => p . horodatage ) , [ generation , generation ] ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
const premier = derniereDate ( fs ) ;
assert . notEqual ( premier , generation ) ;
assert . equal ( seance . charge . retenu . horodatage , premier ) ;
assert . equal ( etatDe ( fs ) . charge . retenu . horodatage , premier ) ;
await seance . executer ( 'modifierParticipant' , { id : 3 , champs : { nom : 'Pervenche-Lacasse' } } ) ;
assert . equal ( seance . charge . retenu . horodatage , premier ) ;
await seance . executer ( 'retenirProposition' , { id : 2 } ) ;
const second = derniereDate ( fs ) ;
assert . notEqual ( second , premier ) ;
assert . deepEqual ( [ seance . charge . retenu . proposition , seance . charge . retenu . horodatage ] , [ 2 , second ] ) ;
assert . deepEqual ( seance . charge . propositions . map ( ( p ) => p . horodatage ) , [ generation , generation ] ) ;
await seance . defaire ( ) ;
assert . deepEqual ( [ seance . charge . retenu . proposition , seance . charge . retenu . horodatage ] , [ 1 , premier ] ) ;
assert . equal ( etatDe ( fs ) . charge . retenu . horodatage , premier ) ;
} ) ;
test ( 'débloquer, depuis la lecture, garde le placement daté de son instant' , async ( ) => {
const charge = avecRetenu ( chargeEssai ( ) ) ;
charge . retenu . horodatage = '2031-05-17T07:30:00-04:00' ;
charge . evenement . etat = 'bloque' ;
const { seance } = await ouvertSur ( charge ) ;
await seance . debloquer ( ) ;
assert . deepEqual ( [ seance . charge . evenement . etat , seance . charge . retenu . horodatage ] , [ 'retenu' , '2031-05-17T07:30:00-04:00' ] ) ;
} ) ;
} ) ;
describe ( 'séance : le papier de la machine (§ 8.5, § 11.2, Q1)' , ( ) => {
const papierLu = ( fs ) => JSON . parse ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) ) . papier ;
// Le texte de l'état et du journal sur le disque, null pour un absent.
const evenementSurDisque = ( fs ) => [ fs . contenu ( 'documents' , ETAT ) , fs . contenu ( 'documents' , JOURNAL ) ] ;
test ( "null au départ ; choisirPapier écrit les seuls réglages, et une séance neuve sur la même racine le lit, jamais redemandé" , async ( ) => {
const { fs , seance } = await ouvertSur ( ) ;
assert . equal ( seance . etat ( ) . papier , null ) ;
const avant = fs . compteurs . ecritures ;
const textes = evenementSurDisque ( fs ) ;
await seance . choisirPapier ( 'Lettre' ) ;
assert . equal ( fs . compteurs . ecritures , avant + 1 ) ;
assert . deepEqual ( evenementSurDisque ( fs ) , textes ) ;
assert . equal ( papierLu ( fs ) , 'Lettre' ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' ) ;
const neuve = monter ( { fs , racine : null , prefixe : 'neuve' } ) ;
assert . equal ( neuve . seance . etat ( ) . papier , null , 'sans racine' ) ;
await neuve . seance . changerRacine ( DOC ) ;
assert . equal ( neuve . seance . etat ( ) . papier , 'Lettre' ) ;
const surRacine = monter ( { fs , prefixe : 'sur-racine' } ) ;
await surRacine . seance . attendre ( ) ;
assert . equal ( surRacine . seance . etat ( ) . papier , 'Lettre' ) ;
} ) ;
test ( "vivant en lecture, en écriture, sur un plan bloqué et sans événement ; jamais l'état ni le journal" , async ( ) => {
const bloque = chargeEssai ( ) ;
bloque . evenement . etat = 'bloque' ;
const cas = [
[ 'lecture' , async ( ) => ouvertSur ( ) ] ,
[ 'écriture' , async ( ) => {
const montage = await ouvertSur ( ) ;
await montage . seance . passerEnEcriture ( ) ;
return montage ;
} ] ,
[ 'bloqué' , async ( ) => ouvertSur ( bloque ) ] ,
[ 'sans événement' , async ( ) => monter ( ) ] ,
] ;
assert . equal ( cas . length , 4 ) ;
for ( const [ libelleCas , monterCas ] of cas ) {
const { fs , seance } = await monterCas ( ) ;
const textes = evenementSurDisque ( fs ) ;
const avant = fs . compteurs . ecritures ;
for ( const [ rang , papier ] of [ 'A4' , 'Lettre' ] . entries ( ) ) {
await seance . choisirPapier ( papier ) ;
assert . equal ( fs . compteurs . ecritures , avant + rang + 1 , libelleCas ) ;
assert . deepEqual ( [ papierLu ( fs ) , seance . etat ( ) . papier ] , [ papier , papier ] , libelleCas ) ;
}
assert . deepEqual ( evenementSurDisque ( fs ) , textes , libelleCas ) ;
}
} ) ;
test ( 'un papier hors du domaine est une faute du code, et rien ne s’ écrit' , async ( ) => {
const { fs , seance } = await ouvertSur ( ) ;
const avant = fs . compteurs . ecritures ;
for ( const faux of [ 'A3' , 'a4' , 'lettre' , null , undefined , 4 ] ) {
await assert . rejects ( seance . choisirPapier ( faux ) , TypeError , String ( faux ) ) ;
}
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( seance . etat ( ) . papier , null ) ;
} ) ;
[FIX] local settings: an unreadable or unwritable file no longer blocks
An unreadable reglages_locaux.json, or a folder that refused to write it,
refused the paper choice, so no badge sheet or preview could be produced
on that machine. The chosen paper now holds for the session on that
working folder, in memory, and the file is never overwritten. On that
folder it wins over a stale paper still in the file; the first settings
write that succeeds there retains it and withdraws the warning. The
warning names the file and no longer promises defaults never written.
Checked: node 3918, browser 773; each fix red under the previous code.
--- FR ---
[FIX] réglages locaux : un fichier illisible ou non écrit ne bloque plus
Un reglages_locaux.json illisible, ou un dossier qui refusait de l'écrire,
refusait le choix du papier : ni planche ni aperçu sur cette machine. Le
papier choisi vaut désormais pour la séance sur ce dossier de travail, en
mémoire, et le fichier n'est jamais écrasé. Sur ce dossier, il l'emporte
sur un papier périmé du fichier ; la première écriture des réglages qui y
réussit le retient et retire l'avertissement. L'avertissement nomme le
fichier et ne promet plus de valeurs par défaut jamais écrites.
Vérifié : node 3918, navigateur 773 ; chaque correction rouge avant elle.
Assisted-by: Claude Opus 5.5
2026-10-10 20:20:04 -04:00
// Des réglages que la lecture ne prend pas : un format plus récent qui
// porte un papier inconnu de cette construction, et un texte tronqué.
const ILLISIBLES = [
[ 'FORME' , '{"format":2,"papier":"Legal","cadrages":[]}' ] ,
[ 'JSON' , '{"format":1,"papier":"A4","cadr' ] ,
] ;
// Une soirée retenue de deux personnes à une table, un tour : une planche
// se prépare dès que le papier est connu.
const PLAN _RETENU = [ { sieges : [ [ 1 , 2 ] ] } ] ;
const chargeRetenue = ( ) =>
soiree ( {
personnes : [ personneBadge ( 1 , 'Iris' , 'Ombrelle' ) , personneBadge ( 2 , 'Théo' , 'Pervenche' ) ] ,
tables : [ tableBadge ( 1 , 1 , 2 ) ] ,
plan : PLAN _RETENU ,
propositions : [ [ 1 , PLAN _RETENU ] ] ,
} ) ;
const avecPlanche = { polices : async ( ) => lirePolices ( ) , provenance : 'production' } ;
test ( 'des réglages illisibles : le papier choisi vaut pour la séance, en mémoire, le fichier intact octet pour octet, et la planche se prépare sans PAPIER_REQUIS' , async ( ) => {
for ( const [ raison , texte ] of ILLISIBLES ) {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , texte ) ;
const { seance } = await ouvertSur ( chargeRetenue ( ) , { fs , ... avecPlanche } ) ;
const avant = fs . compteurs . ecritures ;
const textes = evenementSurDisque ( fs ) ;
const avis = { code : 'REGLAGES_ILLISIBLES' , details : { chemin : FICHIER _REGLAGES , raison } } ;
assert . deepEqual ( await seance . choisirPapier ( 'A4' ) , { avertissements : [ avis ] } , raison ) ;
assert . equal ( seance . etat ( ) . papier , 'A4' , raison ) ;
assert . deepEqual ( seance . etat ( ) . avertissements . filter ( ( { code } ) => code === avis . code ) , [ avis ] , raison ) ;
const annonce = await seance . preparerPlanche ( ) ;
assert . deepEqual ( [ annonce . refus , annonce . papier ] , [ null , 'A4' ] , raison ) ;
await seance . choisirPapier ( 'Lettre' ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' , raison ) ;
assert . equal ( ( await seance . preparerPlanche ( ) ) . papier , 'Lettre' , raison ) ;
assert . deepEqual ( seance . etat ( ) . avertissements . filter ( ( { code } ) => code === avis . code ) , [ avis ] , ` ${ raison } : un avis, pas deux ` ) ;
assert . deepEqual ( fs . octets ( 'documents' , FICHIER _REGLAGES ) , new TextEncoder ( ) . encode ( texte ) , raison ) ;
assert . equal ( fs . compteurs . ecritures , avant , raison ) ;
assert . deepEqual ( evenementSurDisque ( fs ) , textes , raison ) ;
[ADD] outputs: dated placement, draft rule, page footer, machine paper
A badge or a printed page must say which placement it shows and whether it
is a draft. The retained plan and each proposal carry an optional
timestamp, set when their placement changes and kept when a command rebuilds
it unchanged, with or without an instant; format 1 is unchanged and the
shipped demonstrations stay byte-identical. Anything but the retained plan
under a retained or locked state prints as a draft, proposals included. The
footer names event, date, round, timestamp and visible version.
Checked: red first; 3085 node and 639 browser tests green from the index.
--- FR ---
[ADD] sorties : placement daté, règle du brouillon, pied, papier
Un badge ou une page imprimée doit dire quel placement il montre et s'il
est un brouillon. Le retenu et chaque proposition portent un horodatage
facultatif, posé quand leur placement change et gardé quand une commande le
reconstruit à l'identique, avec ou sans instant ; le format 1 ne change pas
et les démonstrations livrées restent identiques à l'octet. Tout ce qui
n'est pas le retenu sous l'état retenu ou bloqué s'imprime en brouillon,
propositions comprises. Le pied nomme événement, date, tour, horodatage et
version visible.
Vérifié : rouge d'abord ; 3085 node et 639 navigateur verts depuis l'index.
Assisted-by: Claude Opus 5.5
2026-10-09 04:50:31 -04:00
}
} ) ;
[FIX] local settings: an unreadable or unwritable file no longer blocks
An unreadable reglages_locaux.json, or a folder that refused to write it,
refused the paper choice, so no badge sheet or preview could be produced
on that machine. The chosen paper now holds for the session on that
working folder, in memory, and the file is never overwritten. On that
folder it wins over a stale paper still in the file; the first settings
write that succeeds there retains it and withdraws the warning. The
warning names the file and no longer promises defaults never written.
Checked: node 3918, browser 773; each fix red under the previous code.
--- FR ---
[FIX] réglages locaux : un fichier illisible ou non écrit ne bloque plus
Un reglages_locaux.json illisible, ou un dossier qui refusait de l'écrire,
refusait le choix du papier : ni planche ni aperçu sur cette machine. Le
papier choisi vaut désormais pour la séance sur ce dossier de travail, en
mémoire, et le fichier n'est jamais écrasé. Sur ce dossier, il l'emporte
sur un papier périmé du fichier ; la première écriture des réglages qui y
réussit le retient et retire l'avertissement. L'avertissement nomme le
fichier et ne promet plus de valeurs par défaut jamais écrites.
Vérifié : node 3918, navigateur 773 ; chaque correction rouge avant elle.
Assisted-by: Claude Opus 5.5
2026-10-10 20:20:04 -04:00
test ( 'des réglages illisibles : la planche se produit sur le papier de la séance, et seul le PDF s’ écrit' , async ( ) => {
const [ , texte ] = ILLISIBLES [ 1 ] ;
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , texte ) ;
const { seance } = await ouvertSur ( chargeRetenue ( ) , { fs , ... avecPlanche } ) ;
await refus ( seance . produirePlanche ( ) , 'PAPIER_REQUIS' ) ;
await seance . choisirPapier ( 'A4' ) ;
const textes = evenementSurDisque ( fs ) ;
const { nom } = await seance . produirePlanche ( ) ;
assert . match ( nom , /_A4_/ ) ;
assert . ok ( fs . octets ( 'documents' , nom ) . length > 0 ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , texte ) ;
assert . deepEqual ( evenementSurDisque ( fs ) , textes ) ;
} ) ;
test ( "le papier de la séance n'est pas retenu sur la machine : une séance neuve sur la même racine le demande, et un changement de racine le garde" , async ( ) => {
const [ , texte ] = ILLISIBLES [ 0 ] ;
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , texte ) ;
const { seance } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
await seance . choisirPapier ( 'A4' ) ;
const neuve = monter ( { fs , prefixe : 'neuve' } ) ;
await neuve . seance . attendre ( ) ;
assert . equal ( neuve . seance . etat ( ) . papier , null ) ;
await seance . changerRacine ( DOC ) ;
assert . equal ( seance . etat ( ) . papier , 'A4' , 'relu sur la même racine illisible' ) ;
fs . deposerDossier ( PORTABLE . id ) ;
await seance . changerRacine ( PORTABLE ) ;
assert . equal ( seance . etat ( ) . papier , 'A4' , 'une racine sans réglages : le choix de la séance' ) ;
fs . deposer ( PORTABLE . id , FICHIER _REGLAGES , '{"cadrages":[],"format":1,"papier":"Lettre"}\n' ) ;
await seance . changerRacine ( DOC ) ;
await seance . changerRacine ( PORTABLE ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' , 'un papier écrit sur la racine l’ emporte' ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , texte ) ;
} ) ;
test ( 'des réglages illisibles : le cadrage différé attend, rien ne s’ écrit, et le choix du papier ne le change pas' , async ( ) => {
const [ , texte ] = ILLISIBLES [ 1 ] ;
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , texte ) ;
const { seance , planificateur } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
seance . demanderCadrage ( { k : 1.5 , tx : 3 , ty : 4 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await seance . choisirPapier ( 'Lettre' ) ;
await laisser ( ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , texte ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' ) ;
} ) ;
test ( 'un cadrage écrit après des réglages illisibles réparés écrit avec lui le papier de la séance, qui devient retenu, et l’ avis se retire' , async ( ) => {
const [ , texte ] = ILLISIBLES [ 1 ] ;
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , texte ) ;
const { seance , planificateur } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
await seance . choisirPapier ( 'A4' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , '{"cadrages":[],"format":1,"papier":"Lettre"}\n' ) ;
seance . demanderCadrage ( { k : 2 , tx : 0 , ty : 0 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
const relus = JSON . parse ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) ) ;
assert . deepEqual ( [ relus . papier , relus . cadrages . length ] , [ 'A4' , 1 ] ) ;
assert . equal ( seance . etat ( ) . papier , 'A4' ) ;
assert . equal ( seance . etat ( ) . avertissements . some ( ( { code } ) => code === 'REGLAGES_ILLISIBLES' ) , false ) ;
const neuve = monter ( { fs , prefixe : 'neuve' } ) ;
await neuve . seance . attendre ( ) ;
assert . equal ( neuve . seance . etat ( ) . papier , 'A4' , 'retenu sur la machine' ) ;
} ) ;
test ( 'un papier écrit après des réglages illisibles réparés : le fichier le porte, la séance suit le fichier, et le papier de la séance s’ efface' , async ( ) => {
const [ , texte ] = ILLISIBLES [ 1 ] ;
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , texte ) ;
const { seance } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
await seance . choisirPapier ( 'A4' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , '{"cadrages":[],"format":1,"papier":null}\n' ) ;
assert . deepEqual ( await seance . choisirPapier ( 'Lettre' ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ papierLu ( fs ) , seance . etat ( ) . papier ] , [ 'Lettre' , 'Lettre' ] ) ;
assert . equal ( seance . etat ( ) . avertissements . some ( ( { code } ) => code === 'REGLAGES_ILLISIBLES' ) , false , 'l’ avis se retire' ) ;
fs . deposerDossier ( PORTABLE . id ) ;
await seance . changerRacine ( PORTABLE ) ;
assert . equal ( seance . etat ( ) . papier , null , 'le choix retenu reste à sa racine ; une racine sans réglages le demande' ) ;
} ) ;
// Un dossier de travail qui refuse toute écriture (EROFS) après
// l'ouverture : les réglages s'y lisent, ils ne s'y écrivent pas.
const avisNonEcrits = { code : 'REGLAGES_NON_ECRITS' , details : { chemin : FICHIER _REGLAGES , dossier : DOCUMENTS , cause : 'EROFS' } } ;
test ( "un dossier qui refuse l'écriture des réglages : le papier choisi vaut pour la séance, un avis le dit, et la planche se prépare sans PAPIER_REQUIS" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , '{"cadrages":[],"format":1,"papier":null}\n' ) ;
const { seance } = await ouvertSur ( chargeRetenue ( ) , { fs , ... avecPlanche } ) ;
fs . pannes . ecriture ( 'documents' ) ;
const textes = evenementSurDisque ( fs ) ;
const reglages = fs . contenu ( 'documents' , FICHIER _REGLAGES ) ;
assert . deepEqual ( await seance . choisirPapier ( 'A4' ) , { avertissements : [ avisNonEcrits ] } ) ;
assert . equal ( seance . etat ( ) . papier , 'A4' ) ;
const annonce = await seance . preparerPlanche ( ) ;
assert . deepEqual ( [ annonce . refus , annonce . papier ] , [ null , 'A4' ] ) ;
assert . deepEqual ( await seance . choisirPapier ( 'Lettre' ) , { avertissements : [ avisNonEcrits ] } ) ;
assert . equal ( ( await seance . preparerPlanche ( ) ) . papier , 'Lettre' ) ;
assert . deepEqual (
seance . etat ( ) . avertissements . filter ( ( { code } ) => code === avisNonEcrits . code ) ,
[ avisNonEcrits ] ,
'un avis, pas deux' ,
) ;
assert . equal ( seance . etat ( ) . avertissements . some ( ( { code } ) => code === 'REGLAGES_ILLISIBLES' ) , false ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , reglages ) ;
assert . deepEqual ( evenementSurDisque ( fs ) , textes ) ;
} ) ;
test ( "un dossier qui refuse l'écriture des réglages : la production ne demande plus le papier, seule l'écriture du PDF est refusée" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
const { seance } = await ouvertSur ( chargeRetenue ( ) , { fs , ... avecPlanche } ) ;
fs . pannes . ecriture ( 'documents' ) ;
await refus ( seance . produirePlanche ( ) , 'PAPIER_REQUIS' ) ;
const textes = evenementSurDisque ( fs ) ;
await seance . choisirPapier ( 'A4' ) ;
const erreur = await refus ( seance . produirePlanche ( ) , 'PLANCHE_NON_ECRITE' ) ;
assert . match ( erreur . details . chemin , /_A4_/ ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , null ) ;
assert . deepEqual ( evenementSurDisque ( fs ) , textes ) ;
} ) ;
test ( "le papier d'une écriture refusée vaut au changement de racine, là où les réglages n'en portent pas" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
const { seance } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
fs . pannes . ecriture ( 'documents' ) ;
await seance . choisirPapier ( 'Lettre' ) ;
fs . deposerDossier ( PORTABLE . id ) ;
await seance . changerRacine ( PORTABLE ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' ) ;
} ) ;
// Des réglages lisibles qui portent déjà un papier, sur la racine des
// documents.
const REGLAGES _A4 = '{"cadrages":[],"format":1,"papier":"A4"}\n' ;
const sansReglages = ( seance ) =>
seance . etat ( ) . avertissements . filter ( ( { code } ) => code === 'REGLAGES_NON_ECRITS' || code === 'REGLAGES_ILLISIBLES' ) ;
test ( "une écriture refusée une fois : le papier de la séance l'emporte sur celui du fichier, le cadrage écrit ensuite l'écrit avec lui, et l'avis se retire" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , REGLAGES _A4 ) ;
const { seance , planificateur } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
fs . pannes . renommage ( 'documents' , 11 ) ;
const avis = { code : 'REGLAGES_NON_ECRITS' , details : { chemin : FICHIER _REGLAGES , dossier : DOCUMENTS , cause : 'EBUSY' } } ;
assert . deepEqual ( await seance . choisirPapier ( 'Lettre' ) , { avertissements : [ avis ] } ) ;
assert . deepEqual ( [ papierLu ( fs ) , seance . etat ( ) . papier ] , [ 'A4' , 'Lettre' ] ) ;
seance . demanderCadrage ( { k : 1.5 , tx : 3 , ty : 4 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
const relus = JSON . parse ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) ) ;
assert . deepEqual ( [ relus . papier , relus . cadrages . length ] , [ 'Lettre' , 1 ] ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' ) ;
assert . deepEqual ( sansReglages ( seance ) , [ ] , 'le choix est retenu : l’ avis se retire' ) ;
await seance . changerRacine ( DOC ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' ) ;
} ) ;
test ( "une écriture refusée une fois, puis un papier qui s'écrit : l'avis se retire de l'état" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , REGLAGES _A4 ) ;
const { seance } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
fs . pannes . renommage ( 'documents' , 11 ) ;
await seance . choisirPapier ( 'Lettre' ) ;
assert . deepEqual ( sansReglages ( seance ) . map ( ( { code } ) => code ) , [ 'REGLAGES_NON_ECRITS' ] ) ;
assert . deepEqual ( await seance . choisirPapier ( 'Lettre' ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ papierLu ( fs ) , seance . etat ( ) . papier ] , [ 'Lettre' , 'Lettre' ] ) ;
assert . deepEqual ( sansReglages ( seance ) , [ ] ) ;
} ) ;
test ( "un dossier qui refuse l'écriture : revenir sur la même racine garde le papier de la séance, que le fichier en porte un autre ou non ; une autre racine lit le sien" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , REGLAGES _A4 ) ;
const { seance , planificateur } = await ouvertSur ( chargeEssai ( ) , { fs } ) ;
fs . pannes . ecriture ( 'documents' ) ;
await seance . choisirPapier ( 'Lettre' ) ;
await seance . changerRacine ( DOC ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' , 'la même racine' ) ;
fs . deposerDossier ( PORTABLE . id ) ;
fs . deposer ( PORTABLE . id , FICHIER _REGLAGES , REGLAGES _A4 ) ;
await seance . changerRacine ( PORTABLE ) ;
assert . equal ( seance . etat ( ) . papier , 'A4' , 'une autre racine : son papier l’ emporte' ) ;
await seance . changerRacine ( DOC ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' , 'de retour sur la racine du choix' ) ;
await seance . ouvrir ( BASE ) ;
seance . demanderCadrage ( { k : 2 , tx : 0 , ty : 0 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' , 'un cadrage refusé ne le change pas' ) ;
assert . equal ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) , REGLAGES _A4 ) ;
} ) ;
test ( 'une écriture des réglages qui lève autre chose qu’ un refus du stockage remonte de choisirPapier, sans poser de papier ni d’ avis' , async ( ) => {
const { fs , seance } = await ouvertSur ( ) ;
// Les réglages se lisent ; leur écriture lève une panne du processus.
const ecrireAtomique = fs . ecrireAtomique ;
fs . ecrireAtomique = async ( racine , chemin , texte ) => {
if ( chemin === FICHIER _REGLAGES ) throw new PanneSimulee ( ) ;
return ecrireAtomique ( racine , chemin , texte ) ;
} ;
await assert . rejects ( seance . choisirPapier ( 'A4' ) , PanneSimulee ) ;
assert . equal ( seance . etat ( ) . papier , null ) ;
assert . deepEqual ( sansReglages ( seance ) , [ ] ) ;
} ) ;
test ( "un papier écrit pour l'ancienne racine, dans la file pendant un changement de racine, ne se publie jamais sur la nouvelle" , async ( ) => {
const { fs , seance } = monter ( { fs : creerFichiersSimules ( { executable : 'E:\\soirees' } ) } ) ;
await seance . attendre ( ) ;
// Un fichier présent : sa lecture se retient, celle d'un absent non.
fs . deposer ( 'documents' , FICHIER _REGLAGES , '{"cadrages":[],"format":1,"papier":null}\n' ) ;
fs . deposerDossier ( PORTABLE . id ) ;
const publies = [ ] ;
seance . subscribe ( ( { racine , papier } ) => publies . push ( [ racine ? . id ? ? null , papier ] ) ) ;
const lecture = retenirLectureReglages ( fs ) ;
const choix = seance . choisirPapier ( 'Lettre' ) ;
await laisser ( ) ;
assert . equal ( lecture . prise ( ) , true , 'la lecture du choix est partie' ) ;
const changement = seance . changerRacine ( PORTABLE ) ;
await laisser ( ) ;
assert . equal ( seance . racine . id , PORTABLE . id , 'la racine a changé, le choix attend' ) ;
lecture . liberer ( ) ;
await choix ;
await changement ;
await seance . attendre ( ) ;
assert . equal ( papierLu ( fs ) , 'Lettre' , 'écrit à sa racine' ) ;
assert . deepEqual ( publies . filter ( ( [ racine , papier ] ) => racine === PORTABLE . id && papier !== null ) , [ ] ) ;
assert . equal ( seance . etat ( ) . papier , null ) ;
} ) ;
[ADD] outputs: dated placement, draft rule, page footer, machine paper
A badge or a printed page must say which placement it shows and whether it
is a draft. The retained plan and each proposal carry an optional
timestamp, set when their placement changes and kept when a command rebuilds
it unchanged, with or without an instant; format 1 is unchanged and the
shipped demonstrations stay byte-identical. Anything but the retained plan
under a retained or locked state prints as a draft, proposals included. The
footer names event, date, round, timestamp and visible version.
Checked: red first; 3085 node and 639 browser tests green from the index.
--- FR ---
[ADD] sorties : placement daté, règle du brouillon, pied, papier
Un badge ou une page imprimée doit dire quel placement il montre et s'il
est un brouillon. Le retenu et chaque proposition portent un horodatage
facultatif, posé quand leur placement change et gardé quand une commande le
reconstruit à l'identique, avec ou sans instant ; le format 1 ne change pas
et les démonstrations livrées restent identiques à l'octet. Tout ce qui
n'est pas le retenu sous l'état retenu ou bloqué s'imprime en brouillon,
propositions comprises. Le pied nomme événement, date, tour, horodatage et
version visible.
Vérifié : rouge d'abord ; 3085 node et 639 navigateur verts depuis l'index.
Assisted-by: Claude Opus 5.5
2026-10-09 04:50:31 -04:00
test ( "un cadrage différé et le choix du papier écrits ensemble : les deux restent, l'un après l'autre" , async ( ) => {
const { fs , seance , planificateur } = await ouvertSur ( ) ;
seance . demanderCadrage ( { k : 1.5 , tx : 3 , ty : 4 } ) ;
const cadrage = planificateur . avancer ( DELAI _REGLAGES _MS ) ;
const papier = seance . choisirPapier ( 'Lettre' ) ;
await cadrage ;
await papier ;
await seance . fermer ( ) ;
await laisser ( ) ;
const relus = JSON . parse ( fs . contenu ( 'documents' , FICHIER _REGLAGES ) ) ;
assert . equal ( relus . papier , 'Lettre' ) ;
assert . deepEqual ( relus . cadrages , [ { evenement : 'evt-lucioles' , k : 1.5 , tx : 3 , ty : 4 } ] ) ;
} ) ;
test ( "le papier qu'une autre séance a choisi se lit après l'écriture suivante de cette séance, cadrage compris" , async ( ) => {
const { fs , seance , planificateur } = await ouvertSur ( ) ;
const autre = monter ( { fs , prefixe : 'autre' } ) ;
await autre . seance . choisirPapier ( 'A4' ) ;
assert . equal ( seance . etat ( ) . papier , null ) ;
seance . demanderCadrage ( { k : 2 , tx : 0 , ty : 0 } ) ;
await planificateur . avancer ( DELAI _REGLAGES _MS ) ;
await laisser ( ) ;
assert . equal ( seance . etat ( ) . papier , 'A4' ) ;
assert . equal ( papierLu ( fs ) , 'A4' ) ;
} ) ;
// Retient la réponse de la première lecture de reglages_locaux.json qui
// suit : la lecture se fait aussitôt, sa réponse attend liberer().
function retenirLectureReglages ( fs ) {
const lireTexte = fs . lireTexte ;
let liberer = null ;
const retenue = new Promise ( ( resolve ) => {
liberer = resolve ;
} ) ;
let prise = false ;
fs . lireTexte = async ( racine , chemin ) => {
if ( prise || chemin !== FICHIER _REGLAGES ) return lireTexte ( racine , chemin ) ;
prise = true ;
const reponse = await lireTexte ( racine , chemin ) ;
await retenue ;
return reponse ;
} ;
return { liberer : ( ) => liberer ( ) , prise : ( ) => prise } ;
}
test ( "une relecture du papier partie avant un choix ne l'écrase pas : à la création, l'état suit le fichier" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , FICHIER _REGLAGES , '{"cadrages":[],"format":1,"papier":"A4"}\n' ) ;
const lecture = retenirLectureReglages ( fs ) ;
const { seance } = monter ( { fs } ) ;
await laisser ( ) ;
assert . equal ( lecture . prise ( ) , true , 'la relecture de la création est partie' ) ;
const choix = seance . choisirPapier ( 'Lettre' ) ;
await laisser ( ) ;
lecture . liberer ( ) ;
await choix ;
await seance . attendre ( ) ;
assert . deepEqual ( [ papierLu ( fs ) , seance . etat ( ) . papier ] , [ 'Lettre' , 'Lettre' ] ) ;
} ) ;
test ( "une relecture du papier partie avant un choix ne l'écrase pas : au changement de racine, l'état suit le fichier" , async ( ) => {
const { fs , seance } = monter ( { fs : creerFichiersSimules ( { executable : 'E:\\soirees' } ) } ) ;
await seance . attendre ( ) ;
fs . deposerDossier ( PORTABLE . id ) ;
fs . deposer ( PORTABLE . id , FICHIER _REGLAGES , '{"cadrages":[],"format":1,"papier":"A4"}\n' ) ;
const lecture = retenirLectureReglages ( fs ) ;
const changement = seance . changerRacine ( PORTABLE ) ;
await laisser ( ) ;
assert . equal ( lecture . prise ( ) , true , 'la relecture du changement de racine est partie' ) ;
const choix = seance . choisirPapier ( 'Lettre' ) ;
await laisser ( ) ;
lecture . liberer ( ) ;
await changement ;
await choix ;
await seance . attendre ( ) ;
assert . equal ( JSON . parse ( fs . contenu ( PORTABLE . id , FICHIER _REGLAGES ) ) . papier , 'Lettre' ) ;
assert . equal ( seance . etat ( ) . papier , 'Lettre' ) ;
} ) ;
} ) ;
[ADD] badges: prepare, produce and save the sheet from the session
The operator needs the badge sheet as a file next to the event, built
from what the session shows at that moment. The session prepares an
announcement (sheets per paper, counts, namesakes, missing glyphs, draft),
then produces the PDF through a single deferred import of the PDF code and
writes it atomically; under the web build it downloads instead. A gesture
started during production waits its turn. A proposal sheet is always a
draft, and an unwritable file or a missing paper choice gives its refusal
with a remedy.
Checked: red first; 3670 node, 707 browser and 101 long tests green from
the index alone.
--- FR ---
[ADD] badges : préparer, produire et déposer la planche depuis la séance
L'opérateur a besoin de la planche en fichier, à côté de l'événement, tirée
de ce que la séance montre à cet instant. La séance prépare une annonce
(planches par papier, comptes, homonymes, glyphes absents, brouillon), puis
produit le PDF par un seul import différé du code PDF et l'écrit de façon
atomique ; sous web, elle le télécharge. Un geste lancé pendant la
production attend son tour. Une planche tirée d'une proposition est
toujours un brouillon, et un fichier non écrit ou un papier non choisi
donnent leur refus avec un remède.
Vérifié : rouge d'abord ; 3670 node, 707 navigateur et 101 longues vertes
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 17:11:48 -04:00
describe ( 'séance : la planche, câblée dans la file des appels (§ 11.6, § 13.4)' , ( ) => {
// Une soirée retenue de quatre personnes sur deux tables de deux places,
// deux tours, le retenu daté de H_RETENU.
const PLAN = [ { sieges : [ [ 1 , 2 ] , [ 3 , 4 ] ] } , { sieges : [ [ 1 , 3 ] , [ 2 , 4 ] ] } ] ;
const chargeRetenue = ( ) =>
soiree ( {
personnes : [
personneBadge ( 1 , 'Iris' , 'Ombrelle' ) ,
personneBadge ( 2 , 'Théo' , 'Pervenche' ) ,
personneBadge ( 3 , 'Ondine' , 'Lacasse' ) ,
personneBadge ( 4 , null , 'Grisaille' ) ,
] ,
tables : [ tableBadge ( 1 , 1 , 2 ) , tableBadge ( 2 , 2 , 2 ) ] ,
plan : PLAN ,
propositions : [ [ 1 , PLAN ] ] ,
} ) ;
test ( 'preparerPlanche lancé derrière un geste lit la charge que le geste laisse ; la police se charge une fois par séance' , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs , chargeRetenue ( ) ) ;
let chargements = 0 ;
const polices = async ( ) => {
chargements += 1 ;
return lirePolices ( ) ;
} ;
const { seance } = monter ( { fs , polices , provenance : 'production' } ) ;
await seance . choisirPapier ( 'A4' ) ;
await seance . ouvrir ( BASE ) ;
await seance . passerEnEcriture ( ) ;
const geste = seance . executer ( 'echangerDansRetenu' , { tour : 1 , a : 1 , b : 3 } ) ;
const annonce = seance . preparerPlanche ( ) ;
await geste ;
const horodatage = seance . charge . retenu . horodatage ;
assert . notEqual ( horodatage , H _RETENU ) ;
const attendu = nomPlanche ( BASE , { papier : 'A4' , imposition : 'feuille' , horodatage , reedition : null } ) ;
assert . equal ( ( await annonce ) . nom , attendu ) ;
assert . equal ( ( await seance . preparerPlanche ( ) ) . nom , attendu ) ;
assert . equal ( chargements , 1 ) ;
} ) ;
test ( 'un papier choisi sans être attendu compte pour la planche demandée juste après : la planche attend les écritures de réglages déjà demandées' , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs , chargeRetenue ( ) ) ;
const { seance } = monter ( { fs , polices : async ( ) => lirePolices ( ) , provenance : 'production' } ) ;
await seance . ouvrir ( BASE ) ;
const choix = seance . choisirPapier ( 'Lettre' ) ;
const annonce = await seance . preparerPlanche ( ) ;
await choix ;
assert . deepEqual ( [ annonce . refus , annonce . papier ] , [ null , 'Lettre' ] ) ;
} ) ;
test ( 'sans police ni provenance, préparer lève TypeError ; un chargement refusé remonte, et le suivant réessaie' , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs , chargeRetenue ( ) ) ;
const sansPolices = monter ( { fs , provenance : 'production' } ) . seance ;
await sansPolices . ouvrir ( BASE ) ;
await assert . rejects ( sansPolices . preparerPlanche ( ) , ( erreur ) => erreur instanceof TypeError && /polices/ . test ( erreur . message ) ) ;
const sansProvenance = monter ( { fs , prefixe : 'sans-provenance' , polices : async ( ) => lirePolices ( ) } ) . seance ;
await sansProvenance . ouvrir ( BASE ) ;
await assert . rejects ( sansProvenance . preparerPlanche ( ) , TypeError ) ;
let essais = 0 ;
const polices = async ( ) => {
essais += 1 ;
if ( essais === 1 ) throw new Error ( 'police illisible' ) ;
return lirePolices ( ) ;
} ;
const { seance } = monter ( { fs , prefixe : 'reessai' , polices , provenance : 'production' } ) ;
await seance . ouvrir ( BASE ) ;
await assert . rejects ( seance . preparerPlanche ( ) , /police illisible/ ) ;
const annonce = await seance . preparerPlanche ( ) ;
assert . equal ( annonce . refus . code , 'PAPIER_REQUIS' ) ;
assert . equal ( essais , 2 ) ;
} ) ;
} ) ;