[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
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// La planche dans la séance (§ 11.2, § 11.6, § 11.8, § 8.4, § 8.6, § 9) :
// préparer annonce, produire dépose le PDF à côté des fichiers d'événement
// — ou le télécharge sous web —, enregistrer sous le confie au dialogue.
// Sortir n'est pas modifier (contrainte 24) : en lecture, sur un plan
// bloqué, sous un format plus récent, la trace des écritures du système
// d'épreuve ne porte que le .pdf ; un refus n'écrit rien ; l'écriture du PDF
// refusée se dit PLANCHE_NON_ECRITE, sans rompre le support de l'événement.
//
// La série surveillée borne le coût (§ 14.4, contrainte 31) : douze
// productions de PDF au plus dans ce fichier, sur une soirée de quatre
// personnes — trois pour les trois modes, trois pour la stabilité des
// octets, une réédition, deux refus d'écriture et l'enregistrement sous,
2026-10-10 17:22:35 -04:00
// une sous web, une proposition. L'imposition et l'ordre se lisent dans la
// place de chaque nom, et le papier, l'imposition et le nombre de planches
// dans la feuille de contrôle : la première production, sous la demande
// par défaut, en A4 — l'ordre alphabétique ; la proposition, sous la
// demande par défaut, en Lettre — feuille par feuille sur deux planches ;
// celle sous web en Lettre, en pile, par table. La réédition porte un
// caractère sans glyphe, que sa feuille de contrôle nomme. Le témoin de 257
// ne s'analyse que pour son annonce. Les noms sont inventés ; la version
// vient du module engendré.
[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 assert from 'node:assert/strict' ;
import { creerFichiersSimules } from '../../test/fichiers_simules.js' ;
import { H _PROPOSITION , H _RETENU , personne , soiree , table } from '../../test/fixtures/badges/cas.js' ;
import { FORME _TEMOIN , chargeTemoin } from '../../test/fixtures/badges/temoin.js' ;
import { describe , test } from '../../test/lanceur.js' ;
import { lirePdf } from '../../test/pdf.js' ;
import { lirePolices } from '../../test/polices.js' ;
2026-10-10 17:22:35 -04:00
import { PAPIERS } from '../geometrie/papiers.js' ;
import { cellules } from '../pdf/imposition.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 { serialiser } from '../stockage/canonique.js' ;
import { creer as creerDansLeDepot } from '../stockage/depot.js' ;
import { FORMAT } from '../stockage/document.js' ;
import { SUFFIXES , nomPlanche } from '../stockage/noms.js' ;
import { VERSION } from '../version.genere.js' ;
import { libelle } from './libelles.js' ;
import { creerSeance } from './seance.js' ;
const V1 = VERSION . affichee ;
const DOCUMENTS = 'D:\\Soirees\\Gestion table tournante Libre' ;
const DOC = { id : 'documents' , chemin : DOCUMENTS } ;
const BASE = 'Soirée des Lucioles' ;
const POLICES = lirePolices ( ) ;
const FILIGRANE = libelle ( 'planche.brouillon' ) ;
// Les primitives qui écrivent, et enregistrerSous, que la trace consigne.
const ECRITURES = [
'sonder' ,
'ecrireAtomique' ,
'ecrireOctetsAtomique' ,
'ajouterLigne' ,
'creerDossier' ,
'deplacer' ,
'supprimer' ,
'verrouiller' ,
'deverrouiller' ,
'enregistrerSous' ,
] ;
// Enveloppe chaque primitive d'écriture du système : la trace reçoit
// [primitive, chemin], le nom proposé pour enregistrerSous.
function tracer ( fs ) {
const trace = [ ] ;
for ( const nom of ECRITURES ) {
const primitive = fs [ nom ] ;
fs [ nom ] = ( ... args ) => {
trace . push ( [ nom , nom === 'enregistrerSous' ? args [ 0 ] : args [ 1 ] ] ) ;
return primitive ( ... args ) ;
} ;
}
return trace ;
}
// Horloge d'épreuve : une minute de plus à chaque lecture, le matin de la
// soirée — jamais l'instant d'un placement construit.
function creerHorloge ( ) {
let n = 0 ;
return {
horodatage ( ) {
n += 1 ;
return ` 2031-05-17T08: ${ String ( n ) . padStart ( 2 , '0' ) } :00-04:00 ` ;
} ,
} ;
}
// Quatre personnes, deux tables de deux places, deux tours ; le retenu vient
// de la proposition 1, la proposition 2 en est une autre.
const PLAN = [ { sieges : [ [ 1 , 2 ] , [ 3 , 4 ] ] } , { sieges : [ [ 1 , 3 ] , [ 2 , 4 ] ] } ] ;
const AUTRE _PLAN = [ { sieges : [ [ 1 , 4 ] , [ 2 , 3 ] ] } , { sieges : [ [ 1 , 2 ] , [ 3 , 4 ] ] } ] ;
function chargeLucioles ( { personnes = null , ... champs } = { } ) {
return soiree ( {
personnes : personnes ? ? [
personne ( 1 , 'Iris' , 'Ombrelle' , { appartenance : 'Club des Merles' } ) ,
personne ( 2 , 'Théo' , 'Pervenche' ) ,
personne ( 3 , 'Ondine' , 'Lacasse' ) ,
personne ( 4 , null , 'Grisaille' ) ,
] ,
tables : [ table ( 1 , 1 , 2 ) , table ( 2 , 2 , 2 ) ] ,
plan : PLAN ,
propositions : [
[ 1 , PLAN ] ,
[ 2 , AUTRE _PLAN ] ,
] ,
... champs ,
} ) ;
}
// Le nom de la planche de la soirée d'essai.
const nomDe = ( { papier = 'A4' , imposition = 'feuille' , horodatage = H _RETENU , reedition = null } = { } ) =>
nomPlanche ( BASE , { papier , imposition , horodatage , reedition } ) ;
// Pose l'événement de la charge sur le disque, par le dépôt ; format :
// celui qu'annonce l'en-tête de l'état, FORMAT par défaut.
async function deposer ( fs , charge , { format = FORMAT } = { } ) {
fs . deposerDossier ( 'documents' ) ;
if ( format === FORMAT ) {
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 ,
} ) ;
return ;
}
const texte = serialiser ( charge , { revision : 1 , produitVersion : V1 } ) ;
fs . deposer ( 'documents' , ` ${ BASE } ${ SUFFIXES . etat } ` , texte . replace ( ` "format": ${ FORMAT } ` , ` "format": ${ format } ` ) ) ;
}
// Une séance sur l'événement de la charge, ouvert en lecture, le papier
// choisi quand il est donné ; la trace part ensuite, vide.
async function monter ( charge = chargeLucioles ( ) , { papier = 'A4' , fs = creerFichiersSimules ( { documents : DOCUMENTS } ) , format } = { } ) {
await deposer ( fs , charge , { format } ) ;
let tirage = 0 ;
const seance = creerSeance ( {
fs ,
racine : DOC ,
horloge : creerHorloge ( ) ,
identifiant : ( ) => {
tirage += 1 ;
return ` seance- ${ tirage } ` ;
} ,
produitVersion : V1 ,
planifier : ( ) => null ,
annuler : ( ) => { } ,
polices : async ( ) => POLICES ,
provenance : 'production' ,
} ) ;
if ( papier !== null ) await seance . choisirPapier ( papier ) ;
await seance . ouvrir ( BASE ) ;
const trace = tracer ( fs ) ;
return { fs , seance , trace } ;
}
// Le refus attendu d'une promesse : son code, et ses détails quand ils sont
// donnés.
async function refusee ( promesse , code , details ) {
await assert . rejects ( promesse , ( erreur ) => {
assert . equal ( erreur . code , code , erreur . message ) ;
if ( details !== undefined ) assert . deepEqual ( erreur . details , details ) ;
return true ;
} ) ;
}
// Les textes d'une page du PDF relu, et si l'un d'eux s'écrit au trait.
const textesDe = ( page ) => page . textes . map ( ( { texte } ) => texte ) ;
const auTrait = ( pdf ) => pdf . pages . some ( ( page ) => page . modesTexte . includes ( 1 ) ) ;
2026-10-10 17:22:35 -04:00
// Les noms d'une page de badges, parmi ceux donnés, cellule par cellule
// dans l'ordre de cellules(papier) ; null pour une cellule où aucun ne se
// lit. Un nom appartient à la cellule qui contient le centre de sa boîte.
const NOMS = [ 'Iris Ombrelle' , 'Théo Pervenche' , 'Ondine Lacasse' , 'Grisaille' ] ;
function nomsParCellule ( page , papier , noms = NOMS ) {
return cellules ( papier ) . map ( ( { x , y , largeur , hauteur } ) => {
const lus = page . textes
. filter ( ( t ) => noms . includes ( t . texte ) )
. filter ( ( t ) => t . x + t . l / 2 > x && t . x + t . l / 2 < x + largeur && t . y + t . h / 2 > y && t . y + t . h / 2 < y + hauteur )
. map ( ( { texte } ) => texte ) ;
assert . ok ( lus . length <= 1 , ` plusieurs noms dans une cellule : ${ lus . join ( ', ' ) } ` ) ;
return lus [ 0 ] ? ? null ;
} ) ;
}
// Les lignes de la feuille de contrôle qui disent le papier, l'imposition et
// le nombre de planches d'une production.
const lignesDeSortie = ( { papier , imposition , planches } ) => [
libelle ( 'planche.controle.papier' , { papier , ... PAPIERS [ papier ] } ) ,
libelle ( 'planche.controle.imposition' , { imposition } ) ,
libelle ( 'planche.controle.planches' , { planches } ) ,
] ;
// Celles que porte la page de contrôle relue, parmi les trois.
const sortieLue = ( pdf , sortie ) => lignesDeSortie ( sortie ) . filter ( ( ligne ) => textesDe ( pdf . pages [ 0 ] ) . includes ( ligne ) ) ;
[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 ( 'planche dans la séance : sortir n’ est pas modifier (contrainte 24, § 8.4, § 9)' , ( ) => {
test ( 'produire en lecture, sur un plan bloqué, sous un format plus récent : la trace ne porte que le .pdf, déposé à côté de l’ événement' , async ( ) => {
const cas = [
[ 'lecture' , chargeLucioles ( ) , { } ] ,
[ 'bloqué' , chargeLucioles ( { etat : 'bloque' } ) , { } ] ,
[ 'format plus récent' , chargeLucioles ( ) , { format : FORMAT + 1 } ] ,
] ;
let retenu = null ;
for ( const [ nom , charge , options ] of cas ) {
const { fs , seance , trace } = await monter ( charge , options ) ;
const revision = seance . etat ( ) . evenement . revision ;
const attendu = nomDe ( ) ;
const annonce = await seance . preparerPlanche ( ) ;
assert . deepEqual (
annonce ,
{
refus : null ,
papier : 'A4' ,
comptes : { participants : 4 , exclus : 0 , produits : 4 } ,
planches : { A4 : 1 , Lettre : 2 } ,
homonymes : [ ] ,
absents : [ ] ,
brouillon : false ,
nom : attendu ,
} ,
nom ,
) ;
assert . deepEqual ( trace , [ ] , ` ${ nom } : préparer écrit ` ) ;
const rendu = await seance . produirePlanche ( ) ;
assert . deepEqual ( rendu , { chemin : ` ${ DOCUMENTS } \\ ${ attendu } ` , nom : attendu } , nom ) ;
assert . deepEqual ( trace , [ [ 'ecrireOctetsAtomique' , attendu ] ] , nom ) ;
assert . equal ( seance . mode , 'lecture' , nom ) ;
assert . equal ( seance . etat ( ) . evenement . revision , revision , nom ) ;
const octets = fs . octets ( 'documents' , attendu ) ;
assert . equal ( new TextDecoder ( ) . decode ( octets . subarray ( 0 , 5 ) ) , '%PDF-' , nom ) ;
retenu ? ? = octets ;
}
// Le retenu d'un plan retenu ne porte pas le filigrane.
const pdf = await lirePdf ( retenu ) ;
assert . ok ( ! pdf . pages . some ( ( page ) => textesDe ( page ) . includes ( FILIGRANE ) ) ) ;
assert . equal ( auTrait ( pdf ) , false ) ;
2026-10-10 17:22:35 -04:00
// La demande par défaut : l'ordre alphabétique, par nom puis prénom, sur
// l'unique planche A4, feuille par feuille.
const sortie = { papier : 'A4' , imposition : 'feuille' , planches : 1 } ;
assert . deepEqual ( sortieLue ( pdf , sortie ) , lignesDeSortie ( sortie ) , textesDe ( pdf . pages [ 0 ] ) . join ( ' | ' ) ) ;
assert . equal ( pdf . pages . length , 2 ) ;
assert . deepEqual ( nomsParCellule ( pdf . pages [ 1 ] , 'A4' ) , [ 'Grisaille' , 'Ondine Lacasse' , 'Iris Ombrelle' , 'Théo Pervenche' ] ) ;
[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
} ) ;
test ( 'sans papier choisi : PAPIER_REQUIS avant toute écriture ; choisi, le nom et l’ annonce le portent, et une autre séance de la racine ne le redemande pas' , async ( ) => {
const { fs , seance , trace } = await monter ( chargeLucioles ( ) , { papier : null } ) ;
const requis = { code : 'PAPIER_REQUIS' , details : { remede : { geste : 'choisirPapier' } } } ;
const annonce = await seance . preparerPlanche ( ) ;
assert . deepEqual ( [ annonce . refus , annonce . papier , annonce . nom ] , [ requis , null , null ] ) ;
assert . deepEqual ( annonce . planches , { A4 : 1 , Lettre : 2 } ) ;
await refusee ( seance . produirePlanche ( ) , 'PAPIER_REQUIS' , requis . details ) ;
assert . deepEqual ( trace , [ ] ) ;
await seance . choisirPapier ( 'Lettre' ) ;
trace . length = 0 ;
const lettre = await seance . preparerPlanche ( { imposition : 'pile' } ) ;
assert . deepEqual ( [ lettre . refus , lettre . papier , lettre . nom ] , [ null , 'Lettre' , nomDe ( { papier : 'Lettre' , imposition : 'pile' } ) ] ) ;
const autre = creerSeance ( {
fs ,
racine : DOC ,
horloge : creerHorloge ( ) ,
identifiant : ( ) => 'seance-autre' ,
produitVersion : V1 ,
planifier : ( ) => null ,
annuler : ( ) => { } ,
polices : async ( ) => POLICES ,
provenance : 'production' ,
} ) ;
await autre . ouvrir ( BASE ) ;
assert . equal ( ( await autre . preparerPlanche ( ) ) . papier , 'Lettre' ) ;
assert . deepEqual ( trace , [ ] ) ;
} ) ;
test ( 'une demande hors de son domaine lève TypeError ; une sélection inconnue, PARTICIPANT_INCONNU ; sans événement, AUCUN_EVENEMENT — rien ne s’ écrit' , async ( ) => {
const { seance , trace } = await monter ( ) ;
await assert . rejects ( seance . preparerPlanche ( { imposition : 'rouleau' } ) , TypeError ) ;
await assert . rejects ( seance . produirePlanche ( { ordre : 'hasard' } ) , TypeError ) ;
await assert . rejects ( seance . preparerPlanche ( { source : 'reservations' } ) , TypeError ) ;
await refusee ( seance . produirePlanche ( { selection : [ 3 , 99 ] } ) , 'PARTICIPANT_INCONNU' ) ;
await refusee ( seance . preparerPlanche ( { source : 'proposition' , proposition : 7 } ) , 'PROPOSITION_INCONNUE' ) ;
await seance . fermer ( ) ;
await refusee ( seance . preparerPlanche ( ) , 'AUCUN_EVENEMENT' ) ;
await refusee ( seance . enregistrerPlancheSous ( ) , 'AUCUN_EVENEMENT' ) ;
assert . deepEqual ( trace , [ ] ) ;
} ) ;
} ) ;
describe ( 'planche dans la séance : l’ annonce (§ 11.2, § 11.5, § 11.8, décision 12)' , ( ) => {
test ( 'le témoin : 65 planches A4 et 129 Lettre, ses comptes, aucun homonyme' , async ( ) => {
const { seance , trace } = await monter ( chargeTemoin ( ) ) ;
const annonce = await seance . preparerPlanche ( ) ;
const { participants , exclus , produits , planchesA4 , planchesLettre } = FORME _TEMOIN ;
assert . deepEqual ( annonce . planches , { A4 : planchesA4 , Lettre : planchesLettre } ) ;
assert . deepEqual ( annonce . comptes , { participants , exclus , produits } ) ;
assert . deepEqual ( [ annonce . refus , annonce . homonymes , annonce . absents , annonce . brouillon ] , [ null , [ ] , [ ] , false ] ) ;
assert . deepEqual ( trace , [ ] ) ;
} ) ;
test ( 'les homonymes que rien ne sépare sont nommés ; un nom à glyphe absent, « Ondine 李 », nomme le caractère' , async ( ) => {
const charge = chargeLucioles ( {
personnes : [
personne ( 1 , 'Iris' , 'Ombrelle' , { appartenance : 'Club des Merles' } ) ,
personne ( 2 , 'Iris' , 'Ombrelle' , { appartenance : 'Club des Merles' } ) ,
personne ( 3 , 'Ondine' , '李' ) ,
personne ( 4 , null , 'Grisaille' ) ,
] ,
} ) ;
const { seance } = await monter ( charge ) ;
const annonce = await seance . preparerPlanche ( ) ;
assert . equal ( annonce . refus , null ) ;
assert . deepEqual ( annonce . homonymes , [ { nom : 'Iris Ombrelle' , ids : [ 1 , 2 ] } ] ) ;
assert . deepEqual ( annonce . absents , [ '李' ] ) ;
} ) ;
test ( 'une proposition s’ annonce BROUILLON, sous son propre horodatage ; sans placement, PLANCHE_SANS_PLACEMENT et rien du modèle' , async ( ) => {
const { seance } = await monter ( ) ;
const proposee = await seance . preparerPlanche ( { source : 'proposition' , proposition : 2 } ) ;
assert . deepEqual ( [ proposee . refus , proposee . brouillon , proposee . nom ] , [ null , true , nomDe ( { horodatage : H _PROPOSITION } ) ] ) ;
const sans = await monter ( chargeLucioles ( { plan : null , etat : 'propose' } ) ) ;
const annonce = await sans . seance . preparerPlanche ( ) ;
assert . deepEqual ( annonce , {
refus : { code : 'PLANCHE_SANS_PLACEMENT' , details : { remede : { geste : 'ouvrirSection' , section : 'propositions' } } } ,
papier : 'A4' ,
comptes : null ,
planches : null ,
homonymes : [ ] ,
absents : [ ] ,
brouillon : true ,
nom : null ,
} ) ;
await refusee ( sans . seance . produirePlanche ( ) , 'PLANCHE_SANS_PLACEMENT' ) ;
await assert . rejects ( sans . seance . preparerPlanche ( { ordre : 'hasard' } ) , TypeError ) ;
assert . deepEqual ( sans . trace , [ ] ) ;
} ) ;
test ( 'dérive : PLANCHE_DERIVE nomme la personne, avant le papier, et rien ne s’ écrit' , async ( ) => {
const charge = chargeLucioles ( ) ;
charge . participants . push ( personne ( 5 , 'Aube' , 'Sarcelle' ) ) ;
charge . prochainsIds . participant = 6 ;
const { seance , trace } = await monter ( charge , { papier : null } ) ;
const annonce = await seance . preparerPlanche ( ) ;
assert . equal ( annonce . refus . code , 'PLANCHE_DERIVE' ) ;
assert . deepEqual ( annonce . refus . details . raisons , [ { code : 'PARTICIPANT_NON_PLACE' , participant : 5 , personne : 'Aube Sarcelle' } ] ) ;
await refusee ( seance . produirePlanche ( ) , 'PLANCHE_DERIVE' ) ;
assert . deepEqual ( trace , [ ] ) ;
} ) ;
test ( 'un nom trop long pour son badge : PLANCHE_NOM_TROP_LONG nomme la personne du badge refusé, pas celle de son rang dans la liste' , async ( ) => {
const long = ` Pervenche ${ 'w' . repeat ( 1990 ) } ` ;
const charge = chargeLucioles ( ) ;
charge . participants [ 1 ] . nom = long ;
const { seance , trace } = await monter ( charge ) ;
const details = { participant : 2 , personne : ` Théo ${ long } ` , remede : { geste : 'ouvrirSection' , section : 'participants' } } ;
const annonce = await seance . preparerPlanche ( ) ;
assert . deepEqual ( annonce . refus , { code : 'PLANCHE_NOM_TROP_LONG' , details } ) ;
await refusee ( seance . produirePlanche ( ) , 'PLANCHE_NOM_TROP_LONG' , details ) ;
assert . deepEqual ( trace , [ ] ) ;
} ) ;
} ) ;
describe ( 'planche dans la séance : les octets et le nom (§ 11.6, § 11.8)' , ( ) => {
test ( 'deux productions du même placement : mêmes octets, même nom ; un geste lancé pendant la seconde attend son tour ; la retouche change le nom ; défaire rend le premier' , async ( ) => {
const { fs , seance , trace } = await monter ( ) ;
const premier = await seance . produirePlanche ( ) ;
const octetsPremiers = fs . octets ( 'documents' , premier . nom ) ;
await seance . passerEnEcriture ( ) ;
trace . length = 0 ;
const production = seance . produirePlanche ( ) ;
const geste = seance . executer ( 'echangerDansRetenu' , { tour : 1 , a : 1 , b : 3 } ) ;
const [ second ] = await Promise . all ( [ production , geste ] ) ;
assert . deepEqual ( second , premier ) ;
assert . deepEqual ( fs . octets ( 'documents' , second . nom ) , octetsPremiers ) ;
// Le PDF part d'abord, puis le geste écrit son entrée et son état.
assert . deepEqual ( trace [ 0 ] , [ 'ecrireOctetsAtomique' , premier . nom ] ) ;
assert . deepEqual ( trace [ 1 ] , [ 'ajouterLigne' , ` ${ BASE } ${ SUFFIXES . journal } ` ] ) ;
assert . ok ( trace . slice ( 1 ) . every ( ( [ primitive ] ) => primitive !== 'ecrireOctetsAtomique' ) ) ;
const retouche = await seance . preparerPlanche ( ) ;
const horodatage = seance . charge . retenu . horodatage ;
assert . notEqual ( horodatage , H _RETENU ) ;
assert . equal ( retouche . nom , nomDe ( { horodatage } ) ) ;
await seance . defaire ( ) ;
const troisieme = await seance . produirePlanche ( ) ;
assert . deepEqual ( troisieme , premier ) ;
assert . deepEqual ( fs . octets ( 'documents' , troisieme . nom ) , octetsPremiers ) ;
} ) ;
test ( 'une réédition de trois badges : un autre nom, « -3 », et la feuille de contrôle dit « Réédition : 3 badges » et nomme le caractère sans glyphe d’ « Ondine 李 »' , async ( ) => {
const charge = chargeLucioles ( ) ;
charge . participants [ 2 ] . nom = '李' ;
const { fs , seance } = await monter ( charge ) ;
const rendu = await seance . produirePlanche ( { selection : [ 3 , 1 , 4 ] } ) ;
assert . equal ( rendu . nom , nomDe ( { reedition : { ids : [ 3 , 1 , 4 ] } } ) ) ;
assert . match ( rendu . nom , /\.badges-3_A4_feuille_/ ) ;
const pdf = await lirePdf ( fs . octets ( 'documents' , rendu . nom ) ) ;
assert . ok ( textesDe ( pdf . pages [ 0 ] ) . includes ( libelle ( 'planche.controle.reedition' , { n : 3 } ) ) , textesDe ( pdf . pages [ 0 ] ) . join ( ' | ' ) ) ;
assert . ok ( textesDe ( pdf . pages [ 0 ] ) . includes ( 'Réédition : 3 badges' ) ) ;
const absents = libelle ( 'planche.controle.absents' , { points : [ 'U+674E' ] } ) ;
assert . ok ( textesDe ( pdf . pages [ 0 ] ) . includes ( absents ) , textesDe ( pdf . pages [ 0 ] ) . join ( ' | ' ) ) ;
assert . equal ( pdf . pages . length , 2 ) ;
} ) ;
2026-10-10 17:22:35 -04:00
test ( 'une proposition produite porte le mot du filigrane au trait, sa feuille de contrôle aussi ; en Lettre sous la demande par défaut, feuille par feuille, dans l’ ordre alphabétique' , async ( ) => {
const { fs , seance } = await monter ( chargeLucioles ( ) , { 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
const rendu = await seance . produirePlanche ( { source : 'proposition' , proposition : 2 } ) ;
2026-10-10 17:22:35 -04:00
assert . equal ( rendu . nom , nomDe ( { papier : 'Lettre' , horodatage : H _PROPOSITION } ) ) ;
[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
const pdf = await lirePdf ( fs . octets ( 'documents' , rendu . nom ) ) ;
assert . ok ( pdf . pages . every ( ( page ) => textesDe ( page ) . includes ( FILIGRANE ) ) ) ;
assert . equal ( auTrait ( pdf ) , true ) ;
2026-10-10 17:22:35 -04:00
// Deux badges par planche : feuille par feuille, la planche p porte les
// badges 2p − 1 et 2p de l'ordre alphabétique — en pile, ce seraient p
// et p + 2 ; par table, la proposition 2 assied Ombrelle avec Grisaille.
const sortie = { papier : 'Lettre' , imposition : 'feuille' , planches : 2 } ;
assert . deepEqual ( sortieLue ( pdf , sortie ) , lignesDeSortie ( sortie ) , textesDe ( pdf . pages [ 0 ] ) . join ( ' | ' ) ) ;
assert . deepEqual (
pdf . pages . slice ( 1 ) . map ( ( page ) => nomsParCellule ( page , 'Lettre' ) ) ,
[
[ 'Grisaille' , 'Ondine Lacasse' ] ,
[ 'Iris Ombrelle' , 'Théo Pervenche' ] ,
] ,
) ;
[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 ( 'planche dans la séance : l’ écriture refusée, enregistrer sous, le web (§ 8.6, décision 13)' , ( ) => {
test ( 'un renommage refusé : PLANCHE_NON_ECRITE nomme le fichier, le dossier et la cause, offre « Enregistrer sous… », et le support de l’ événement reste intact' , async ( ) => {
const { fs , seance , trace } = await monter ( ) ;
fs . pannes . renommage ( 'documents' ) ;
const details = { chemin : nomDe ( ) , dossier : DOCUMENTS , cause : 'EBUSY' , remede : { geste : 'enregistrerSous' } } ;
await refusee ( seance . produirePlanche ( ) , 'PLANCHE_NON_ECRITE' , details ) ;
assert . equal ( seance . supportRompu , false ) ;
assert . equal ( fs . octets ( 'documents' , nomDe ( ) ) , null ) ;
assert . deepEqual ( trace , [ [ 'ecrireOctetsAtomique' , nomDe ( ) ] ] ) ;
assert . match ( libelle ( 'PLANCHE_NON_ECRITE' , details ) , /Enregistrer sous/ ) ;
} ) ;
test ( 'le support rompu : préparer passe, produire se refuse PLANCHE_NON_ECRITE sans toucher au refus du support, et enregistrerPlancheSous passe par le dialogue' , async ( ) => {
const { fs , seance , trace } = await monter ( ) ;
await seance . passerEnEcriture ( ) ;
fs . pannes . ecriture ( 'documents' ) ;
await assert . rejects ( seance . executer ( 'echangerDansRetenu' , { tour : 1 , a : 1 , b : 3 } ) ) ;
assert . equal ( seance . supportRompu , true ) ;
const refusSupport = seance . refusSupport ;
trace . length = 0 ;
assert . equal ( ( await seance . preparerPlanche ( ) ) . refus , null ) ;
await refusee ( seance . produirePlanche ( ) , 'PLANCHE_NON_ECRITE' , {
chemin : nomDe ( ) ,
dossier : DOCUMENTS ,
cause : 'EROFS' ,
remede : { geste : 'enregistrerSous' } ,
} ) ;
assert . deepEqual ( seance . refusSupport , refusSupport ) ;
const recus = [ ] ;
fs . enregistrerSous = async ( nom , octets ) => {
recus . push ( [ nom , octets ] ) ;
return 'E:\\Copies\\planche.pdf' ;
} ;
assert . equal ( await seance . enregistrerPlancheSous ( ) , 'E:\\Copies\\planche.pdf' ) ;
assert . deepEqual ( recus . map ( ( [ nom ] ) => nom ) , [ nomDe ( ) ] ) ;
assert . ok ( recus [ 0 ] [ 1 ] instanceof Uint8Array ) ;
assert . equal ( new TextDecoder ( ) . decode ( recus [ 0 ] [ 1 ] . subarray ( 0 , 5 ) ) , '%PDF-' ) ;
assert . deepEqual ( trace . map ( ( [ primitive ] ) => primitive ) , [ 'ecrireOctetsAtomique' ] ) ;
} ) ;
test ( 'sous web, produire passe par enregistrerSous : le téléchargement, chemin null, rien dans le dossier ; en Lettre, en pile, par table, chaque nom à sa place' , async ( ) => {
const fs = creerFichiersSimules ( { documents : DOCUMENTS , nature : 'web' } ) ;
const { seance , trace } = await monter ( chargeLucioles ( ) , { fs , papier : 'Lettre' } ) ;
const recus = [ ] ;
fs . enregistrerSous = async ( nom , octets ) => {
recus . push ( [ nom , octets ] ) ;
return nom ;
} ;
const attendu = nomDe ( { papier : 'Lettre' , imposition : 'pile' } ) ;
const rendu = await seance . produirePlanche ( { imposition : 'pile' , ordre : 'table' } ) ;
assert . deepEqual ( rendu , { chemin : null , nom : attendu } ) ;
assert . match ( attendu , /_Lettre_pile_/ ) ;
assert . deepEqual ( trace , [ ] ) ;
assert . equal ( recus . length , 1 ) ;
assert . equal ( recus [ 0 ] [ 0 ] , attendu ) ;
assert . equal ( fs . octets ( 'documents' , attendu ) , null ) ;
// Par table, le tour 1 donne Ombrelle, Pervenche (table 1), puis
// Grisaille, Lacasse (table 2) ; en pile sur deux planches, la planche p
// porte les badges p et p + 2 de cet ordre.
const pdf = await lirePdf ( recus [ 0 ] [ 1 ] ) ;
assert . equal ( pdf . pages . length , 3 ) ;
2026-10-10 17:22:35 -04:00
const sortie = { papier : 'Lettre' , imposition : 'pile' , planches : 2 } ;
assert . deepEqual ( sortieLue ( pdf , sortie ) , lignesDeSortie ( sortie ) , textesDe ( pdf . pages [ 0 ] ) . join ( ' | ' ) ) ;
[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
assert . deepEqual (
2026-10-10 17:22:35 -04:00
pdf . pages . slice ( 1 ) . map ( ( page ) => nomsParCellule ( page , '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
[
[ 'Iris Ombrelle' , 'Grisaille' ] ,
[ 'Théo Pervenche' , 'Ondine Lacasse' ] ,
] ,
) ;
} ) ;
} ) ;