From 2bca543e7efa969bdf5d5ec83df39f56475725b7 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 6 Oct 2026 17:03:15 -0400 Subject: [PATCH] [ADD] demo: delivered event files, example CSVs, payload fingerprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four demonstrations ship as event files written once by a script, the source of truth of § 15.5: a test replays the generator and compares SHA-256 fingerprints of the payload alone, naming the paths that differ, so a new build version never forces a regeneration. The affiliation profile is read from the delivered file. The two demonstration CSVs are the export of those files; the edge-case CSV is written by hand, and its windows-1252 variant derived. Version and forged-name guards skip these generated files, as § 18.5 and § 15.6 allow. Checked: node and long series; the script writes only its seven files. --- FR --- [ADD] démo : fichiers d'événement livrés, CSV d'exemple, empreinte Les quatre démonstrations se livrent en fichiers d'événement qu'un script écrit une fois, source de vérité du § 15.5 : une épreuve rejoue le générateur et compare les empreintes SHA-256 de la seule charge, en nommant les chemins qui diffèrent ; une nouvelle version n'impose donc aucune régénération. Le profil d'appartenances se lit dans le fichier livré. Les deux CSV de démonstration sont l'export de ces fichiers ; celui des cas limites s'écrit à la main, sa variante windows-1252 en dérive. Les gardes de version et de noms forgés passent ces fichiers. Vérifié : séries node et longue ; le script n'écrit que ses sept fichiers. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb --- .gitattributes | 2 + exemples/participants_cas_limites.csv | 16 + exemples/participants_cas_limites_cp1252.csv | 16 + exemples/participants_demo_grande.csv | 261 ++++++++++ exemples/participants_demo_petite.csv | 13 + scripts/engendrer_livrables.js | 351 +++++++++++++ scripts/engendrer_livrables.test.js | 491 ++++++++++++++++++ scripts/version.js | 11 +- scripts/version.test.js | 9 + src/demo/catalogue.long.test.js | 63 ++- src/demo/livrees.js | 57 ++ src/demo/livrees.long.test.js | 166 ++++++ src/demo/livrees.test.js | 233 +++++++++ .../livrees/grande-sans-exception.gtt.json | 377 ++++++++++++++ src/demo/livrees/grande.gtt.json | 377 ++++++++++++++ src/demo/livrees/petite-conflit.gtt.json | 32 ++ src/demo/livrees/petite.gtt.json | 32 ++ src/demo/noms.js | 5 +- 18 files changed, 2493 insertions(+), 19 deletions(-) create mode 100644 exemples/participants_cas_limites.csv create mode 100644 exemples/participants_cas_limites_cp1252.csv create mode 100644 exemples/participants_demo_grande.csv create mode 100644 exemples/participants_demo_petite.csv create mode 100644 scripts/engendrer_livrables.js create mode 100644 scripts/engendrer_livrables.test.js create mode 100644 src/demo/livrees.js create mode 100644 src/demo/livrees.long.test.js create mode 100644 src/demo/livrees.test.js create mode 100644 src/demo/livrees/grande-sans-exception.gtt.json create mode 100644 src/demo/livrees/grande.gtt.json create mode 100644 src/demo/livrees/petite-conflit.gtt.json create mode 100644 src/demo/livrees/petite.gtt.json diff --git a/.gitattributes b/.gitattributes index e3d801d..77dae47 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ * text=auto eol=lf *.cmd text eol=crlf test/fixtures/csv/** -text +src/demo/livrees/*.gtt.json -text +exemples/*.csv -text diff --git a/exemples/participants_cas_limites.csv b/exemples/participants_cas_limites.csv new file mode 100644 index 0000000..3c367cc --- /dev/null +++ b/exemples/participants_cas_limites.csv @@ -0,0 +1,16 @@ +Nom;Prénom;Organisation;Courriel;Rôle;Exclu;Notes;Téléphone +Lanterneau;Basile;Chœur de l’Anse;basile.lanterneau@exemple.test;animation;;;555-0101 +Pommerel;Capucine;Société des Hérons;capucine.pommerel@exemple.test;;;;555-0102 +Marbrelle;Gaspard;Cercle des Saules;;animation;;;555-0103 +Sablonnet;Ysaline;Chœur de l’Anse;;;;;555-0104 +Givrecœur;Anselme;;anselme.givrecoeur@exemple.test;;;;555-0105 +Tourterel;Héloïse;Cercle des Saules;;;oui;S’est désistée la veille;555-0106 +Fenouillet;Côme;Société des Hérons;;;;"végétarien; sans noix, ni arachides";555-0107 +Coudraie;Apolline;Chœur de l’Anse;;;;"Arrive après le premier tour +repart avant le dessert";555-0108 +Lanterneau;Basile;Chœur de l’Anse;;;;;555-0109 +Brunœil;;Cercle des Saules;brunoeil@exemple.test;;;;555-0110 +Ormelin;Faustine;Societe des herons;;;;;555-0111 +;Clotilde;Cercle des Saules;clotilde@exemple.test;;;;555-0112 +Ardoisier;Edgar;Société des Hérons;;;;;555-0113 +Cressonnier;Bertille;Cercle des Saules;;;;;555-0114 diff --git a/exemples/participants_cas_limites_cp1252.csv b/exemples/participants_cas_limites_cp1252.csv new file mode 100644 index 0000000..2274e8c --- /dev/null +++ b/exemples/participants_cas_limites_cp1252.csv @@ -0,0 +1,16 @@ +Nom,Prénom,Organisation,Courriel,Rôle,Exclu,Notes,Téléphone +Lanterneau,Basile,Chœur de l’Anse,basile.lanterneau@exemple.test,animation,,,555-0101 +Pommerel,Capucine,Société des Hérons,capucine.pommerel@exemple.test,,,,555-0102 +Marbrelle,Gaspard,Cercle des Saules,,animation,,,555-0103 +Sablonnet,Ysaline,Chœur de l’Anse,,,,,555-0104 +Givrecœur,Anselme,,anselme.givrecoeur@exemple.test,,,,555-0105 +Tourterel,Héloïse,Cercle des Saules,,,oui,S’est désistée la veille,555-0106 +Fenouillet,Côme,Société des Hérons,,,,"végétarien; sans noix, ni arachides",555-0107 +Coudraie,Apolline,Chœur de l’Anse,,,,"Arrive après le premier tour +repart avant le dessert",555-0108 +Lanterneau,Basile,Chœur de l’Anse,,,,,555-0109 +Brunœil,,Cercle des Saules,brunoeil@exemple.test,,,,555-0110 +Ormelin,Faustine,Societe des herons,,,,,555-0111 +,Clotilde,Cercle des Saules,clotilde@exemple.test,,,,555-0112 +Ardoisier,Edgar,Société des Hérons,,,,,555-0113 +Cressonnier,Bertille,Cercle des Saules,,,,,555-0114 diff --git a/exemples/participants_demo_grande.csv b/exemples/participants_demo_grande.csv new file mode 100644 index 0000000..2c7b344 --- /dev/null +++ b/exemples/participants_demo_grande.csv @@ -0,0 +1,261 @@ +nom;prenom;appartenance;courriel;titre_pressenti;exclu;notes +Mirvault;Isabelle;Comptoir Gravelune;;;non; +Neskière;Youssef;Verrerie Vaubrenne;;animation;non; +Abrevande;Félix;Imprimerie Yzorne;;;non; +Fralvançon;Hugo;Brasserie Wassorel;;;non; +Fralveux;Manon;Menuiserie Estrolle;;;non; +Luskotte;Catherine;Fonderie Estrolle;;;non; +WAMMERWOLM-MAWWEMMEL;Zoé;Menuiserie Ulvardes;;;non; +Neskichon;Louise;Ateliers Rossolde;;animation;non; +Urvesson;Catherine;Fabrique Ulvardes;;;non; +Hulvivet;Karim;Fonderie Xandrelle;;;non; +Selveron;Caroline;Ateliers Cassorgue;;;non; +Korvault;Catherine;Tannerie Nervanche;;;non; +Fralvotte;Caroline;Fonderie Ivoranche;;;non; +Jusvesson;Geneviève;Tannerie Belvarre;;;non; +Abrevumet;Paul;Ateliers Aubrenc;;;non; +Fralvault;Léa;Fonderie Sorbagne;;;non; +Obrivet;Fatima;Fabrique Sorbagne;;;non; +Voskeval;Philippe;Menuiserie Estrolle;;animation;non; +Dulveval;Zoé;Menuiserie Quimvaux;;;non; +Korveux;Paul;Filature Olbrecq;;;non; +Luskière;Julie;Fabrique Sorbagne;;;non; +Clovande;Émilie;Ateliers Falvigne;;;non; +Hulvande;Paul;Verrerie Vaubrenne;;;non; +Fralvesson;Hugo;Filature Wassorel;;;non; +Urvière;Isabelle;Brasserie Belvarre;;;non; +Rozvotte;Thomas;Verrerie Ivoranche;;;non; +Bralmumet;Isabelle;Verrerie Aubrenc;;;non; +Dulvouze;Catherine;Ateliers Rossolde;;;non; +Voskichon;Marc;Menuiserie Estrolle;;;non; +Mirvivet;Simon;Ateliers Ivoranche;;;non; +Clovault;Louise;Imprimerie Yzorne;;;non; +Voskeux;Lucie;Coopérative Morvalle;;;non; +Eskignac;Amina;Brasserie Xandrelle;;;non; +Korvouze;Fatima;Ateliers Tavrenne;;animation;non; +Selvoubre;Luc;Brasserie Sorbagne;;;non; +Fralvichon;Étienne;Papeterie Ulvardes;;;non; +Voskivet;Philippe;Ateliers Tavrenne;;;non; +Quilmançon;Louise;Menuiserie Estrolle;;;non; +Luskande;Sophie;Fabrique Morvalle;;;non; +Abreveron;Pierre;Ateliers Rossolde;;;non; +Neskeux;Philippe;Comptoir Xandrelle;;;non; +Neskançon;Amina;Fabrique Ulvardes;;;non; +Tolvande;Léa;Ateliers Rossolde;;;non; +Quilmeron;Antoine;Fabrique Kalvenne;;;non; +Clovumet;Guillaume;Verrerie Pravette;;;non; +Eskeux;Michel;Papeterie Wassorel;;;non; +Hulvault;Louise;Coopérative Jolvane;;;non; +Hulveron;Catherine;Coopérative Rossolde;;animation;non; +Ivrivet;François;Menuiserie Sorbagne;;animation;non; +Eskumet;Samuel;Verrerie Ivoranche;;;non; +Fralvivet;Claire;Imprimerie Yzorne;;;non; +Mirveux;Léa;Menuiserie Wassorel;;;non; +Gorzesson;Karim;Papeterie Ulvardes;;;non; +Fralvumet;Jean;Ateliers Falvigne;;animation;non; +Urvançon;Manon;Ateliers Tavrenne;;animation;non; +Quilmande;Sylvie;Imprimerie Yzorne;;;non; +Gorzichon;François;Ateliers Estrolle;;;non; +Pruvesson;Amina;Fonderie Sorbagne;;;non; +Quilmière;Lucie;Fabrique Ivoranche;;;non; +Korvesson;Fatima;Ateliers Rossolde;;;non; +Selvesson;Nicolas;Imprimerie Yzorne;;;non; +Selvivet;Louis;Menuiserie Hostrenne;;animation;non; +Neskesson;Louise;Tannerie Belvarre;;;non; +Tolvesson;Lucie;Filature Zelvaine;;;non; +Quilmesson;Olivier;Comptoir Gravelune;;;non; +Luskivet;Guillaume;Brasserie Lavorneau;;;non; +Neskault;Gabriel;Menuiserie Tavrenne;;;non; +Urvumet;Antoine;Menuiserie Hostrenne;;;non; +Jusvançon;Olivier;Brasserie Morvalle;;;non; +Dulvançon;Simon;Menuiserie Ivoranche;;;non; +Gorzard;Chloé;Fabrique Sorbagne;;;non; +Pruveux;Gabriel;Papeterie Estrolle;;;non; +Clovière;Manon;Ateliers Ivoranche;;;non; +Obrouze;François;Tannerie Olbrecq;;animation;non; +Tolveval;Karim;Tannerie Belvarre;;;non; +Dulvumet;Geneviève;Menuiserie Quimvaux;;animation;non; +Bérêçàuvïlle-Dœûmôntëÿ;Hugo;Ateliers Falvigne;;;non; +Gorzeron;Claire;Menuiserie Hostrenne;;;non; +Luskumet;Hélène;Brasserie Xandrelle;;;non; +Fralvande;Louis;Ateliers Rossolde;;;non; +Selvault;Émilie;Fabrique Sorbagne;;;non; +Neskumet;Léa;Verrerie Aubrenc;;;non; +Korvichon;Isabelle;Menuiserie Ulvardes;;;non; +Obrande;Michel;Menuiserie Estrolle;;;non; +Luskeval;Isabelle;Brasserie Wassorel;;;non; +Tolvoubre;Fatima;Papeterie Ulvardes;;;non; +Gorzière;Zoé;Menuiserie Olbrecq;;;non; +Korvignac;Sylvie;Fabrique Ulvardes;;;non; +Obreval;Antoine;Ateliers Cassorgue;;;non; +Korveron;Michel;Fonderie Sorbagne;;animation;non; +Tolvumet;Camille;Fonderie Estrolle;;;non; +Dulvesson;Marie;Ateliers Cassorgue;;;non; +Jusvard;Youssef;Tannerie Aubrenc;;animation;non; +Tolvignac;Guillaume;Menuiserie Sorbagne;;;non; +Mirvotte;Gabriel;Tannerie Belvarre;;;non; +Urvard;Marie;Filature Wassorel;;animation;non; +Bralmesson;Marie;Coopérative Dravenne;;animation;non; +Neskignac;Paul;Brasserie Sorbagne;;;non; +Bralmeux;Samuel;Ateliers Rossolde;;animation;non; +Ivrotte;Raphaël;Coopérative Rossolde;;;non; +Luskouze;Marc;Ateliers Rossolde;;;non; +Jusvotte;Sylvie;Brasserie Belvarre;;;non; +Bralmoubre;Véronique;Fonderie Xandrelle;;;non; +Selvard;Pierre;Verrerie Vaubrenne;;;non; +Hulveux;Marie;Coopérative Rossolde;;;non; +Pruvotte;Sophie;Imprimerie Yzorne;;animation;non; +Fralveval;Karim;Coopérative Gravelune;;;non; +Gorzignac;Philippe;Imprimerie Falvigne;;;non; +Selvouze;Étienne;Ateliers Tavrenne;;;non; +Hulvichon;Louis;Ateliers Falvigne;;;non; +Urvoubre;Sylvie;Verrerie Vaubrenne;;;non; +Pruveval;Pierre;Comptoir Xandrelle;;;non; +Hulvotte;Guillaume;Verrerie Vaubrenne;;;non; +Abrevignac;Geneviève;Brasserie Morvalle;;;non; +Abrevault;Gabriel;Ateliers Rossolde;;animation;non; +Bralmignac;Amina;Fonderie Estrolle;;;non; +Bralmançon;Olivier;Comptoir Lavorneau;;;non; +Luskoubre;Marc;Verrerie Vaubrenne;;;non; +Abrevoubre;Geneviève;Menuiserie Gravelune;;;non; +Korvoubre;Raphaël;Verrerie Vaubrenne;;;non; +Mirvard;Pierre;Verrerie Ivoranche;;;non; +Jusvumet;Camille;Fonderie Estrolle;;;non; +Cloveval;Samuel;Menuiserie Ulvardes;;;non; +Selvière;Antoine;Coopérative Dravenne;;;non; +Jusvouze;Véronique;Verrerie Vaubrenne;;;non; +Neskivet;Paul;Comptoir Jolvane;;;non; +Dulvande;Samuel;Verrerie Pravette;;;non; +Bralmault;Antoine;Fabrique Sorbagne;;animation;non; +Abrevière;Émilie;Imprimerie Yzorne;;animation;non; +Selveval;Lucie;Filature Zelvaine;;;non; +Tolvichon;Olivier;Fonderie Xandrelle;;;non; +Hulvard;Fatima;Ateliers Estrolle;;;non; +Ivreux;Alexandre;Tannerie Aubrenc;;;non; +Korveval;Félix;Tannerie Nervanche;;;non; +Urvichon;Karim;Tannerie Nervanche;;;non; +Dulvichon;Émilie;Menuiserie Ulvardes;;;non; +Tolvouze;Luc;Fabrique Ivoranche;;;non; +Jusvivet;Claire;Imprimerie Yzorne;;animation;non; +Quilmault;Amina;Coopérative Jolvane;;;non; +Ivrouze;François;Fonderie Aubrenc;;;non; +Gorzumet;Hélène;Fonderie Aubrenc;;;non; +Ivrault;Sophie;Coopérative Morvalle;;;non; +Bralmière;Louise;Tannerie Quimvaux;;;non; +Korvande;Nicolas;Comptoir Sorbagne;;;non; +Ivrumet;Geneviève;Coopérative Jolvane;;;non; +Obresson;Geneviève;Coopérative Gravelune;;;non; +Obrançon;Guillaume;Papeterie Hostrenne;;;non; +Urvignac;Fatima;Ateliers Aubrenc;;;non; +Bralmotte;Claire;Ateliers Tavrenne;;;non; +Hulvignac;Félix;Imprimerie Hostrenne;;;non; +Clovotte;François;Coopérative Rossolde;;;non; +Dulvoubre;Philippe;Brasserie Belvarre;;;non; +Rozveron;Samuel;Fonderie Sorbagne;;;non; +Hulvoubre;Pierre;Menuiserie Olbrecq;;;non; +Urvouze;Émilie;Comptoir Gravelune;;;non; +Korvançon;Félix;Fonderie Ivoranche;;;non; +Neskande;Émilie;Coopérative Gravelune;;;non; +Mirvumet;Chantal;Menuiserie Gravelune;;;non; +Korvotte;Alexandre;Menuiserie Nervanche;;;non; +Eskeval;Zoé;Tannerie Nervanche;;;non; +Neskard;Camille;Menuiserie Gravelune;;animation;non; +Tolveron;Manon;Brasserie Belvarre;;animation;non; +Voskoubre;Nathalie;Fonderie Kalvenne;;;non; +Pruvière;Sophie;Ateliers Rossolde;;;non; +Obrichon;Paul;Ateliers Rossolde;;;non; +Abrevançon;Fatima;Tannerie Nervanche;;animation;non; +Eskière;Nathalie;Fonderie Xandrelle;;;non; +Ivreron;Caroline;Imprimerie Falvigne;;;non; +Neskoubre;Véronique;Fonderie Estrolle;;;non; +Urvivet;Anne;Fonderie Kalvenne;;;non; +Hulvançon;Olivier;Menuiserie Estrolle;;;non; +Clovançon;Karim;Fabrique Sorbagne;;;non; +Quilmotte;Zoé;Ateliers Aubrenc;;;non; +Eskouze;Félix;Filature Zelvaine;;;non; +Rozvichon;Amina;Verrerie Olbrecq;;;non; +Fralvière;Simon;Comptoir Sorbagne;;;non; +Vandergrolcquesmirtavelle-Ostrovaubelindigues;Marc;Tannerie Aubrenc;;animation;non; +Hulvière;Pierre;Papeterie Hostrenne;;;non; +Jusvault;Raphaël;Fabrique Sorbagne;;;non; +Dulvivet;Guillaume;Filature Olbrecq;;animation;non; +Fralvoubre;Pierre;Imprimerie Falvigne;;;non; +Selveux;Hélène;Brasserie Lavorneau;;;non; +Rozvançon;Caroline;Brasserie Belvarre;;;non; +Korvière;Olivier;Ateliers Falvigne;;;non; +Ivrichon;Nicolas;Fabrique Sorbagne;;;non; +Korvumet;Félix;Imprimerie Yzorne;;;non; +Voskande;Félix;Ateliers Falvigne;;animation;non; +Selvumet;Guillaume;Papeterie Estrolle;;;non; +Urvotte;Michel;Verrerie Ivoranche;;;non; +Eskault;Émilie;Coopérative Jolvane;;;non; +Bralmeval;Hélène;Brasserie Dravenne;;;non; +Hulvesson;Guillaume;Comptoir Jolvane;;;non; +Jusvoubre;Caroline;Fabrique Sorbagne;;;non; +Luskeux;Félix;Tannerie Belvarre;;;non; +Pruvouze;Antoine;Imprimerie Yzorne;;;non; +Quilmignac;Julie;Menuiserie Jolvane;;;non; +Tolvançon;Étienne;Menuiserie Estrolle;;animation;non; +Voskumet;Hugo;Ateliers Tavrenne;;;non; +Quilmumet;Manon;Fonderie Sorbagne;;;non; +Voskouze;Marc;Fonderie Estrolle;;;non; +Abrevard;Samuel;Filature Dravenne;;;non; +Luskichon;Guillaume;Brasserie Belvarre;;animation;non; +Clovesson;Manon;Fabrique Sorbagne;;;non; +Hulvumet;Simon;Ateliers Ivoranche;;;non; +Voskeron;Lucie;Verrerie Ivoranche;;;non; +Selvignac;Thomas;Brasserie Sorbagne;;animation;non; +Neskeron;Jean;Filature Zelvaine;;;non; +Pruvivet;Manon;Menuiserie Aubrenc;;;non; +Clovard;Sophie;Brasserie Belvarre;;;non; +Gorzeval;Étienne;Papeterie Ulvardes;;;non; +Ivroubre;Philippe;Ateliers Tavrenne;;animation;non; +Luskault;Gabriel;Verrerie Gravelune;;;non; +Korvard;Émilie;Verrerie Vaubrenne;;;non; +Quilmichon;Thomas;Fonderie Estrolle;;;non; +Eskançon;Alexandre;Menuiserie Hostrenne;;;non; +Luskançon;Émilie;Ateliers Tavrenne;;;non; +Dulvault;Marc;Verrerie Ivoranche;;;non; +Rozveval;Hélène;Comptoir Ivoranche;;;non; +Pruvançon;Julie;Imprimerie Yzorne;;;non; +Mirveron;Louis;Filature Wassorel;;;non; +Eskard;François;Fonderie Xandrelle;;;non; +Bralmouze;Hugo;Tannerie Falvigne;;animation;non; +Ivrançon;Louis;Coopérative Morvalle;;;non; +Clovivet;Karim;Fonderie Kalvenne;;;non; +Rozvande;Léa;Verrerie Olbrecq;;;non; +Hulvouze;Amina;Coopérative Dravenne;;;non; +Eskesson;Étienne;Menuiserie Ulvardes;;;non; +Abreveval;Philippe;Ateliers Falvigne;;;non; +Quilmeux;Émilie;Ateliers Falvigne;;;non; +Ivreval;Gabriel;Verrerie Vaubrenne;;animation;non; +Voskotte;Simon;Fabrique Ivoranche;;;non; +Obrignac;Chantal;Imprimerie Yzorne;;;non; +Eskotte;Guillaume;Verrerie Nervanche;;;non; +Obrard;Léa;Comptoir Sorbagne;;;non; +Gorzivet;Guillaume;Fonderie Estrolle;;;non; +Mirvançon;Geneviève;Brasserie Belvarre;;;non; +Selvotte;Julie;Comptoir Xandrelle;;;non; +Mirvesson;Amina;Brasserie Sorbagne;;;non; +Luskignac;Isabelle;Papeterie Estrolle;;;non; +Obrumet;Chloé;Comptoir Yzorne;;;non; +Abrevesson;Anne;Fonderie Estrolle;;;non; +Rozvumet;Fatima;Menuiserie Hostrenne;;;non; +Mirvoubre;Guillaume;Comptoir Sorbagne;;;non; +Rozvoubre;Véronique;Fonderie Xandrelle;;;non; +Gorzotte;Claire;Menuiserie Olbrecq;;;non; +Ivrière;Antoine;Fabrique Sorbagne;;;non; +Selvichon;Isabelle;Tannerie Nervanche;;;non; +Urvault;Isabelle;Comptoir Yzorne;;;non; +Voskesson;Julie;Brasserie Belvarre;;;non; +Abrevotte;Chantal;Ateliers Ivoranche;;;non; +Neskotte;Amina;Fabrique Kalvenne;;;non; +Voskançon;Étienne;Menuiserie Olbrecq;;;non; +Fralveron;Zoé;Menuiserie Sorbagne;;;non; +Rozvouze;Marc;Ateliers Estrolle;;;non; +Abreveux;Sylvie;Comptoir Jolvane;;;non; +Pruvignac;Karim;Menuiserie Estrolle;;;non; +Mirveval;Zoé;Fabrique Kalvenne;;;non; +Fralvignac;Philippe;Menuiserie Estrolle;;;non; +Gorzeux;Gabriel;Menuiserie Ulvardes;;;non; +Urveval;Jean;Menuiserie Quimvaux;;;non; diff --git a/exemples/participants_demo_petite.csv b/exemples/participants_demo_petite.csv new file mode 100644 index 0000000..34b0f14 --- /dev/null +++ b/exemples/participants_demo_petite.csv @@ -0,0 +1,13 @@ +nom;prenom;appartenance;courriel;titre_pressenti;exclu;notes +Bralmumet;Caroline;Ateliers Jolvane;;;non; +Neskault;Hugo;Tannerie Cassorgue;;;non; +Ivrotte;Youssef;Verrerie Ulvardes;;;non; +Selvière;Louis;Tannerie Cassorgue;;;non; +Selvançon;Youssef;Ateliers Jolvane;;;non; +Quilmoubre;Amina;Verrerie Ulvardes;;;non; +Voskotte;Julie;Verrerie Ulvardes;;;non; +Neskivet;Paul;Ateliers Jolvane;;;non; +Bralmignac;Michel;Tannerie Cassorgue;;;non; +Tolveval;Zoé;Verrerie Ulvardes;;;non; +Urvard;Luc;Tannerie Cassorgue;;;non; +Mirveval;Véronique;Ateliers Jolvane;;;non; diff --git a/scripts/engendrer_livrables.js b/scripts/engendrer_livrables.js new file mode 100644 index 0000000..4bc5162 --- /dev/null +++ b/scripts/engendrer_livrables.js @@ -0,0 +1,351 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Les fichiers livrés que le catalogue des démonstrations détermine (§ 10.3, +// § 15.5) : le fichier d'état de chaque démonstration, sous +// src/demo/livrees/ ; l'export CSV de la liste des participants de la petite +// et de la grande, et la variante windows-1252 du CSV des cas limites, sous +// exemples/. Le CSV des cas limites s'écrit à la main : le script le lit et +// ne l'écrit jamais. +// +// node scripts/engendrer_livrables.js [destination] +// +// La destination, la racine du projet par défaut, reçoit les sept fichiers +// sous leurs chemins relatifs, et rien n'est écrit ailleurs ; un chemin +// relatif se lit depuis le répertoire courant. Tout se calcule avant la +// première écriture. Le fichier livré est la source de vérité (§ 15.5, +// point 5) : src/demo/livrees.long.test.js rejoue ce script et compare +// l'empreinte de chaque charge à celle du fichier livré. Lancer le script +// est donc le geste conscient qui change une démonstration : il imprime, +// pour chaque fichier, s'il est créé, inchangé ou réécrit, et l'empreinte de +// la charge de chaque fichier d'état. Ni horloge ni tirage n'y entrent. +// +// Une démonstration livrée est la configuration de son entrée du catalogue, +// mise en charge (§ 4, § 8.8) : +// - l'événement demo:, au nom du catalogue, sans date, en brouillon et +// sans filiation ; +// - les sièges par défaut : la capacité la plus fréquente des tables, la +// plus grande à égalité ; une table d'une autre capacité porte la sienne +// en surcharge (§ 6.1) ; +// - les tables rondes, rangées en grille dans l'ordre de la configuration, +// rangée par rangée, leurs centres espacés de 300 cm ; la grille a la plus +// petite largeur dont le carré contient toutes les tables, et seule sa +// dernière rangée est incomplète ; +// - les contraintes de la configuration, les sièges non attribués, la +// génération par défaut de la recherche ; +// - chaque réservation, celle d'un animateur, au siège 1 de sa table pour +// tous les tours, ce siège titré « animation », et le titre pressenti +// « animation » porté par l'animateur ; +// - aucune proposition : le placement n'est pas figé (§ 15.5, point 6). +// Son fichier d'état s'écrit à la révision 1, sans journal, sous la version +// de la construction qui écrit (§ 18.6). +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { decoder } from '../src/csv/encodage.js'; +import { SEPARATEUR, ecrireCsv, exporterParticipants } from '../src/csv/export.js'; +import { decouper } from '../src/csv/lecture.js'; +import { CATALOGUE } from '../src/demo/catalogue.js'; +import { serialiser, serialiserCharge } from '../src/stockage/canonique.js'; +import { analyser, creerCharge } from '../src/stockage/document.js'; +import { VERSION } from '../src/version.genere.js'; + +const RACINE = resolve(fileURLToPath(new URL('..', import.meta.url))); + +// Chemins des livrables, relatifs à la racine, séparés par « / ». +const DOSSIER_LIVREES = 'src/demo/livrees'; +const CAS_LIMITES = 'exemples/participants_cas_limites.csv'; +const CAS_LIMITES_CP1252 = 'exemples/participants_cas_limites_cp1252.csv'; +const csvDeDemonstration = (cle) => `exemples/participants_demo_${cle}.csv`; + +// Démonstrations dont la liste des participants est livrée en CSV, dans +// l'ordre du § 10.3. +const DEMONSTRATIONS_EN_CSV = ['petite', 'grande']; + +// Séparateur de la variante windows-1252 (§ 10.3). +const VIRGULE = ','; + +// Écart entre les centres de deux tables voisines de la grille, en cm. +const ECART_CM = 300; + +// Le siège de l'animateur, et le titre qu'il porte. +const SIEGE_ANIMATEUR = 1; +const ANIMATION = 'animation'; + +const USAGE = 'usage : node scripts/engendrer_livrables.js [destination]'; + +// Capacité la plus fréquente parmi les tables, la plus grande à égalité ; le +// résultat ne dépend pas de l'ordre des tables. +function capaciteParDefaut(tables) { + const effectifs = new Map(); + for (const { capacite } of tables) effectifs.set(capacite, (effectifs.get(capacite) ?? 0) + 1); + let defaut = null; + for (const [capacite, effectif] of effectifs) { + const effectifDuDefaut = effectifs.get(defaut) ?? 0; + if (effectif > effectifDuDefaut || (effectif === effectifDuDefaut && capacite > defaut)) defaut = capacite; + } + return defaut; +} + +// Largeur de la grille de nombre tables : la plus petite largeur c telle +// que c × c ≥ nombre. +function colonnesDe(nombre) { + let colonnes = 1; + while (colonnes * colonnes < nombre) colonnes += 1; + return colonnes; +} + +// Plus grand identifiant d'une liste d'enregistrements ; 0 pour une liste +// vide. +const plusGrandId = (enregistrements) => enregistrements.reduce((plus, { id }) => Math.max(plus, id), 0); + +// Animateur de chaque table réservée, par identifiant de table, dans l'ordre +// des réservations. Lève quand une réservation ne vaut pas pour tous les +// tours, ou qu'une table en reçoit deux : le siège 1 ne porte qu'un +// animateur, et le script n'écrit que ce que le catalogue produit. +function animateursDe(reservations, cle) { + const animateurs = new Map(); + for (const { participant, table, portee } of reservations) { + if (portee !== 'tous' || animateurs.has(table)) { + throw new Error( + `démonstration ${cle} : la table ${table} reçoit au plus un animateur, réservé pour tous les tours`, + ); + } + animateurs.set(table, participant); + } + return animateurs; +} + +/** + * Charge de la démonstration d'une entrée du catalogue, telle que l'en-tête + * du module la décrit. La configuration est construite une fois ; la charge + * rendue est faite d'objets neufs. + * + * @param {{cle: string, nom: string, construire: () => import('../src/moteur/types.js').Configuration}} entree + * @returns {import('../src/stockage/types.js').Charge} + * @throws {Error} quand une réservation ne vaut pas pour tous les tours, ou + * qu'une table en reçoit deux + */ +export function chargeDeDemonstration({ cle, nom, construire }) { + const { participants, tables, tours, reservations, contraintes } = construire(); + const siegesParDefaut = capaciteParDefaut(tables); + const neuve = creerCharge({ id: `demo:${cle}`, nom, date: null, siegesParDefaut, tours }); + const animateurs = animateursDe(reservations, cle); + const reserves = new Set(animateurs.values()); + const colonnes = colonnesDe(tables.length); + const { separerAppartenances, nouveauxVoisins, nouvelleTable, varierAppartenances } = contraintes; + return { + ...neuve, + reglages: { ...neuve.reglages, separerAppartenances, nouveauxVoisins, nouvelleTable, varierAppartenances }, + prochainsIds: { participant: plusGrandId(participants) + 1, table: plusGrandId(tables) + 1, proposition: 1 }, + participants: participants.map((participant) => ({ + id: participant.id, + nom: participant.nom, + prenom: participant.prenom ?? null, + appartenance: participant.appartenance, + courriel: null, + titrePressenti: reserves.has(participant.id) ? ANIMATION : null, + notes: null, + exclu: participant.exclu === true, + })), + tables: tables.map(({ id, numero, capacite }, rang) => ({ + id, + numero, + sieges: capacite === siegesParDefaut ? null : capacite, + forme: 'ronde', + position: { x: (rang % colonnes) * ECART_CM, y: Math.floor(rang / colonnes) * ECART_CM }, + })), + reservations: [...animateurs].map(([table, participant]) => ({ + participant, + table, + siege: SIEGE_ANIMATEUR, + portee: 'tous', + tour: null, + })), + titres: [...animateurs.keys()].map((table) => ({ table, siege: SIEGE_ANIMATEUR, libelle: ANIMATION })), + }; +} + +/** + * Texte du fichier d'état livré d'une entrée du catalogue : sa charge, en + * forme canonique, à la révision 1, sous la version de la construction qui + * écrit. + * + * @param {{cle: string, nom: string, construire: Function}} entree + * @param {{produitVersion?: string}} [options] version affichée de la + * construction, VERSION.affichee par défaut + * @returns {string} + */ +export function texteDeDemonstration(entree, { produitVersion = VERSION.affichee } = {}) { + return serialiser(chargeDeDemonstration(entree), { revision: 1, produitVersion }); +} + +/** + * Empreinte d'une charge (§ 15.5, point 5) : SHA-256 de son texte canonique + * seul, en 64 chiffres hexadécimaux minuscules. L'en-tête n'y entre pas : la + * version de la construction qui écrit ne la change pas. + * + * @param {import('../src/stockage/types.js').Charge} charge + * @returns {string} + */ +export function empreinte(charge) { + return createHash('sha256').update(serialiserCharge(charge)).digest('hex'); +} + +// Octet windows-1252 de chaque caractère que ce codage représente, lu dans +// le décodeur windows-1252 lui-même, octet par octet : l'encodage en est +// l'inverse exact. 00 à 7F donnent l'ASCII, A0 à FF les mêmes points de +// code, 80 à 9F les signes propres à windows-1252, dont « € », « œ » et +// l'apostrophe typographique. +const DECODEUR_1252 = new TextDecoder('windows-1252'); +const OCTET_1252 = new Map( + Array.from({ length: 256 }, (_, octet) => [DECODEUR_1252.decode(Uint8Array.of(octet)), octet]), +); + +/** + * Octets windows-1252 d'un texte, un par caractère, sans marque. + * + * @param {string} texte + * @returns {Uint8Array} + * @throws {RangeError} pour un caractère que windows-1252 ne représente pas, + * nommé par son point de code et son rang dans le texte, en unités UTF-16 + */ +export function enWindows1252(texte) { + const octets = []; + let rang = 0; + for (const caractere of texte) { + const octet = OCTET_1252.get(caractere); + if (octet === undefined) { + const point = caractere.codePointAt(0).toString(16).toUpperCase().padStart(4, '0'); + throw new RangeError(`U+${point}, au rang ${rang} : caractère absent de windows-1252`); + } + octets.push(octet); + rang += caractere.length; + } + return Uint8Array.from(octets); +} + +/** + * Variante windows-1252 du CSV des cas limites (§ 10.3) : ses + * enregistrements, découpés sous « ; », réécrits sous « , » par l'écriture + * unique des CSV, puis encodés en windows-1252, sans marque. Chaque champ + * garde son texte ; un champ qui porte la virgule, un guillemet ou une fin + * de ligne s'écrit cité. + * + * @param {Uint8Array} octets le fichier écrit à la main, que decoder lit + * @returns {Uint8Array} + * @throws {Error} quand un guillemet reste ouvert à la fin du fichier + */ +export function varianteWindows1252(octets) { + const { enregistrements, guillemetOuvert } = decouper(decoder(octets).texte, SEPARATEUR); + if (guillemetOuvert) throw new Error(`${CAS_LIMITES} : un guillemet reste ouvert à la fin du fichier`); + // ecrireCsv écrit de l'UTF-8 avec marque, que le décodeur UTF-8 retire. + return enWindows1252(new TextDecoder().decode(ecrireCsv(enregistrements, VIRGULE))); +} + +/** + * Les sept livrables, calculés sans rien écrire : { chemin, octets, + * empreinte } ; le chemin est relatif à la racine, séparé par « / » ; + * l'empreinte, celle de la charge d'un fichier d'état, null pour un CSV. Dans + * l'ordre : le fichier d'état de chaque démonstration, dans l'ordre du + * catalogue ; l'export de la liste des participants de la petite, puis de la + * grande, chacun lu dans le texte du fichier d'état de sa démonstration ; la + * variante windows-1252 du CSV des cas limites. + * + * @param {{casLimites: Uint8Array, produitVersion?: string}} options + * casLimites : les octets du CSV des cas limites, écrit à la main + * @returns {Array<{chemin: string, octets: Uint8Array, empreinte: string|null}>} + */ +export function livrables({ casLimites, produitVersion = VERSION.affichee }) { + const etats = CATALOGUE.map((entree) => { + const texte = texteDeDemonstration(entree, { produitVersion }); + return { cle: entree.cle, texte, charge: analyser(texte).charge }; + }); + const fichiers = etats.map(({ cle, texte, charge }) => ({ + chemin: `${DOSSIER_LIVREES}/${cle}.gtt.json`, + octets: new TextEncoder().encode(texte), + empreinte: empreinte(charge), + })); + for (const cle of DEMONSTRATIONS_EN_CSV) { + const etat = etats.find((candidat) => candidat.cle === cle); + if (etat === undefined) throw new Error(`aucune démonstration ${cle} au catalogue`); + fichiers.push({ + chemin: csvDeDemonstration(cle), + octets: exporterParticipants(etat.charge.participants), + empreinte: null, + }); + } + fichiers.push({ chemin: CAS_LIMITES_CP1252, octets: varianteWindows1252(casLimites), empreinte: null }); + return fichiers; +} + +// Octets d'un fichier, ou null quand il n'existe pas. +function lireSiPresent(chemin) { + try { + return readFileSync(chemin); + } catch (erreur) { + if (erreur.code === 'ENOENT') return null; + throw erreur; + } +} + +/** + * Écrit les sept livrables sous destination, après les avoir tous calculés, + * et rien ailleurs. Le CSV des cas limites se lit sous source. Rend chaque + * livrable avec son état : « créé » quand le fichier n'existait pas, + * « inchangé » quand il portait déjà ces octets, « réécrit » sinon. + * + * @param {string} destination racine qui reçoit les fichiers + * @param {{source?: string, produitVersion?: string}} [options] source : la + * racine du projet par défaut + * @returns {Array<{chemin: string, octets: Uint8Array, empreinte: string|null, etat: string}>} + */ +export function ecrireLivrables(destination, { source = RACINE, produitVersion } = {}) { + const casLimites = readFileSync(join(source, ...CAS_LIMITES.split('/'))); + return livrables({ casLimites, produitVersion }).map((fichier) => { + const cible = join(destination, ...fichier.chemin.split('/')); + const avant = lireSiPresent(cible); + mkdirSync(dirname(cible), { recursive: true }); + writeFileSync(cible, fichier.octets); + const etat = avant === null ? 'créé' : Buffer.compare(avant, fichier.octets) === 0 ? 'inchangé' : 'réécrit'; + return { ...fichier, etat }; + }); +} + +// Ligne de commande : une destination au plus, la racine par défaut. Sort à +// 0 après l'écriture, à 1 quand un livrable ne se calcule ou ne s'écrit pas, +// à 2 sur un usage fautif. +function executer(arguments_) { + if (arguments_.length > 1 || arguments_.some((argument) => argument.startsWith('-'))) { + console.error(USAGE); + return 2; + } + const destination = arguments_.length === 1 ? resolve(arguments_[0]) : RACINE; + let ecrits; + try { + ecrits = ecrireLivrables(destination); + } catch (erreur) { + console.error(`Livrables : ${erreur.message}`); + return 1; + } + console.log(`Livrables écrits sous ${destination}, construction ${VERSION.affichee} :`); + for (const { chemin, octets, empreinte: hachage, etat } of ecrits) { + const suite = hachage === null ? '' : `, empreinte de la charge ${hachage}`; + console.log(` ${etat.padEnd(9)}${chemin} (${octets.length} octets${suite})`); + } + return 0; +} + +// Vrai quand ce fichier est le script que Node a lancé, et non un module +// importé. +function estLanceDirectement() { + if (process.argv[1] === undefined) return false; + try { + return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +} + +if (estLanceDirectement()) process.exitCode = executer(process.argv.slice(2)); diff --git a/scripts/engendrer_livrables.test.js b/scripts/engendrer_livrables.test.js new file mode 100644 index 0000000..2be9eaf --- /dev/null +++ b/scripts/engendrer_livrables.test.js @@ -0,0 +1,491 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuves du script qui engendre les fichiers livrés (§ 10.3, § 15.5) : la +// charge de chaque démonstration — la configuration du catalogue, +// l'événement demo:, la salle en grille, les animateurs au siège 1 —, +// le fichier d'état qu'il en écrit, la variante windows-1252 du CSV des cas +// limites, et la ligne de commande, qui écrit dans le dossier donné et nulle +// part ailleurs, et réécrit les mêmes octets quand on la relance. « Nulle +// part ailleurs » s'éprouve sur une copie du projet, photographiée avant et +// après chaque lancement : tout fichier écrit hors des sept livrables s'y +// voit, le CSV des cas limites, que le script lit sans l'écrire, comme les +// autres. Le contenu des fichiers livrés eux-mêmes s'éprouve à côté d'eux, +// dans src/demo/livrees.test.js et src/demo/livrees.long.test.js. +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { cpSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { exporterParticipants } from '../src/csv/export.js'; +import { decouper } from '../src/csv/lecture.js'; +import { CATALOGUE } from '../src/demo/catalogue.js'; +import { serialiser } from '../src/stockage/canonique.js'; +import { GENERATION_PAR_DEFAUT, analyser, configurationDepuisCharge } from '../src/stockage/document.js'; +import { VERSION } from '../src/version.genere.js'; +import { describe, test } from '../test/lanceur.js'; +import { versionVoisine } from '../test/version_voisine.js'; +import { + chargeDeDemonstration, + enWindows1252, + livrables, + texteDeDemonstration, + varianteWindows1252, +} from './engendrer_livrables.js'; + +const RACINE = fileURLToPath(new URL('..', import.meta.url)); +const SCRIPT = fileURLToPath(new URL('./engendrer_livrables.js', import.meta.url)); +const CAS_LIMITES = join(RACINE, 'exemples', 'participants_cas_limites.csv'); + +// Les sept livrables que le script écrit, chemins relatifs à la racine, dans +// l'ordre où il les écrit : un fichier d'état par démonstration du +// catalogue, les deux CSV de démonstration, puis la variante windows-1252 du +// CSV des cas limites (§ 10.3). +const LIVRABLES = [ + ...CATALOGUE.map(({ cle }) => `src/demo/livrees/${cle}.gtt.json`), + 'exemples/participants_demo_petite.csv', + 'exemples/participants_demo_grande.csv', + 'exemples/participants_cas_limites_cp1252.csv', +]; + +// Ce que le script lit, chemins relatifs à la racine du projet : package.json, +// qui déclare les modules ES, le script, le code de src/ qu'il importe et les +// CSV de exemples/, celui des cas limites compris, qu'il lit sans l'écrire. +const COPIES = ['package.json', 'scripts/engendrer_livrables.js', 'src', 'exemples']; + +// Date de modification, en secondes, de chaque fichier copié : un instant +// ancien. Toute écriture ultérieure la déplace, d'octets identiques comprise, +// quelle que soit la finesse de l'horloge du système de fichiers. +const DATE_ANCIENNE = 1_000_000_000; + +const MARQUE = '\u{FEFF}'; + +// Entrée du catalogue de clé donnée. +const entreeDe = (cle) => CATALOGUE.find((entree) => entree.cle === cle); + +// Entrée d'essai : la petite démonstration, sa configuration retouchée par +// retoucher, qui reçoit une copie et la rend. +const entreeRetouchee = (retoucher) => ({ + cle: 'essai', + nom: 'Essai', + construire: () => retoucher(entreeDe('petite').construire()), +}); + +const parTable = (a, b) => a.table - b.table; + +// Dossier temporaire neuf, passé à faire, puis effacé ; rend ce que rend +// faire. +function avecDossier(faire) { + const dossier = mkdtempSync(join(tmpdir(), 'livrables-')); + try { + return faire(dossier); + } finally { + rmSync(dossier, { recursive: true, force: true }); + } +} + +// Fichiers d'un dossier, à toute profondeur, en chemins relatifs séparés par +// « / », triés. +const fichiersSous = (dossier) => + readdirSync(dossier, { recursive: true, withFileTypes: true }) + .filter((entree) => entree.isFile()) + .map((entree) => relative(dossier, join(entree.parentPath, entree.name)).split(sep).join('/')) + .sort(); + +// Instantané d'un dossier : la taille et la date de modification, en +// nanosecondes, de chaque fichier à toute profondeur, sous son chemin relatif +// séparé par « / ». +const instantane = (dossier) => + new Map( + fichiersSous(dossier).map((chemin) => { + const { size, mtimeNs } = statSync(join(dossier, chemin), { bigint: true }); + return [chemin, `${size} octets, modifié à ${mtimeNs} ns`]; + }), + ); + +// Chemins que deux instantanés distinguent, triés : un fichier créé, retiré, +// ou dont la taille ou la date de modification change. Un fichier réécrit avec +// les mêmes octets en fait partie, sa date de modification avance. +const differences = (avant, apres) => + [...new Set([...avant.keys(), ...apres.keys()])].filter((chemin) => avant.get(chemin) !== apres.get(chemin)).sort(); + +// Lance le script de chemin donné depuis le répertoire cwd ; rend son statut +// et ses sorties. +const lancerScript = (script, cwd, ...arguments_) => + spawnSync(process.execPath, [script, ...arguments_], { cwd, encoding: 'utf8' }); + +// Lance le script du projet depuis le répertoire cwd. +const lancer = (cwd, ...arguments_) => lancerScript(SCRIPT, cwd, ...arguments_); + +// Dossier temporaire neuf qui porte, sous projet/, une copie de ce que le +// script lit, chaque fichier daté de DATE_ANCIENNE. Passe à faire ce dossier +// et lancerCopie, qui lance depuis lui le script copié ; efface le dossier ; +// rend ce que rend faire. Le script y est à la racine de son arbre, comme +// dans le projet : un fichier qu'il écrit hors de la destination tombe dans +// ce dossier, copie du projet ou répertoire courant, jamais dans le projet. +function avecProjetCopie(faire) { + return avecDossier((dossier) => { + for (const chemin of COPIES) { + cpSync(join(RACINE, chemin), join(dossier, 'projet', chemin), { recursive: true }); + } + for (const chemin of fichiersSous(dossier)) utimesSync(join(dossier, chemin), DATE_ANCIENNE, DATE_ANCIENNE); + const copie = join(dossier, 'projet', 'scripts', 'engendrer_livrables.js'); + return faire(dossier, (...arguments_) => lancerScript(copie, dossier, ...arguments_)); + }); +} + +const echapper = (texte) => texte.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +describe("engendrer_livrables : la charge d'une démonstration (§ 15)", () => { + test('porte la configuration du catalogue : participants, capacités, tours, réservations, contraintes', () => { + for (const entree of CATALOGUE) { + const configuration = entree.construire(); + const charge = chargeDeDemonstration(entree); + const lue = configurationDepuisCharge(charge); + assert.deepEqual( + { ...lue, reservations: [...lue.reservations].sort(parTable) }, + { + ...configuration, + participants: configuration.participants.map(({ id, nom, appartenance }) => ({ + id, + nom, + appartenance, + exclu: false, + })), + reservations: [...configuration.reservations].sort(parTable), + }, + entree.cle, + ); + assert.deepEqual( + charge.participants.map(({ prenom }) => prenom), + configuration.participants.map(({ prenom }) => prenom), + entree.cle, + ); + } + }); + + test("l'événement demo: : nom du catalogue, sans date, en brouillon, sans filiation, les tours et le défaut de sièges de la configuration", () => { + // Sièges par défaut des quatre démonstrations (§ 15.1, § 15.3). + const defauts = new Map([['grande', 8], ['grande-sans-exception', 8], ['petite', 3], ['petite-conflit', 3]]); + assert.deepEqual(CATALOGUE.map(({ cle }) => cle).sort(), [...defauts.keys()].sort()); + for (const entree of CATALOGUE) { + assert.deepEqual( + chargeDeDemonstration(entree).evenement, + { + id: `demo:${entree.cle}`, + nom: entree.nom, + date: null, + siegesParDefaut: defauts.get(entree.cle), + tours: entree.construire().tours, + unite: 'cm', + etat: 'brouillon', + filiation: null, + }, + entree.cle, + ); + } + }); + + test('les réglages : les contraintes de la configuration, une à une, les sièges non attribués, la génération par défaut', () => { + for (const entree of CATALOGUE) { + assert.deepEqual( + chargeDeDemonstration(entree).reglages, + { ...entree.construire().contraintes, attribuerSieges: false, generation: { ...GENERATION_PAR_DEFAUT } }, + entree.cle, + ); + } + const contraintes = { + separerAppartenances: false, + nouveauxVoisins: true, + nouvelleTable: false, + varierAppartenances: true, + }; + const entree = entreeRetouchee((configuration) => ({ ...configuration, contraintes })); + assert.deepEqual(chargeDeDemonstration(entree).reglages, { + ...contraintes, + attribuerSieges: false, + generation: { ...GENERATION_PAR_DEFAUT }, + }); + }); + + test('les tables : rondes, en grille rangée par rangée, centres à 300 cm, la dernière rangée seule incomplète', () => { + assert.deepEqual(chargeDeDemonstration(entreeDe('petite')).tables, [ + { id: 1, numero: 1, sieges: null, forme: 'ronde', position: { x: 0, y: 0 } }, + { id: 2, numero: 2, sieges: null, forme: 'ronde', position: { x: 300, y: 0 } }, + { id: 3, numero: 3, sieges: null, forme: 'ronde', position: { x: 0, y: 300 } }, + { id: 4, numero: 4, sieges: null, forme: 'ronde', position: { x: 300, y: 300 } }, + ]); + // 33 tables : six colonnes, cinq rangées pleines, puis trois tables. + const { tables } = chargeDeDemonstration(entreeDe('grande')); + const positions = tables.map(({ position: { x, y } }) => [x, y]); + assert.deepEqual(positions.slice(0, 7), [[0, 0], [300, 0], [600, 0], [900, 0], [1200, 0], [1500, 0], [0, 300]]); + assert.deepEqual(positions.slice(-4), [[1500, 1200], [0, 1500], [300, 1500], [600, 1500]]); + assert.equal(new Set(positions.map(String)).size, 33); + assert.ok(tables.every(({ forme }) => forme === 'ronde')); + assert.deepEqual(tables.map(({ id, numero }) => [id, numero]), positions.map((_, rang) => [rang + 1, rang + 1])); + }); + + test("les sièges : le défaut est la capacité la plus fréquente, la plus grande à égalité ; toute autre capacité surcharge sa table (§ 6.1)", () => { + const surcharges = (charge) => + charge.tables.filter(({ sieges }) => sieges !== null).map(({ numero, sieges }) => [numero, sieges]); + assert.deepEqual(surcharges(chargeDeDemonstration(entreeDe('grande'))), [[30, 7], [31, 7], [32, 7], [33, 7]]); + for (const cle of ['grande-sans-exception', 'petite', 'petite-conflit']) { + assert.deepEqual(surcharges(chargeDeDemonstration(entreeDe(cle))), [], cle); + } + const capacites = [6, 9, 6, 9]; + const egalite = chargeDeDemonstration( + entreeRetouchee((configuration) => ({ + ...configuration, + tables: configuration.tables.map((table, rang) => ({ ...table, capacite: capacites[rang] })), + })), + ); + assert.equal(egalite.evenement.siegesParDefaut, 9); + assert.deepEqual(surcharges(egalite), [[1, 6], [3, 6]]); + }); + + test("grandes : chaque animateur réservé au siège 1 de sa table pour tous les tours, ce siège titré « animation », le titre pressenti porté par lui seul ; petites : ni réservation ni titre", () => { + for (const cle of ['grande', 'grande-sans-exception']) { + const entree = entreeDe(cle); + const animateurs = entree.construire().reservations; + const charge = chargeDeDemonstration(entree); + assert.equal(animateurs.length, 33, cle); + assert.deepEqual( + [...charge.reservations].sort(parTable), + animateurs + .map(({ participant, table }) => ({ participant, table, siege: 1, portee: 'tous', tour: null })) + .sort(parTable), + cle, + ); + assert.deepEqual( + [...charge.titres].sort(parTable), + charge.tables.map(({ id }) => ({ table: id, siege: 1, libelle: 'animation' })), + cle, + ); + assert.deepEqual( + charge.participants + .filter(({ titrePressenti }) => titrePressenti !== null) + .map(({ id, titrePressenti }) => [id, titrePressenti]), + animateurs.map(({ participant }) => [participant, 'animation']).sort((a, b) => a[0] - b[0]), + cle, + ); + } + for (const cle of ['petite', 'petite-conflit']) { + const charge = chargeDeDemonstration(entreeDe(cle)); + assert.deepEqual([charge.reservations, charge.titres], [[], []], cle); + assert.ok(charge.participants.every(({ titrePressenti }) => titrePressenti === null), cle); + } + }); + + test("ni proposition ni retenu, personne d'exclu, ni courriel ni notes ; les compteurs dépassent chaque identifiant", () => { + for (const entree of CATALOGUE) { + const charge = chargeDeDemonstration(entree); + assert.deepEqual([charge.propositions, charge.retenu], [[], null], entree.cle); + assert.ok( + charge.participants.every(({ exclu, courriel, notes }) => exclu === false && courriel === null && notes === null), + entree.cle, + ); + const plusGrand = (liste) => Math.max(...liste.map(({ id }) => id)); + assert.deepEqual( + charge.prochainsIds, + { participant: plusGrand(charge.participants) + 1, table: plusGrand(charge.tables) + 1, proposition: 1 }, + entree.cle, + ); + } + }); + + test("refuse une configuration qu'il ne sait pas écrire : deux réservations à une table, une réservation d'un seul tour", () => { + const avecReservations = (reservations) => + entreeRetouchee((configuration) => ({ ...configuration, reservations })); + assert.throws( + () => + chargeDeDemonstration( + avecReservations([ + { participant: 1, table: 2, portee: 'tous' }, + { participant: 5, table: 2, portee: 'tous' }, + ]), + ), + /essai.*table 2/, + ); + assert.throws( + () => chargeDeDemonstration(avecReservations([{ participant: 1, table: 3, portee: 'tour', tour: 2 }])), + /essai.*table 3/, + ); + }); +}); + +describe("engendrer_livrables : le fichier d'état écrit (§ 8.8, § 18.6)", () => { + test('révision 1, la version de la construction qui écrit, la forme canonique ; UTF-8 sans marque, des LF seules, une fin de ligne finale', () => { + for (const entree of CATALOGUE) { + const texte = texteDeDemonstration(entree); + const lu = analyser(texte); + assert.deepEqual( + [lu.entete.format, lu.entete.produitVersion, lu.entete.revision], + [1, VERSION.affichee, 1], + entree.cle, + ); + assert.equal(serialiser(lu.charge, lu.entete), texte, entree.cle); + assert.ok(!texte.startsWith(MARQUE) && !texte.includes('\r'), entree.cle); + assert.ok(texte.endsWith('}\n') && !texte.endsWith('\n\n'), entree.cle); + } + }); + + test("une autre construction n'écrit qu'un autre en-tête : la charge est la même", () => { + const voisine = versionVoisine(VERSION.affichee); + for (const entree of CATALOGUE) { + const ici = analyser(texteDeDemonstration(entree)); + const ailleurs = analyser(texteDeDemonstration(entree, { produitVersion: voisine })); + assert.equal(ailleurs.entete.produitVersion, voisine, entree.cle); + assert.deepEqual(ailleurs.charge, ici.charge, entree.cle); + } + }); +}); + +describe('engendrer_livrables : la variante windows-1252 (§ 10.3)', () => { + test('enWindows1252 : un octet par caractère, les signes propres à windows-1252 compris', () => { + // Octets de la table publiée de windows-1252. + assert.deepEqual( + [...enWindows1252('aé€\u{2019}œŸ\u{A0}ÿ')], + [0x61, 0xe9, 0x80, 0x92, 0x9c, 0x9f, 0xa0, 0xff], + ); + const decodeur = new TextDecoder('windows-1252'); + for (let octet = 0; octet < 256; octet += 1) { + assert.deepEqual([...enWindows1252(decodeur.decode(Uint8Array.of(octet)))], [octet], `octet ${octet}`); + } + }); + + test('enWindows1252 refuse un caractère que windows-1252 ne porte pas, et nomme son point de code et son rang', () => { + assert.throws( + () => enWindows1252('noté ✓'), + (erreur) => erreur instanceof RangeError && /U\+2713/.test(erreur.message) && /rang 5/.test(erreur.message), + ); + assert.throws(() => enWindows1252('\u{1D11E}'), /U\+1D11E/); + }); + + test('varianteWindows1252 : les mêmes enregistrements, séparés par des virgules, en windows-1252 sans marque', () => { + const source = [ + 'Nom;Prénom;Notes', + 'Lanterneau;Basile;"chœur; l\u{2019}Anse, nord"', + 'Coudraie;Apolline;"deux\nlignes"', + 'Brunœil;;sans prénom', + ]; + const octets = new TextEncoder().encode(`${MARQUE}${source.join('\r\n')}\r\n`); + const variante = varianteWindows1252(octets); + assert.equal( + new TextDecoder('windows-1252').decode(variante), + [ + 'Nom,Prénom,Notes', + 'Lanterneau,Basile,"chœur; l\u{2019}Anse, nord"', + 'Coudraie,Apolline,"deux\nlignes"', + 'Brunœil,,sans prénom', + '', + ].join('\r\n'), + ); + assert.deepEqual([...variante.subarray(0, 3)], [...enWindows1252('Nom')]); + assert.deepEqual( + decouper(new TextDecoder('windows-1252').decode(variante), ',').enregistrements, + decouper(new TextDecoder().decode(octets), ';').enregistrements, + ); + }); + + test('varianteWindows1252 refuse un fichier dont un guillemet reste ouvert', () => { + const octets = new TextEncoder().encode(`${MARQUE}Nom;Notes\r\nLanterneau;"sans fin\r\n`); + assert.throws(() => varianteWindows1252(octets), /guillemet/); + }); +}); + +describe('engendrer_livrables : les livrables (§ 10.3, § 15.5)', () => { + test("les sept fichiers, dans l'ordre ; les CSV de démonstration exportent les fichiers d'état écrits, la variante dérive des cas limites", () => { + const casLimites = readFileSync(CAS_LIMITES); + const fichiers = livrables({ casLimites }); + assert.deepEqual( + fichiers.map(({ chemin }) => chemin), + LIVRABLES, + ); + const octetsDe = new Map(fichiers.map(({ chemin, octets }) => [chemin, octets])); + for (const entree of CATALOGUE) { + assert.equal( + new TextDecoder().decode(octetsDe.get(`src/demo/livrees/${entree.cle}.gtt.json`)), + texteDeDemonstration(entree), + entree.cle, + ); + } + for (const cle of ['petite', 'grande']) { + const etat = new TextDecoder().decode(octetsDe.get(`src/demo/livrees/${cle}.gtt.json`)); + assert.deepEqual( + octetsDe.get(`exemples/participants_demo_${cle}.csv`), + exporterParticipants(analyser(etat).charge.participants), + cle, + ); + } + assert.deepEqual(octetsDe.get('exemples/participants_cas_limites_cp1252.csv'), varianteWindows1252(casLimites)); + }); +}); + +describe('engendrer_livrables : la ligne de commande', () => { + test('écrit les sept livrables sous le dossier donné, relatif au répertoire courant, et rien ailleurs, au premier lancement comme au suivant', () => + avecProjetCopie((dossier, lancerCopie) => { + // Le dossier entier, copie du projet comprise, est photographié avant et + // après chaque lancement : seuls les sept livrables, sous sortie/, y + // apparaissent. Le CSV des cas limites écrit à la main, tout autre + // fichier de la copie et le répertoire courant doivent y rester tels + // quels. + const sept = LIVRABLES.map((chemin) => `sortie/${chemin}`).sort(); + const avant = instantane(dossier); + assert.ok(avant.has('projet/exemples/participants_cas_limites.csv'), 'la copie porte le CSV des cas limites'); + const premier = lancerCopie('sortie'); + assert.equal(premier.status, 0, premier.stderr); + const apres = instantane(dossier); + assert.deepEqual(differences(avant, apres), sept, "le premier lancement n'écrit pas exactement les sept livrables"); + const attendus = livrables({ casLimites: readFileSync(CAS_LIMITES) }); + for (const { chemin, octets } of attendus) { + assert.deepEqual(new Uint8Array(readFileSync(join(dossier, 'sortie', chemin))), octets, chemin); + assert.match(premier.stdout, new RegExp(`^ *créé +${echapper(chemin)} `, 'm'), chemin); + } + // Relancé, le script ne touche rien hors des sept livrables, qu'il + // réécrit sans en changer un octet : leurs dates peuvent avancer, leurs + // octets non. + const second = lancerCopie('sortie'); + assert.equal(second.status, 0, second.stderr); + assert.deepEqual( + differences(apres, instantane(dossier)).filter((chemin) => !sept.includes(chemin)), + [], + 'le lancement suivant écrit autre chose que les sept livrables', + ); + for (const { chemin, octets } of attendus) { + assert.deepEqual(new Uint8Array(readFileSync(join(dossier, 'sortie', chemin))), octets, chemin); + } + })); + + test('relancé, il réécrit les mêmes octets, et le dit', () => + avecDossier((dossier) => { + const premier = lancer(dossier, '.'); + assert.equal(premier.status, 0, premier.stderr); + const octets = LIVRABLES.map((chemin) => readFileSync(join(dossier, chemin))); + const second = lancer(dossier, dossier); + assert.equal(second.status, 0, second.stderr); + LIVRABLES.forEach((chemin, rang) => { + assert.deepEqual(readFileSync(join(dossier, chemin)), octets[rang], chemin); + assert.match(second.stdout, new RegExp(`^ *inchangé +${echapper(chemin)} `, 'm'), chemin); + }); + })); + + test("une destination qui ne s'écrit pas sort en 1 en nommant la cause", () => + avecDossier((dossier) => { + writeFileSync(join(dossier, 'occupe'), 'un fichier, pas un dossier\n'); + const resultat = lancer(dossier, 'occupe'); + assert.equal(resultat.status, 1, resultat.stdout); + assert.match(resultat.stderr, /^Livrables : .*occupe/m); + assert.deepEqual(fichiersSous(dossier), ['occupe']); + })); + + test("un argument de trop, ou une option, sort en 2 en rappelant l'usage, sans rien écrire", () => + avecDossier((dossier) => { + for (const arguments_ of [['a', 'b'], ['--aide']]) { + const resultat = lancer(dossier, ...arguments_); + assert.equal(resultat.status, 2, arguments_.join(' ')); + assert.match(resultat.stderr, /usage : node scripts\/engendrer_livrables\.js \[destination\]/); + } + assert.deepEqual(fichiersSous(dossier), []); + })); +}); diff --git a/scripts/version.js b/scripts/version.js index 1a12801..97ca63c 100644 --- a/scripts/version.js +++ b/scripts/version.js @@ -299,7 +299,14 @@ const LIGNES_OBLIGATOIRES = [ // contrôlent ; // - les documents (*.md), dont le point 7 contrôle les noms cités ; // - les épreuves de la dérivation, qui citent des versions par nécessité ; -// - les données d'épreuve, sous test/fixtures/. +// - les données d'épreuve, sous test/fixtures/ ; +// - les démonstrations livrées, sous src/demo/livrees/, données engendrées +// dont l'en-tête porte la version de la construction qui les a écrites +// (§ 18.5, § 18.6) : une livraison suivante ne les réécrit pas (§ 15.5, +// point 5). +// Un répertoire s'exclut par son chemin exact, et rien n'est lu sous lui ; +// un fichier dont le nom commence comme le sien, src/demo/livrees.js, reste +// balayé. // package.json reste balayé : la forme technique y est à sa place, la forme // affichée non. const ARBRES_BALAYES = ['src', 'scripts', 'electron', 'test']; @@ -310,7 +317,7 @@ const FICHIERS_EXCLUS = new Set([ 'CHANGELOG.md', 'scripts/version.test.js', ]); -const REPERTOIRES_EXCLUS = new Set(['test/fixtures']); +const REPERTOIRES_EXCLUS = new Set(['test/fixtures', 'src/demo/livrees']); const DOCUMENT = /\.md$/i; const ARBRE_DES_SOURCES = 'src/'; diff --git a/scripts/version.test.js b/scripts/version.test.js index 1939bf2..0a8afde 100644 --- a/scripts/version.test.js +++ b/scripts/version.test.js @@ -629,12 +629,21 @@ describe('version : contrôle du § 18.5, un défaut à la fois', () => { assert.deepEqual(controler(racine, { dateCivile: DATE }), []); })); + test('point 6 : seul le répertoire des démonstrations livrées est exclu, pas un fichier qui porte son nom', () => + surArbre((racine) => { + ecrire(racine, 'src/demo/livrees.js', "export const v = '2026.10.05.01';\n"); + ecrire(racine, 'src/demo/livrees/sous/grande.gtt.json', '{"produitVersion":"2026.10.05.01"}\n'); + const echecs = exigerEchecs(racine, [6], [/^6\. src\/demo\/livrees\.js, ligne 1 : « 2026\.10\.05\.01 », forme affichée/]); + assert.equal(echecs.length, 1, echecs.join('\n')); + })); + test('point 6 : données, guides, dépendances et sorties restent hors du périmètre', () => surArbre((racine) => { const versions = `${VERSION} ${TECHNIQUE} ${TECHNIQUE}.0\n`; for (const chemin of [ 'scripts/version.test.js', 'test/fixtures/demonstration.json', + 'src/demo/livrees/grande.gtt.json', 'GUIDE-USAGE.md', 'docs/notes.md', 'node_modules/paquet/index.js', diff --git a/src/demo/catalogue.long.test.js b/src/demo/catalogue.long.test.js index 62bda5f..165470b 100644 --- a/src/demo/catalogue.long.test.js +++ b/src/demo/catalogue.long.test.js @@ -3,8 +3,12 @@ // Garde des noms forgés (§ 14.9, § 15.6) : aucun patronyme forgé complet, nom // long compris, ni aucune racine d'organisation n'apparaît en mot entier dans -// un fichier texte du projet hors de src/demo/noms.js, qui les définit. Les -// données d'épreuve de test/fixtures/ sont balayées comme le reste. +// un fichier texte du projet hors de src/demo/noms.js, qui les définit, et +// des fichiers que scripts/engendrer_livrables.js engendre et qui les portent +// par construction : les démonstrations livrées, sous src/demo/livrees/, et +// les deux CSV de démonstration, sous exemples/. Le CSV des cas limites, +// écrit à la main, sa variante windows-1252 et les données d'épreuve de +// test/fixtures/ sont balayés comme le reste. // // Un mot entier est bordé, de chaque côté, par autre chose qu'une lettre, une // marque combinante ou un chiffre : le trait de soulignement et le trait @@ -16,11 +20,13 @@ // Chaque fichier de l'arbre exerce une règle : une sonde de test/fixtures/ // porte chaque nom cherché ; d'autres placent un nom au-delà du premier Mio // du texte, en NFD, après un octet nul, dans une entrée cachée, dans un -// noms.js qui n'est pas celui des listes, ou sous un répertoire exclu ; trois -// derniers se lisent malgré un nom voisin d'une exclusion : src/distances/, -// distinctes.js et un fichier .git. L'épreuve affirme les fichiers lus, le -// compte exact des trouvailles, et que la garde de l'épreuve du projet -// désigne exactement les fichiers placés sous un répertoire exclu. +// noms.js qui n'est pas celui des listes, dans un fichier engendré, ou sous +// un répertoire exclu ; les derniers se lisent malgré un nom voisin d'une +// exclusion : src/distances/, distinctes.js, un fichier .git, un module +// src/demo/livrees.js, le CSV des cas limites et sa variante. L'épreuve +// affirme les fichiers lus, le compte exact des trouvailles, et que la garde +// de l'épreuve du projet désigne exactement les fichiers placés sous un +// répertoire exclu. // // L'épreuve établit l'absence dans ce projet, rien sur le monde réel : aucune // vérification locale ne prouve qu'un nom forgé ne désigne nulle part une @@ -28,7 +34,7 @@ import assert from 'node:assert/strict'; import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { dirname, join, relative } from 'node:path'; +import { dirname, join, relative, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, test } from '../../test/lanceur.js'; import { NOMS_LONGS, RACINES, patronymes } from './noms.js'; @@ -36,6 +42,20 @@ import { NOMS_LONGS, RACINES, patronymes } from './noms.js'; const RACINE = fileURLToPath(new URL('../..', import.meta.url)); const LISTES = join('src', 'demo', 'noms.js'); +// Fichiers qui portent les noms forgés par construction, que +// scripts/engendrer_livrables.js écrit (§ 10.3, § 15.6) : tout fichier sous +// src/demo/livrees/, et les deux CSV de démonstration. +const DEMONSTRATIONS_LIVREES = join('src', 'demo', 'livrees') + sep; +const CSV_DE_DEMONSTRATION = new Set([ + join('exemples', 'participants_demo_petite.csv'), + join('exemples', 'participants_demo_grande.csv'), +]); + +// Vrai pour les listes du générateur et pour un fichier que +// scripts/engendrer_livrables.js en engendre. +const porteLesNomsParConstruction = (chemin) => + chemin === LISTES || chemin.startsWith(DEMONSTRATIONS_LIVREES) || CSV_DE_DEMONSTRATION.has(chemin); + // Répertoires hors balayage, à toute profondeur : .git, les dépendances, les // caches des outils, et les sorties de construction, qui portent une copie // des listes mises en paquet (www, dist et chaque dist-…, plateformes de @@ -87,17 +107,18 @@ function motifDes(noms) { // Occurrences du motif dans un texte, normalisé en NFC. const occurrences = (motif, texte) => [...texte.normalize('NFC').matchAll(motif)].map(([mot]) => mot); -// Balaie l'arbre de racine : chaque fichier texte hors des répertoires exclus -// et des listes du générateur se lit en entier, décodé en UTF-8, et chaque -// nom cherché s'y relève. Rend les chemins examinés, relatifs à racine et -// triés, et les trouvailles { chemin, mot }, fichier après fichier, dans -// l'ordre du texte. +// Balaie l'arbre de racine : chaque fichier texte hors des répertoires +// exclus, des listes du générateur et des fichiers que +// scripts/engendrer_livrables.js en engendre se lit en entier, décodé en +// UTF-8, et chaque nom cherché s'y relève. Rend les chemins examinés, +// relatifs à racine et triés, et les trouvailles { chemin, mot }, fichier +// après fichier, dans l'ordre du texte. function balayer(racine) { const motif = motifDes(nomsCherches()); const examines = []; const trouvailles = []; for (const chemin of fichiers(racine)) { - if (chemin === LISTES) continue; + if (porteLesNomsParConstruction(chemin)) continue; const octets = readFileSync(join(racine, chemin)); if (estBinaire(octets)) continue; examines.push(chemin); @@ -178,12 +199,22 @@ describe('noms forgés : balayage du projet (§ 15.6)', () => { [join('src', 'distances', 'a.js'), patronyme, [patronyme]], [join('src', 'distinctes.js'), 'paires distinctes\n', []], [join('sous', '.git'), 'gitdir: ../.git/worktrees/sous\n', []], + // Les voisins des fichiers engendrés se lisent : un module dont le nom + // commence comme leur répertoire, le CSV des cas limites, écrit à la + // main, et sa variante windows-1252, que le script dérive de lui. + [join('src', 'demo', 'livrees.js'), patronyme, [patronyme]], + [join('exemples', 'participants_cas_limites.csv'), patronyme, [patronyme]], + [join('exemples', 'participants_cas_limites_cp1252.csv'), patronyme, [patronyme]], ]; // [chemin, contenu] : les fichiers que le balayage saute — un octet nul - // dans les 8000 premiers, les listes du générateur. + // dans les 8000 premiers, les listes du générateur, les fichiers que le + // script en engendre. const sautes = [ ['nul-dedans.dat', nulA(7999, patronyme)], [LISTES, patronyme], + [join('src', 'demo', 'livrees', 'grande.gtt.json'), patronyme], + [join('exemples', 'participants_demo_petite.csv'), patronyme], + [join('exemples', 'participants_demo_grande.csv'), patronyme], ]; // Un fichier sous chaque répertoire exclu, à la racine et plus profond ; // chacun porte un nom forgé. @@ -224,7 +255,7 @@ describe('noms forgés : balayage du projet (§ 15.6)', () => { assert.deepEqual(arbre.map(([chemin]) => chemin).filter(sousUnRepertoireExclu), exclus); }); - test('aucun patronyme forgé ni aucune racine hors de src/demo/noms.js', () => { + test('aucun patronyme forgé ni aucune racine hors de src/demo/noms.js et des fichiers engendrés', () => { assert.ok(nomsCherches().length > 0, 'aucun nom forgé à chercher'); const { examines, trouvailles } = balayer(RACINE); assert.ok(examines.length > 0, 'balayage vide'); diff --git a/src/demo/livrees.js b/src/demo/livrees.js new file mode 100644 index 0000000..32d3cf1 --- /dev/null +++ b/src/demo/livrees.js @@ -0,0 +1,57 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Les démonstrations livrées (§ 15, § 15.5) : le texte du fichier d'état de +// chaque démonstration du catalogue, tel que scripts/engendrer_livrables.js +// l'a écrit sous livrees/. Le fichier livré est la source de vérité (§ 15.5, +// point 5) : montrer une démonstration lit ce fichier, sans rejouer le +// générateur. Charger une démonstration crée un événement neuf depuis ce +// texte (§ 15) ; le texte, une chaîne, ne se modifie pas, et la +// démonstration d'origine non plus. +// +// Chaque fichier s'importe en module JSON, syntaxe du langage que Node et +// Vite lisent l'un comme l'autre : le module se charge sous Node seul, ce +// qu'exige le repli « node --test » du § 14.8, et un fichier absent casse +// la construction. Un module JSON rend le document, non ses octets : le +// sérialiseur canonique le réécrit sous son propre en-tête. Chaque fichier +// livré est en forme canonique, UTF-8 sans marque, fin de ligne LF ; le +// texte rendu est donc le sien, octet pour octet, ce que livrees.test.js +// exige sous Vitest comme sous Node seul. +// +// Chaque texte décrit l'événement demo:, à la révision 1, en brouillon, +// sans proposition. +import { serialiser } from '../stockage/canonique.js'; +import grande from './livrees/grande.gtt.json' with { type: 'json' }; +import grandeSansException from './livrees/grande-sans-exception.gtt.json' with { type: 'json' }; +import petite from './livrees/petite.gtt.json' with { type: 'json' }; +import petiteConflit from './livrees/petite-conflit.gtt.json' with { type: 'json' }; + +// Texte canonique d'un fichier d'état lu en module JSON : sa charge, sous la +// révision et la version de son en-tête ; format et comptes se recalculent. +const texteDe = ({ entete, charge }) => serialiser(charge, entete); + +/** + * Les quatre démonstrations livrées, dans l'ordre du catalogue + * (catalogue.js) : { cle, texte }, figées ; cle est celle de l'entrée du + * catalogue, texte le fichier d'état livré. + * @type {ReadonlyArray>} + */ +export const LIVREES = Object.freeze([ + Object.freeze({ cle: 'grande', texte: texteDe(grande) }), + Object.freeze({ cle: 'grande-sans-exception', texte: texteDe(grandeSansException) }), + Object.freeze({ cle: 'petite', texte: texteDe(petite) }), + Object.freeze({ cle: 'petite-conflit', texte: texteDe(petiteConflit) }), +]); + +/** + * Texte livré de la démonstration de clé donnée. + * + * @param {string} cle la clé d'une entrée du catalogue + * @returns {string} + * @throws {RangeError} pour une clé qu'aucune démonstration livrée ne porte + */ +export function texteLivre(cle) { + const livree = LIVREES.find((candidate) => candidate.cle === cle); + if (livree === undefined) throw new RangeError(`aucune démonstration livrée de clé ${JSON.stringify(cle)}`); + return livree.texte; +} diff --git a/src/demo/livrees.long.test.js b/src/demo/livrees.long.test.js new file mode 100644 index 0000000..20f366c --- /dev/null +++ b/src/demo/livrees.long.test.js @@ -0,0 +1,166 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuves longues des démonstrations livrées (§ 14.4) : l'empreinte de la +// charge de chaque fichier livré contre celle que rejoue le script (§ 15.5, +// point 5), le profil d'appartenances lu dans le fichier (§ 15.2), et +// l'aller-retour sur chacun, grande démonstration comprise (§ 8.9). Le +// fichier livré est la source de vérité : un écart d'empreinte se nomme par +// les chemins de la charge qui diffèrent, et se résout en engendrant de +// nouveau, geste conscient, jamais en corrigeant l'épreuve. +import assert from 'node:assert/strict'; +import { empreinte, texteDeDemonstration } from '../../scripts/engendrer_livrables.js'; +import { describe, test } from '../../test/lanceur.js'; +import { versionVoisine } from '../../test/version_voisine.js'; +import { serialiser } from '../stockage/canonique.js'; +import { difference } from '../stockage/correctifs.js'; +import { analyser } from '../stockage/document.js'; +import { examiner } from '../stockage/placements.js'; +import { VERSION } from '../version.genere.js'; +import { CATALOGUE } from './catalogue.js'; +import { LIVREES, texteLivre } from './livrees.js'; + +const EMPREINTE = /^[0-9a-f]{64}$/; + +// Chemin d'une opération de correctif, lisible : les clés jointes par des +// points, un rang entre crochets, un enregistrement par [id=n]. +const cheminLisible = (chemin) => + chemin + .map((etape) => { + if (typeof etape === 'string') return `.${etape}`; + return typeof etape === 'number' ? `[${etape}]` : `[id=${etape.id}]`; + }) + .join('') + .replace(/^\./, ''); + +// Affirme que la charge rejouée a l'empreinte de la charge livrée ; à +// l'échec, nomme chaque chemin où elles diffèrent, tel que le correctif de +// l'une à l'autre le pose ou le retire (§ 15.5, point 5). +function memeEmpreinte(livree, rejouee, cle) { + if (empreinte(rejouee) === empreinte(livree)) return; + const chemins = difference(livree, rejouee).map(({ op, chemin }) => `${op} ${cheminLisible(chemin)}`); + assert.fail( + `${cle} : la charge que rejoue le script diffère du fichier livré : ${chemins.join(' ; ')}. ` + + 'Engendrer de nouveau est un geste conscient : node scripts/engendrer_livrables.js', + ); +} + +// Profil d'appartenances d'une liste de participants (§ 15.2) : pour chaque +// taille de groupe, le nombre de groupes de cette taille, tailles +// croissantes ; les personnes sans appartenance ne forment pas de groupe. +function profil(participants) { + const tailles = new Map(); + for (const { appartenance } of participants) { + if (appartenance !== null) tailles.set(appartenance, (tailles.get(appartenance) ?? 0) + 1); + } + const groupesParTaille = new Map(); + for (const taille of tailles.values()) groupesParTaille.set(taille, (groupesParTaille.get(taille) ?? 0) + 1); + return [...groupesParTaille].sort((a, b) => a[0] - b[0]); +} + +// Les fichiers livrés, tous ; lève quand il n'y en a aucun (§ 14.2). +function balayer() { + assert.ok(LIVREES.length > 0, 'aucun fichier livré examiné'); + return LIVREES; +} + +// Aller-retour d'un fichier livré (§ 8.9) : charger — analyser, puis +// contrôler les placements, qui n'écartent ni ne signalent rien —, +// enregistrer sous une autre construction et à la révision suivante, +// recharger. Rend la charge chargée, la charge rechargée, et le texte que +// l'enregistrement écrirait sous l'en-tête lu. +function allerRetour(texte) { + const lu = analyser(texte); + const { charge, fautives, derives } = examiner(lu.charge); + assert.deepEqual([fautives, derives], [[], []]); + const enregistre = serialiser(charge, { + revision: lu.entete.revision + 1, + produitVersion: versionVoisine(VERSION.affichee), + }); + const relu = analyser(enregistre); + assert.notEqual(relu.entete.revision, lu.entete.revision); + return { charge, relue: relu.charge, sousLEnteteLu: serialiser(charge, lu.entete) }; +} + +describe('démonstrations livrées : empreinte (§ 15.5, point 5)', () => { + test("pour chaque entrée du catalogue, la charge que rejoue le script a l'empreinte du fichier livré", () => { + assert.ok(CATALOGUE.length > 0, 'catalogue vide'); + for (const entree of CATALOGUE) { + const livree = analyser(texteLivre(entree.cle)).charge; + const rejouee = analyser(texteDeDemonstration(entree)).charge; + memeEmpreinte(livree, rejouee, entree.cle); + } + }); + + test("un changement de produitVersion seul ne change pas l'empreinte : elle porte sur la charge, l'en-tête exclu", () => { + const voisine = versionVoisine(VERSION.affichee); + for (const entree of CATALOGUE) { + const texteIci = texteDeDemonstration(entree); + const texteAilleurs = texteDeDemonstration(entree, { produitVersion: voisine }); + assert.notEqual(texteAilleurs, texteIci, entree.cle); + const ailleurs = analyser(texteAilleurs); + assert.equal(ailleurs.entete.produitVersion, voisine, entree.cle); + assert.equal(empreinte(ailleurs.charge), empreinte(analyser(texteIci).charge), entree.cle); + memeEmpreinte(analyser(texteLivre(entree.cle)).charge, ailleurs.charge, entree.cle); + } + }); + + test("l'empreinte : 64 chiffres hexadécimaux, une par charge — les quatre démonstrations en ont quatre", () => { + const empreintes = balayer().map(({ texte }) => empreinte(analyser(texte).charge)); + assert.ok(empreintes.every((valeur) => EMPREINTE.test(valeur)), empreintes.join(' ')); + assert.equal(new Set(empreintes).size, empreintes.length); + }); + + test("l'échec nomme les chemins qui diffèrent, jamais seulement deux empreintes", () => { + const livree = analyser(texteLivre('petite')).charge; + const rejouee = structuredClone(livree); + rejouee.participants.find(({ id }) => id === 7).nom = 'Lanterneau'; + rejouee.tables.find(({ id }) => id === 3).position.x += 1; + assert.notEqual(empreinte(rejouee), empreinte(livree)); + assert.throws( + () => memeEmpreinte(livree, rejouee, 'petite'), + (erreur) => { + assert.match(erreur.message, /^petite : /); + assert.match(erreur.message, /poser participants\[id=7\]\.nom ; poser tables\[id=3\]\.position\.x\./); + assert.doesNotMatch(erreur.message, /[0-9a-f]{64}/); + return true; + }, + ); + }); +}); + +describe("démonstrations livrées : profil d'appartenances (§ 15.2)", () => { + test("les tailles d'appartenance lues dans chaque fichier livré égalent celles du générateur", () => { + for (const entree of CATALOGUE) { + const attendu = profil(analyser(texteLivre(entree.cle)).charge.participants); + assert.ok(attendu.length > 0, entree.cle); + assert.deepEqual(profil(entree.construire().participants), attendu, entree.cle); + } + }); + + test('le profil compte les groupes par taille, sans les personnes sans appartenance', () => { + const personne = (id, appartenance) => ({ id, appartenance }); + assert.deepEqual( + profil([ + personne(1, 'Chœur de l\u{2019}Anse'), + personne(2, 'Cercle des Saules'), + personne(3, 'Chœur de l\u{2019}Anse'), + personne(4, null), + personne(5, 'Société des Hérons'), + ]), + [[1, 2], [2, 1]], + ); + }); +}); + +describe('démonstrations livrées : aller-retour sur chacune, grande comprise (§ 8.9)', () => { + test("charger, enregistrer, recharger rend la même charge ; le texte livré est celui qu'écrit le sérialiseur", () => { + const examines = balayer(); + for (const { cle, texte } of examines) { + const { charge, relue, sousLEnteteLu } = allerRetour(texte); + assert.deepEqual(relue, charge, cle); + assert.equal(sousLEnteteLu, texte, cle); + } + assert.ok(examines.some(({ cle }) => cle === 'grande'), 'la grande démonstration non examinée'); + }); +}); diff --git a/src/demo/livrees.test.js b/src/demo/livrees.test.js new file mode 100644 index 0000000..28b8d2d --- /dev/null +++ b/src/demo/livrees.test.js @@ -0,0 +1,233 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuves des démonstrations livrées et des CSV d'exemple, série surveillée +// (§ 8.9, § 10.3, § 14.4, § 14.8, § 15) : un fichier par entrée du +// catalogue, que LIVREES porte octet pour octet, sous Vitest comme sous Node +// seul ; chacun une démonstration neuve, en forme canonique ; +// l'aller-retour sur la petite démonstration ; les deux CSV de +// démonstration, l'export exact des fichiers livrés ; les deux CSV des cas +// limites, la même lecture en deux encodages, où chaque cas du § 10.3 se +// voit, et des noms inventés hors des listes du générateur. L'empreinte, le +// profil et l'aller-retour sur la grande sont dans livrees.long.test.js. +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { readdirSync, readFileSync } from 'node:fs'; +import { varianteWindows1252 } from '../../scripts/engendrer_livrables.js'; +import { describe, test } from '../../test/lanceur.js'; +import { versionVoisine } from '../../test/version_voisine.js'; +import { apercevoir } from '../csv/apercu.js'; +import { decoder } from '../csv/encodage.js'; +import { exporterParticipants } from '../csv/export.js'; +import { serialiser } from '../stockage/canonique.js'; +import { analyser } from '../stockage/document.js'; +import { examiner } from '../stockage/placements.js'; +import { VERSION } from '../version.genere.js'; +import { CATALOGUE } from './catalogue.js'; +import { LIVREES, texteLivre } from './livrees.js'; +import { NOMS_LONGS, PRENOMS, RACINES, TETES, patronymes } from './noms.js'; + +const DOSSIER_LIVREES = new URL('./livrees/', import.meta.url); +const EXEMPLES = new URL('../../exemples/', import.meta.url); + +// Les démonstrations dont la série surveillée fait l'aller-retour ; la série +// longue le fait sur toutes (§ 14.4). +const PETITES = new Set(['petite', 'petite-conflit']); + +const MARQUE_UTF8 = [0xef, 0xbb, 0xbf]; + +const lireLivre = (cle) => readFileSync(new URL(`${cle}.gtt.json`, DOSSIER_LIVREES)); +const lireExemple = (nom) => readFileSync(new URL(nom, EXEMPLES)); + +// Texte d'un fichier livré, décodé en UTF-8 strict : un octet invalide lève. +const texteDuFichier = (cle) => new TextDecoder('utf-8', { fatal: true }).decode(lireLivre(cle)); + +// Programme que Node exécute seul, hors de Vite et de Vitest : il importe +// livrees.js et écrit LIVREES en JSON sur sa sortie standard. +const SOUS_NODE_SEUL = [ + `const { LIVREES } = await import(${JSON.stringify(new URL('./livrees.js', import.meta.url).href)});`, + 'process.stdout.write(JSON.stringify(LIVREES));', +].join('\n'); + +// Les fichiers livrés que retient garder ; lève quand il n'en reste aucun : +// un balayage qui n'examine rien ne prouve rien (§ 14.2). +function balayer(garder) { + const examines = LIVREES.filter(({ cle }) => garder(cle)); + assert.ok(examines.length > 0, 'aucun fichier livré examiné'); + return examines; +} + +// Aller-retour d'un fichier livré (§ 8.9) : charger — analyser, puis +// contrôler les placements, qui n'écartent ni ne signalent rien —, +// enregistrer sous une autre construction et à la révision suivante, +// recharger. Rend la charge chargée, la charge rechargée, et le texte que +// l'enregistrement écrirait sous l'en-tête lu. +function allerRetour(texte) { + const lu = analyser(texte); + const { charge, fautives, derives } = examiner(lu.charge); + assert.deepEqual([fautives, derives], [[], []]); + const enregistre = serialiser(charge, { + revision: lu.entete.revision + 1, + produitVersion: versionVoisine(VERSION.affichee), + }); + const relu = analyser(enregistre); + assert.notEqual(relu.entete.revision, lu.entete.revision); + return { charge, relue: relu.charge, sousLEnteteLu: serialiser(charge, lu.entete) }; +} + +// Rang du premier octet où deux suites diffèrent, longueur comprise, ou -1. +function premierEcart(a, b) { + const longueur = Math.min(a.length, b.length); + for (let rang = 0; rang < longueur; rang += 1) if (a[rang] !== b[rang]) return rang; + return a.length === b.length ? -1 : longueur; +} + +// Les deux lectures du CSV des cas limites : celle du fichier écrit à la +// main et celle de sa variante windows-1252. +function lecturesDesCasLimites() { + return [ + ['participants_cas_limites.csv', decoder(lireExemple('participants_cas_limites.csv'))], + ['participants_cas_limites_cp1252.csv', decoder(lireExemple('participants_cas_limites_cp1252.csv'))], + ].map(([nom, { texte, encodage }]) => ({ nom, encodage, apercu: apercevoir(texte) })); +} + +describe('démonstrations livrées : les fichiers (§ 15.5, point 5)', () => { + test("un fichier par entrée du catalogue, nommé .gtt.json, et rien d'autre : ni journal ni proposition", () => { + const fichiers = readdirSync(DOSSIER_LIVREES).sort(); + assert.ok(fichiers.length > 0, 'aucun fichier livré'); + assert.deepEqual(fichiers, CATALOGUE.map(({ cle }) => `${cle}.gtt.json`).sort()); + }); + + test("LIVREES porte le texte de chaque fichier, octet pour octet, dans l'ordre du catalogue, figé", () => { + assert.deepEqual( + LIVREES.map(({ cle }) => cle), + CATALOGUE.map(({ cle }) => cle), + ); + for (const livree of LIVREES) { + assert.equal( + livree.texte, + texteDuFichier(livree.cle), + `${livree.cle} : LIVREES ne porte pas le texte du fichier ; un fichier hors de la forme canonique s'y réécrit autrement`, + ); + assert.ok(Object.isFrozen(livree), livree.cle); + } + assert.ok(Object.isFrozen(LIVREES)); + }); + + test('Node seul, sans Vite ni Vitest, importe livrees.js et y lit le texte de chaque fichier : le repli « node --test » le charge (§ 14.8)', () => { + const resultat = spawnSync(process.execPath, ['--input-type=module', '--eval', SOUS_NODE_SEUL], { + encoding: 'utf8', + }); + assert.equal(resultat.status, 0, resultat.stderr); + const lues = JSON.parse(resultat.stdout); + assert.deepEqual( + lues.map(({ cle }) => cle), + CATALOGUE.map(({ cle }) => cle), + ); + for (const { cle, texte } of lues) { + assert.equal(texte, texteDuFichier(cle), `${cle} : le texte lu sous Node seul diffère du fichier`); + } + }); + + test("texteLivre rend le texte d'une clé, et refuse une clé qu'aucune démonstration ne porte", () => { + for (const { cle, texte } of LIVREES) assert.equal(texteLivre(cle), texte, cle); + assert.throws(() => texteLivre('moyenne'), (erreur) => erreur instanceof RangeError && /moyenne/.test(erreur.message)); + }); + + test('chaque fichier : UTF-8 sans marque, des LF seules, une seule fin de ligne finale', () => { + for (const { cle } of LIVREES) { + const octets = lireLivre(cle); + assert.notDeepEqual([...octets.subarray(0, 3)], MARQUE_UTF8, cle); + const texte = new TextDecoder('utf-8', { fatal: true }).decode(octets); + assert.ok(!texte.includes('\r'), cle); + assert.ok(texte.endsWith('}\n') && !texte.endsWith('\n\n'), cle); + } + }); + + test("chaque fichier est une démonstration neuve : identifiant demo:, nom du catalogue, révision 1, brouillon, sans filiation, ni proposition ni retenu", () => { + for (const { cle, texte } of LIVREES) { + const { entete, charge } = analyser(texte); + assert.deepEqual( + [entete.revision, charge.evenement.id, charge.evenement.nom, charge.evenement.etat, charge.evenement.filiation], + [1, `demo:${cle}`, CATALOGUE.find((entree) => entree.cle === cle).nom, 'brouillon', null], + cle, + ); + assert.deepEqual([charge.propositions, charge.retenu], [[], null], cle); + } + }); +}); + +describe('démonstrations livrées : aller-retour sur la petite (§ 8.9, § 14.4)', () => { + test("charger, enregistrer, recharger rend la même charge ; le texte livré est celui qu'écrit le sérialiseur", () => { + for (const { cle, texte } of balayer((cle) => PETITES.has(cle))) { + const { charge, relue, sousLEnteteLu } = allerRetour(texte); + assert.deepEqual(relue, charge, cle); + assert.equal(sousLEnteteLu, texte, cle); + } + }); + + test("le balayage refuse de passer quand il n'examine aucun fichier (§ 14.2)", () => { + assert.throws(() => balayer(() => false), /aucun fichier livré examiné/); + }); +}); + +describe("CSV d'exemple : les deux de démonstration (§ 10.3)", () => { + test("participants_demo_petite.csv et participants_demo_grande.csv égalent, octet pour octet, l'export des fichiers livrés", () => { + for (const cle of ['petite', 'grande']) { + const lus = lireExemple(`participants_demo_${cle}.csv`); + const attendus = exporterParticipants(analyser(texteLivre(cle)).charge.participants); + assert.equal(premierEcart(lus, attendus), -1, `participants_demo_${cle}.csv : premier octet qui diffère`); + } + }); +}); + +describe("CSV d'exemple : les cas limites (§ 10.3)", () => { + test('UTF-8 avec marque et « ; », puis windows-1252 et « , » : la même lecture, séparateur à part', () => { + assert.deepEqual([...lireExemple('participants_cas_limites.csv').subarray(0, 3)], MARQUE_UTF8); + const [utf8, windows1252] = lecturesDesCasLimites(); + assert.deepEqual( + [utf8.encodage, utf8.apercu.separateur, windows1252.encodage, windows1252.apercu.separateur], + ['utf-8-bom', ';', 'windows-1252', ','], + ); + assert.equal(utf8.apercu.refusGlobal, null); + assert.deepEqual({ ...windows1252.apercu, separateur: ';' }, utf8.apercu); + }); + + test('la variante windows-1252 est celle que le script dérive du fichier écrit à la main', () => { + const lus = lireExemple('participants_cas_limites_cp1252.csv'); + const attendus = varianteWindows1252(lireExemple('participants_cas_limites.csv')); + assert.equal(premierEcart(lus, attendus), -1, 'participants_cas_limites_cp1252.csv : premier octet qui diffère'); + }); + + test('chaque cas du § 10.3 se voit dans les deux fichiers', () => { + for (const { nom, apercu } of lecturesDesCasLimites()) { + const champs = apercu.lignes.map((ligne) => ligne.champs); + const bruts = apercu.lignes.flatMap(({ brut }) => brut); + const cas = [ + ['une colonne non reconnue', apercu.association.nonReconnues.length > 0], + ['une personne sans appartenance', champs.some(({ appartenance }) => appartenance === null)], + ['une exclusion', champs.some(({ exclu }) => exclu === true)], + // Un champ qui porte le séparateur n'a pu se lire entier que cité. + ['un champ cité contenant le séparateur', bruts.some((champ) => champ.includes(apercu.separateur))], + ['un champ cité sur deux lignes', bruts.some((champ) => champ.includes('\n'))], + ['un doublon', apercu.doublons.length > 0], + ['une personne sans prénom', champs.some(({ prenom }) => prenom === null)], + ['deux orthographes fondues', apercu.fusions.some(({ orthographes }) => orthographes.length >= 2)], + ['une ligne refusée faute de nom', apercu.refusees.some(({ code }) => code === 'NOM_ABSENT')], + ]; + assert.deepEqual(cas.filter(([, vu]) => !vu).map(([libelle]) => libelle), [], nom); + } + }); + + test('noms, organisations et courriels inventés : aucun nom des listes du générateur, chaque courriel sous .test', () => { + const { texte } = decoder(lireExemple('participants_cas_limites.csv')); + // Chaque nom des listes, en mot entier, sans égard à la casse, en NFC. + const noms = [...PRENOMS, ...patronymes(), ...NOMS_LONGS, ...TETES, ...RACINES] + .map((nom) => nom.normalize('NFC').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + const motif = new RegExp(`(? mot), []); + const courriels = texte.match(/[^\s;,"]+@[^\s;,"]+/g) ?? []; + assert.ok(courriels.length > 0, 'aucun courriel'); + assert.deepEqual(courriels.filter((courriel) => !courriel.endsWith('.test')), []); + }); +}); diff --git a/src/demo/livrees/grande-sans-exception.gtt.json b/src/demo/livrees/grande-sans-exception.gtt.json new file mode 100644 index 0000000..3e4c42a --- /dev/null +++ b/src/demo/livrees/grande-sans-exception.gtt.json @@ -0,0 +1,377 @@ +{ + "entete": {"format":1,"produitVersion":"2026.10.05.01","revision":1,"comptes":{"participants":260,"tables":33,"reservations":33,"titres":33,"propositions":0,"retenu":0}}, + "charge": { + "evenement": {"id":"demo:grande-sans-exception","nom":"Grande démonstration sans exception","date":null,"siegesParDefaut":8,"tours":4,"unite":"cm","etat":"brouillon","filiation":null}, + "reglages": {"separerAppartenances":true,"nouveauxVoisins":true,"nouvelleTable":true,"varierAppartenances":true,"attribuerSieges":false,"generation":{"nombre":5,"arret":200000,"historique":1000}}, + "prochainsIds": {"participant":261,"table":34,"proposition":1}, + "participants": [ + {"id":1,"nom":"Mirvault","prenom":"Isabelle","appartenance":"Comptoir Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":2,"nom":"Neskière","prenom":"Youssef","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":3,"nom":"Abrevande","prenom":"Félix","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":4,"nom":"Fralvançon","prenom":"Hugo","appartenance":"Brasserie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":5,"nom":"Fralveux","prenom":"Manon","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":6,"nom":"Luskotte","prenom":"Catherine","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":7,"nom":"WAMMERWOLM-MAWWEMMEL","prenom":"Zoé","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":8,"nom":"Neskichon","prenom":"Louise","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":9,"nom":"Urvesson","prenom":"Catherine","appartenance":"Fabrique Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":10,"nom":"Hulvivet","prenom":"Karim","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":11,"nom":"Selveron","prenom":"Caroline","appartenance":"Ateliers Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":12,"nom":"Korvault","prenom":"Catherine","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":13,"nom":"Fralvotte","prenom":"Caroline","appartenance":"Fonderie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":14,"nom":"Jusvesson","prenom":"Geneviève","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":15,"nom":"Abrevumet","prenom":"Paul","appartenance":"Ateliers Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":16,"nom":"Fralvault","prenom":"Léa","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":17,"nom":"Obrivet","prenom":"Fatima","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":18,"nom":"Voskeval","prenom":"Philippe","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":19,"nom":"Dulveval","prenom":"Zoé","appartenance":"Menuiserie Quimvaux","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":20,"nom":"Korveux","prenom":"Paul","appartenance":"Filature Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":21,"nom":"Luskière","prenom":"Julie","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":22,"nom":"Clovande","prenom":"Émilie","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":23,"nom":"Hulvande","prenom":"Paul","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":24,"nom":"Fralvesson","prenom":"Hugo","appartenance":"Filature Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":25,"nom":"Urvière","prenom":"Isabelle","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":26,"nom":"Rozvotte","prenom":"Thomas","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":27,"nom":"Bralmumet","prenom":"Isabelle","appartenance":"Verrerie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":28,"nom":"Dulvouze","prenom":"Catherine","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":29,"nom":"Voskichon","prenom":"Marc","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":30,"nom":"Mirvivet","prenom":"Simon","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":31,"nom":"Clovault","prenom":"Louise","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":32,"nom":"Voskeux","prenom":"Lucie","appartenance":"Coopérative Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":33,"nom":"Eskignac","prenom":"Amina","appartenance":"Brasserie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":34,"nom":"Korvouze","prenom":"Fatima","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":35,"nom":"Selvoubre","prenom":"Luc","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":36,"nom":"Fralvichon","prenom":"Étienne","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":37,"nom":"Voskivet","prenom":"Philippe","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":38,"nom":"Quilmançon","prenom":"Louise","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":39,"nom":"Luskande","prenom":"Sophie","appartenance":"Fabrique Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":40,"nom":"Abreveron","prenom":"Pierre","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":41,"nom":"Neskeux","prenom":"Philippe","appartenance":"Comptoir Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":42,"nom":"Neskançon","prenom":"Amina","appartenance":"Fabrique Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":43,"nom":"Tolvande","prenom":"Léa","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":44,"nom":"Quilmeron","prenom":"Antoine","appartenance":"Fabrique Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":45,"nom":"Clovumet","prenom":"Guillaume","appartenance":"Verrerie Pravette","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":46,"nom":"Eskeux","prenom":"Michel","appartenance":"Papeterie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":47,"nom":"Hulvault","prenom":"Louise","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":48,"nom":"Hulveron","prenom":"Catherine","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":49,"nom":"Ivrivet","prenom":"François","appartenance":"Menuiserie Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":50,"nom":"Eskumet","prenom":"Samuel","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":51,"nom":"Fralvivet","prenom":"Claire","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":52,"nom":"Mirveux","prenom":"Léa","appartenance":"Menuiserie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":53,"nom":"Gorzesson","prenom":"Karim","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":54,"nom":"Fralvumet","prenom":"Jean","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":55,"nom":"Urvançon","prenom":"Manon","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":56,"nom":"Quilmande","prenom":"Sylvie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":57,"nom":"Gorzichon","prenom":"François","appartenance":"Ateliers Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":58,"nom":"Pruvesson","prenom":"Amina","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":59,"nom":"Quilmière","prenom":"Lucie","appartenance":"Fabrique Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":60,"nom":"Korvesson","prenom":"Fatima","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":61,"nom":"Selvesson","prenom":"Nicolas","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":62,"nom":"Selvivet","prenom":"Louis","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":63,"nom":"Neskesson","prenom":"Louise","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":64,"nom":"Tolvesson","prenom":"Lucie","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":65,"nom":"Quilmesson","prenom":"Olivier","appartenance":"Comptoir Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":66,"nom":"Luskivet","prenom":"Guillaume","appartenance":"Brasserie Lavorneau","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":67,"nom":"Neskault","prenom":"Gabriel","appartenance":"Menuiserie Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":68,"nom":"Urvumet","prenom":"Antoine","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":69,"nom":"Jusvançon","prenom":"Olivier","appartenance":"Brasserie Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":70,"nom":"Dulvançon","prenom":"Simon","appartenance":"Menuiserie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":71,"nom":"Gorzard","prenom":"Chloé","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":72,"nom":"Pruveux","prenom":"Gabriel","appartenance":"Papeterie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":73,"nom":"Clovière","prenom":"Manon","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":74,"nom":"Obrouze","prenom":"François","appartenance":"Tannerie Olbrecq","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":75,"nom":"Tolveval","prenom":"Karim","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":76,"nom":"Dulvumet","prenom":"Geneviève","appartenance":"Menuiserie Quimvaux","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":77,"nom":"Bérêçàuvïlle-Dœûmôntëÿ","prenom":"Hugo","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":78,"nom":"Gorzeron","prenom":"Claire","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":79,"nom":"Luskumet","prenom":"Hélène","appartenance":"Brasserie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":80,"nom":"Fralvande","prenom":"Louis","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":81,"nom":"Selvault","prenom":"Émilie","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":82,"nom":"Neskumet","prenom":"Léa","appartenance":"Verrerie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":83,"nom":"Korvichon","prenom":"Isabelle","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":84,"nom":"Obrande","prenom":"Michel","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":85,"nom":"Luskeval","prenom":"Isabelle","appartenance":"Brasserie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":86,"nom":"Tolvoubre","prenom":"Fatima","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":87,"nom":"Gorzière","prenom":"Zoé","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":88,"nom":"Korvignac","prenom":"Sylvie","appartenance":"Fabrique Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":89,"nom":"Obreval","prenom":"Antoine","appartenance":"Ateliers Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":90,"nom":"Korveron","prenom":"Michel","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":91,"nom":"Tolvumet","prenom":"Camille","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":92,"nom":"Dulvesson","prenom":"Marie","appartenance":"Ateliers Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":93,"nom":"Jusvard","prenom":"Youssef","appartenance":"Tannerie Aubrenc","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":94,"nom":"Tolvignac","prenom":"Guillaume","appartenance":"Menuiserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":95,"nom":"Mirvotte","prenom":"Gabriel","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":96,"nom":"Urvard","prenom":"Marie","appartenance":"Filature Wassorel","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":97,"nom":"Bralmesson","prenom":"Marie","appartenance":"Coopérative Dravenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":98,"nom":"Neskignac","prenom":"Paul","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":99,"nom":"Bralmeux","prenom":"Samuel","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":100,"nom":"Ivrotte","prenom":"Raphaël","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":101,"nom":"Luskouze","prenom":"Marc","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":102,"nom":"Jusvotte","prenom":"Sylvie","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":103,"nom":"Bralmoubre","prenom":"Véronique","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":104,"nom":"Selvard","prenom":"Pierre","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":105,"nom":"Hulveux","prenom":"Marie","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":106,"nom":"Pruvotte","prenom":"Sophie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":107,"nom":"Fralveval","prenom":"Karim","appartenance":"Coopérative Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":108,"nom":"Gorzignac","prenom":"Philippe","appartenance":"Imprimerie Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":109,"nom":"Selvouze","prenom":"Étienne","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":110,"nom":"Hulvichon","prenom":"Louis","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":111,"nom":"Urvoubre","prenom":"Sylvie","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":112,"nom":"Pruveval","prenom":"Pierre","appartenance":"Comptoir Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":113,"nom":"Hulvotte","prenom":"Guillaume","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":114,"nom":"Abrevignac","prenom":"Geneviève","appartenance":"Brasserie Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":115,"nom":"Abrevault","prenom":"Gabriel","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":116,"nom":"Bralmignac","prenom":"Amina","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":117,"nom":"Bralmançon","prenom":"Olivier","appartenance":"Comptoir Lavorneau","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":118,"nom":"Luskoubre","prenom":"Marc","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":119,"nom":"Abrevoubre","prenom":"Geneviève","appartenance":"Menuiserie Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":120,"nom":"Korvoubre","prenom":"Raphaël","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":121,"nom":"Mirvard","prenom":"Pierre","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":122,"nom":"Jusvumet","prenom":"Camille","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":123,"nom":"Cloveval","prenom":"Samuel","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":124,"nom":"Selvière","prenom":"Antoine","appartenance":"Coopérative Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":125,"nom":"Jusvouze","prenom":"Véronique","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":126,"nom":"Neskivet","prenom":"Paul","appartenance":"Comptoir Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":127,"nom":"Dulvande","prenom":"Samuel","appartenance":"Verrerie Pravette","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":128,"nom":"Bralmault","prenom":"Antoine","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":129,"nom":"Abrevière","prenom":"Émilie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":130,"nom":"Selveval","prenom":"Lucie","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":131,"nom":"Tolvichon","prenom":"Olivier","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":132,"nom":"Hulvard","prenom":"Fatima","appartenance":"Ateliers Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":133,"nom":"Ivreux","prenom":"Alexandre","appartenance":"Tannerie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":134,"nom":"Korveval","prenom":"Félix","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":135,"nom":"Urvichon","prenom":"Karim","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":136,"nom":"Dulvichon","prenom":"Émilie","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":137,"nom":"Tolvouze","prenom":"Luc","appartenance":"Fabrique Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":138,"nom":"Jusvivet","prenom":"Claire","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":139,"nom":"Quilmault","prenom":"Amina","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":140,"nom":"Ivrouze","prenom":"François","appartenance":"Fonderie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":141,"nom":"Gorzumet","prenom":"Hélène","appartenance":"Fonderie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":142,"nom":"Ivrault","prenom":"Sophie","appartenance":"Coopérative Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":143,"nom":"Bralmière","prenom":"Louise","appartenance":"Tannerie Quimvaux","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":144,"nom":"Korvande","prenom":"Nicolas","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":145,"nom":"Ivrumet","prenom":"Geneviève","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":146,"nom":"Obresson","prenom":"Geneviève","appartenance":"Coopérative Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":147,"nom":"Obrançon","prenom":"Guillaume","appartenance":"Papeterie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":148,"nom":"Urvignac","prenom":"Fatima","appartenance":"Ateliers Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":149,"nom":"Bralmotte","prenom":"Claire","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":150,"nom":"Hulvignac","prenom":"Félix","appartenance":"Imprimerie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":151,"nom":"Clovotte","prenom":"François","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":152,"nom":"Dulvoubre","prenom":"Philippe","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":153,"nom":"Rozveron","prenom":"Samuel","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":154,"nom":"Hulvoubre","prenom":"Pierre","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":155,"nom":"Urvouze","prenom":"Émilie","appartenance":"Comptoir Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":156,"nom":"Korvançon","prenom":"Félix","appartenance":"Fonderie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":157,"nom":"Neskande","prenom":"Émilie","appartenance":"Coopérative Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":158,"nom":"Mirvumet","prenom":"Chantal","appartenance":"Menuiserie Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":159,"nom":"Korvotte","prenom":"Alexandre","appartenance":"Menuiserie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":160,"nom":"Eskeval","prenom":"Zoé","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":161,"nom":"Neskard","prenom":"Camille","appartenance":"Menuiserie Gravelune","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":162,"nom":"Tolveron","prenom":"Manon","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":163,"nom":"Voskoubre","prenom":"Nathalie","appartenance":"Fonderie Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":164,"nom":"Pruvière","prenom":"Sophie","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":165,"nom":"Obrichon","prenom":"Paul","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":166,"nom":"Abrevançon","prenom":"Fatima","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":167,"nom":"Eskière","prenom":"Nathalie","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":168,"nom":"Ivreron","prenom":"Caroline","appartenance":"Imprimerie Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":169,"nom":"Neskoubre","prenom":"Véronique","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":170,"nom":"Urvivet","prenom":"Anne","appartenance":"Fonderie Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":171,"nom":"Hulvançon","prenom":"Olivier","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":172,"nom":"Clovançon","prenom":"Karim","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":173,"nom":"Quilmotte","prenom":"Zoé","appartenance":"Ateliers Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":174,"nom":"Eskouze","prenom":"Félix","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":175,"nom":"Rozvichon","prenom":"Amina","appartenance":"Verrerie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":176,"nom":"Fralvière","prenom":"Simon","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":177,"nom":"Vandergrolcquesmirtavelle-Ostrovaubelindigues","prenom":"Marc","appartenance":"Tannerie Aubrenc","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":178,"nom":"Hulvière","prenom":"Pierre","appartenance":"Papeterie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":179,"nom":"Jusvault","prenom":"Raphaël","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":180,"nom":"Dulvivet","prenom":"Guillaume","appartenance":"Filature Olbrecq","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":181,"nom":"Fralvoubre","prenom":"Pierre","appartenance":"Imprimerie Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":182,"nom":"Selveux","prenom":"Hélène","appartenance":"Brasserie Lavorneau","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":183,"nom":"Rozvançon","prenom":"Caroline","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":184,"nom":"Korvière","prenom":"Olivier","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":185,"nom":"Ivrichon","prenom":"Nicolas","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":186,"nom":"Korvumet","prenom":"Félix","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":187,"nom":"Voskande","prenom":"Félix","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":188,"nom":"Selvumet","prenom":"Guillaume","appartenance":"Papeterie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":189,"nom":"Urvotte","prenom":"Michel","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":190,"nom":"Eskault","prenom":"Émilie","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":191,"nom":"Bralmeval","prenom":"Hélène","appartenance":"Brasserie Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":192,"nom":"Hulvesson","prenom":"Guillaume","appartenance":"Comptoir Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":193,"nom":"Jusvoubre","prenom":"Caroline","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":194,"nom":"Luskeux","prenom":"Félix","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":195,"nom":"Pruvouze","prenom":"Antoine","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":196,"nom":"Quilmignac","prenom":"Julie","appartenance":"Menuiserie Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":197,"nom":"Tolvançon","prenom":"Étienne","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":198,"nom":"Voskumet","prenom":"Hugo","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":199,"nom":"Quilmumet","prenom":"Manon","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":200,"nom":"Voskouze","prenom":"Marc","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":201,"nom":"Abrevard","prenom":"Samuel","appartenance":"Filature Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":202,"nom":"Luskichon","prenom":"Guillaume","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":203,"nom":"Clovesson","prenom":"Manon","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":204,"nom":"Hulvumet","prenom":"Simon","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":205,"nom":"Voskeron","prenom":"Lucie","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":206,"nom":"Selvignac","prenom":"Thomas","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":207,"nom":"Neskeron","prenom":"Jean","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":208,"nom":"Pruvivet","prenom":"Manon","appartenance":"Menuiserie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":209,"nom":"Clovard","prenom":"Sophie","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":210,"nom":"Gorzeval","prenom":"Étienne","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":211,"nom":"Ivroubre","prenom":"Philippe","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":212,"nom":"Luskault","prenom":"Gabriel","appartenance":"Verrerie Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":213,"nom":"Korvard","prenom":"Émilie","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":214,"nom":"Quilmichon","prenom":"Thomas","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":215,"nom":"Eskançon","prenom":"Alexandre","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":216,"nom":"Luskançon","prenom":"Émilie","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":217,"nom":"Dulvault","prenom":"Marc","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":218,"nom":"Rozveval","prenom":"Hélène","appartenance":"Comptoir Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":219,"nom":"Pruvançon","prenom":"Julie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":220,"nom":"Mirveron","prenom":"Louis","appartenance":"Filature Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":221,"nom":"Eskard","prenom":"François","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":222,"nom":"Bralmouze","prenom":"Hugo","appartenance":"Tannerie Falvigne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":223,"nom":"Ivrançon","prenom":"Louis","appartenance":"Coopérative Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":224,"nom":"Clovivet","prenom":"Karim","appartenance":"Fonderie Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":225,"nom":"Rozvande","prenom":"Léa","appartenance":"Verrerie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":226,"nom":"Hulvouze","prenom":"Amina","appartenance":"Coopérative Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":227,"nom":"Eskesson","prenom":"Étienne","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":228,"nom":"Abreveval","prenom":"Philippe","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":229,"nom":"Quilmeux","prenom":"Émilie","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":230,"nom":"Ivreval","prenom":"Gabriel","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":231,"nom":"Voskotte","prenom":"Simon","appartenance":"Fabrique Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":232,"nom":"Obrignac","prenom":"Chantal","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":233,"nom":"Eskotte","prenom":"Guillaume","appartenance":"Verrerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":234,"nom":"Obrard","prenom":"Léa","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":235,"nom":"Gorzivet","prenom":"Guillaume","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":236,"nom":"Mirvançon","prenom":"Geneviève","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":237,"nom":"Selvotte","prenom":"Julie","appartenance":"Comptoir Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":238,"nom":"Mirvesson","prenom":"Amina","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":239,"nom":"Luskignac","prenom":"Isabelle","appartenance":"Papeterie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":240,"nom":"Obrumet","prenom":"Chloé","appartenance":"Comptoir Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":241,"nom":"Abrevesson","prenom":"Anne","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":242,"nom":"Rozvumet","prenom":"Fatima","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":243,"nom":"Mirvoubre","prenom":"Guillaume","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":244,"nom":"Rozvoubre","prenom":"Véronique","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":245,"nom":"Gorzotte","prenom":"Claire","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":246,"nom":"Ivrière","prenom":"Antoine","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":247,"nom":"Selvichon","prenom":"Isabelle","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":248,"nom":"Urvault","prenom":"Isabelle","appartenance":"Comptoir Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":249,"nom":"Voskesson","prenom":"Julie","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":250,"nom":"Abrevotte","prenom":"Chantal","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":251,"nom":"Neskotte","prenom":"Amina","appartenance":"Fabrique Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":252,"nom":"Voskançon","prenom":"Étienne","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":253,"nom":"Fralveron","prenom":"Zoé","appartenance":"Menuiserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":254,"nom":"Rozvouze","prenom":"Marc","appartenance":"Ateliers Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":255,"nom":"Abreveux","prenom":"Sylvie","appartenance":"Comptoir Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":256,"nom":"Pruvignac","prenom":"Karim","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":257,"nom":"Mirveval","prenom":"Zoé","appartenance":"Fabrique Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":258,"nom":"Fralvignac","prenom":"Philippe","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":259,"nom":"Gorzeux","prenom":"Gabriel","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":260,"nom":"Urveval","prenom":"Jean","appartenance":"Menuiserie Quimvaux","courriel":null,"titrePressenti":null,"notes":null,"exclu":false} + ], + "tables": [ + {"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}}, + {"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}}, + {"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":600,"y":0}}, + {"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":900,"y":0}}, + {"id":5,"numero":5,"sieges":null,"forme":"ronde","position":{"x":1200,"y":0}}, + {"id":6,"numero":6,"sieges":null,"forme":"ronde","position":{"x":1500,"y":0}}, + {"id":7,"numero":7,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}}, + {"id":8,"numero":8,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}}, + {"id":9,"numero":9,"sieges":null,"forme":"ronde","position":{"x":600,"y":300}}, + {"id":10,"numero":10,"sieges":null,"forme":"ronde","position":{"x":900,"y":300}}, + {"id":11,"numero":11,"sieges":null,"forme":"ronde","position":{"x":1200,"y":300}}, + {"id":12,"numero":12,"sieges":null,"forme":"ronde","position":{"x":1500,"y":300}}, + {"id":13,"numero":13,"sieges":null,"forme":"ronde","position":{"x":0,"y":600}}, + {"id":14,"numero":14,"sieges":null,"forme":"ronde","position":{"x":300,"y":600}}, + {"id":15,"numero":15,"sieges":null,"forme":"ronde","position":{"x":600,"y":600}}, + {"id":16,"numero":16,"sieges":null,"forme":"ronde","position":{"x":900,"y":600}}, + {"id":17,"numero":17,"sieges":null,"forme":"ronde","position":{"x":1200,"y":600}}, + {"id":18,"numero":18,"sieges":null,"forme":"ronde","position":{"x":1500,"y":600}}, + {"id":19,"numero":19,"sieges":null,"forme":"ronde","position":{"x":0,"y":900}}, + {"id":20,"numero":20,"sieges":null,"forme":"ronde","position":{"x":300,"y":900}}, + {"id":21,"numero":21,"sieges":null,"forme":"ronde","position":{"x":600,"y":900}}, + {"id":22,"numero":22,"sieges":null,"forme":"ronde","position":{"x":900,"y":900}}, + {"id":23,"numero":23,"sieges":null,"forme":"ronde","position":{"x":1200,"y":900}}, + {"id":24,"numero":24,"sieges":null,"forme":"ronde","position":{"x":1500,"y":900}}, + {"id":25,"numero":25,"sieges":null,"forme":"ronde","position":{"x":0,"y":1200}}, + {"id":26,"numero":26,"sieges":null,"forme":"ronde","position":{"x":300,"y":1200}}, + {"id":27,"numero":27,"sieges":null,"forme":"ronde","position":{"x":600,"y":1200}}, + {"id":28,"numero":28,"sieges":null,"forme":"ronde","position":{"x":900,"y":1200}}, + {"id":29,"numero":29,"sieges":null,"forme":"ronde","position":{"x":1200,"y":1200}}, + {"id":30,"numero":30,"sieges":null,"forme":"ronde","position":{"x":1500,"y":1200}}, + {"id":31,"numero":31,"sieges":null,"forme":"ronde","position":{"x":0,"y":1500}}, + {"id":32,"numero":32,"sieges":null,"forme":"ronde","position":{"x":300,"y":1500}}, + {"id":33,"numero":33,"sieges":null,"forme":"ronde","position":{"x":600,"y":1500}} + ], + "reservations": [ + {"participant":2,"table":17,"siege":1,"portee":"tous","tour":null}, + {"participant":8,"table":22,"siege":1,"portee":"tous","tour":null}, + {"participant":18,"table":6,"siege":1,"portee":"tous","tour":null}, + {"participant":34,"table":3,"siege":1,"portee":"tous","tour":null}, + {"participant":48,"table":15,"siege":1,"portee":"tous","tour":null}, + {"participant":49,"table":32,"siege":1,"portee":"tous","tour":null}, + {"participant":54,"table":26,"siege":1,"portee":"tous","tour":null}, + {"participant":55,"table":1,"siege":1,"portee":"tous","tour":null}, + {"participant":62,"table":25,"siege":1,"portee":"tous","tour":null}, + {"participant":74,"table":31,"siege":1,"portee":"tous","tour":null}, + {"participant":76,"table":30,"siege":1,"portee":"tous","tour":null}, + {"participant":90,"table":8,"siege":1,"portee":"tous","tour":null}, + {"participant":93,"table":21,"siege":1,"portee":"tous","tour":null}, + {"participant":96,"table":14,"siege":1,"portee":"tous","tour":null}, + {"participant":97,"table":27,"siege":1,"portee":"tous","tour":null}, + {"participant":99,"table":33,"siege":1,"portee":"tous","tour":null}, + {"participant":106,"table":12,"siege":1,"portee":"tous","tour":null}, + {"participant":115,"table":24,"siege":1,"portee":"tous","tour":null}, + {"participant":128,"table":4,"siege":1,"portee":"tous","tour":null}, + {"participant":129,"table":29,"siege":1,"portee":"tous","tour":null}, + {"participant":138,"table":20,"siege":1,"portee":"tous","tour":null}, + {"participant":161,"table":16,"siege":1,"portee":"tous","tour":null}, + {"participant":162,"table":5,"siege":1,"portee":"tous","tour":null}, + {"participant":166,"table":13,"siege":1,"portee":"tous","tour":null}, + {"participant":177,"table":10,"siege":1,"portee":"tous","tour":null}, + {"participant":180,"table":18,"siege":1,"portee":"tous","tour":null}, + {"participant":187,"table":7,"siege":1,"portee":"tous","tour":null}, + {"participant":197,"table":11,"siege":1,"portee":"tous","tour":null}, + {"participant":202,"table":2,"siege":1,"portee":"tous","tour":null}, + {"participant":206,"table":28,"siege":1,"portee":"tous","tour":null}, + {"participant":211,"table":19,"siege":1,"portee":"tous","tour":null}, + {"participant":222,"table":23,"siege":1,"portee":"tous","tour":null}, + {"participant":230,"table":9,"siege":1,"portee":"tous","tour":null} + ], + "titres": [ + {"table":1,"siege":1,"libelle":"animation"}, + {"table":2,"siege":1,"libelle":"animation"}, + {"table":3,"siege":1,"libelle":"animation"}, + {"table":4,"siege":1,"libelle":"animation"}, + {"table":5,"siege":1,"libelle":"animation"}, + {"table":6,"siege":1,"libelle":"animation"}, + {"table":7,"siege":1,"libelle":"animation"}, + {"table":8,"siege":1,"libelle":"animation"}, + {"table":9,"siege":1,"libelle":"animation"}, + {"table":10,"siege":1,"libelle":"animation"}, + {"table":11,"siege":1,"libelle":"animation"}, + {"table":12,"siege":1,"libelle":"animation"}, + {"table":13,"siege":1,"libelle":"animation"}, + {"table":14,"siege":1,"libelle":"animation"}, + {"table":15,"siege":1,"libelle":"animation"}, + {"table":16,"siege":1,"libelle":"animation"}, + {"table":17,"siege":1,"libelle":"animation"}, + {"table":18,"siege":1,"libelle":"animation"}, + {"table":19,"siege":1,"libelle":"animation"}, + {"table":20,"siege":1,"libelle":"animation"}, + {"table":21,"siege":1,"libelle":"animation"}, + {"table":22,"siege":1,"libelle":"animation"}, + {"table":23,"siege":1,"libelle":"animation"}, + {"table":24,"siege":1,"libelle":"animation"}, + {"table":25,"siege":1,"libelle":"animation"}, + {"table":26,"siege":1,"libelle":"animation"}, + {"table":27,"siege":1,"libelle":"animation"}, + {"table":28,"siege":1,"libelle":"animation"}, + {"table":29,"siege":1,"libelle":"animation"}, + {"table":30,"siege":1,"libelle":"animation"}, + {"table":31,"siege":1,"libelle":"animation"}, + {"table":32,"siege":1,"libelle":"animation"}, + {"table":33,"siege":1,"libelle":"animation"} + ], + "propositions": [], + "retenu": null + } +} diff --git a/src/demo/livrees/grande.gtt.json b/src/demo/livrees/grande.gtt.json new file mode 100644 index 0000000..905bfc7 --- /dev/null +++ b/src/demo/livrees/grande.gtt.json @@ -0,0 +1,377 @@ +{ + "entete": {"format":1,"produitVersion":"2026.10.05.01","revision":1,"comptes":{"participants":260,"tables":33,"reservations":33,"titres":33,"propositions":0,"retenu":0}}, + "charge": { + "evenement": {"id":"demo:grande","nom":"Grande démonstration","date":null,"siegesParDefaut":8,"tours":4,"unite":"cm","etat":"brouillon","filiation":null}, + "reglages": {"separerAppartenances":true,"nouveauxVoisins":true,"nouvelleTable":true,"varierAppartenances":true,"attribuerSieges":false,"generation":{"nombre":5,"arret":200000,"historique":1000}}, + "prochainsIds": {"participant":261,"table":34,"proposition":1}, + "participants": [ + {"id":1,"nom":"Mirvault","prenom":"Isabelle","appartenance":"Comptoir Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":2,"nom":"Neskière","prenom":"Youssef","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":3,"nom":"Abrevande","prenom":"Félix","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":4,"nom":"Fralvançon","prenom":"Hugo","appartenance":"Brasserie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":5,"nom":"Fralveux","prenom":"Manon","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":6,"nom":"Luskotte","prenom":"Catherine","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":7,"nom":"WAMMERWOLM-MAWWEMMEL","prenom":"Zoé","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":8,"nom":"Neskichon","prenom":"Louise","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":9,"nom":"Urvesson","prenom":"Catherine","appartenance":"Fabrique Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":10,"nom":"Hulvivet","prenom":"Karim","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":11,"nom":"Selveron","prenom":"Caroline","appartenance":"Ateliers Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":12,"nom":"Korvault","prenom":"Catherine","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":13,"nom":"Fralvotte","prenom":"Caroline","appartenance":"Fonderie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":14,"nom":"Jusvesson","prenom":"Geneviève","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":15,"nom":"Abrevumet","prenom":"Paul","appartenance":"Ateliers Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":16,"nom":"Fralvault","prenom":"Léa","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":17,"nom":"Obrivet","prenom":"Fatima","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":18,"nom":"Voskeval","prenom":"Philippe","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":19,"nom":"Dulveval","prenom":"Zoé","appartenance":"Menuiserie Quimvaux","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":20,"nom":"Korveux","prenom":"Paul","appartenance":"Filature Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":21,"nom":"Luskière","prenom":"Julie","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":22,"nom":"Clovande","prenom":"Émilie","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":23,"nom":"Hulvande","prenom":"Paul","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":24,"nom":"Fralvesson","prenom":"Hugo","appartenance":"Filature Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":25,"nom":"Urvière","prenom":"Isabelle","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":26,"nom":"Rozvotte","prenom":"Thomas","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":27,"nom":"Bralmumet","prenom":"Isabelle","appartenance":"Verrerie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":28,"nom":"Dulvouze","prenom":"Catherine","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":29,"nom":"Voskichon","prenom":"Marc","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":30,"nom":"Mirvivet","prenom":"Simon","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":31,"nom":"Clovault","prenom":"Louise","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":32,"nom":"Voskeux","prenom":"Lucie","appartenance":"Coopérative Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":33,"nom":"Eskignac","prenom":"Amina","appartenance":"Brasserie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":34,"nom":"Korvouze","prenom":"Fatima","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":35,"nom":"Selvoubre","prenom":"Luc","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":36,"nom":"Fralvichon","prenom":"Étienne","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":37,"nom":"Voskivet","prenom":"Philippe","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":38,"nom":"Quilmançon","prenom":"Louise","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":39,"nom":"Luskande","prenom":"Sophie","appartenance":"Fabrique Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":40,"nom":"Abreveron","prenom":"Pierre","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":41,"nom":"Neskeux","prenom":"Philippe","appartenance":"Comptoir Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":42,"nom":"Neskançon","prenom":"Amina","appartenance":"Fabrique Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":43,"nom":"Tolvande","prenom":"Léa","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":44,"nom":"Quilmeron","prenom":"Antoine","appartenance":"Fabrique Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":45,"nom":"Clovumet","prenom":"Guillaume","appartenance":"Verrerie Pravette","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":46,"nom":"Eskeux","prenom":"Michel","appartenance":"Papeterie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":47,"nom":"Hulvault","prenom":"Louise","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":48,"nom":"Hulveron","prenom":"Catherine","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":49,"nom":"Ivrivet","prenom":"François","appartenance":"Menuiserie Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":50,"nom":"Eskumet","prenom":"Samuel","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":51,"nom":"Fralvivet","prenom":"Claire","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":52,"nom":"Mirveux","prenom":"Léa","appartenance":"Menuiserie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":53,"nom":"Gorzesson","prenom":"Karim","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":54,"nom":"Fralvumet","prenom":"Jean","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":55,"nom":"Urvançon","prenom":"Manon","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":56,"nom":"Quilmande","prenom":"Sylvie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":57,"nom":"Gorzichon","prenom":"François","appartenance":"Ateliers Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":58,"nom":"Pruvesson","prenom":"Amina","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":59,"nom":"Quilmière","prenom":"Lucie","appartenance":"Fabrique Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":60,"nom":"Korvesson","prenom":"Fatima","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":61,"nom":"Selvesson","prenom":"Nicolas","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":62,"nom":"Selvivet","prenom":"Louis","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":63,"nom":"Neskesson","prenom":"Louise","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":64,"nom":"Tolvesson","prenom":"Lucie","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":65,"nom":"Quilmesson","prenom":"Olivier","appartenance":"Comptoir Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":66,"nom":"Luskivet","prenom":"Guillaume","appartenance":"Brasserie Lavorneau","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":67,"nom":"Neskault","prenom":"Gabriel","appartenance":"Menuiserie Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":68,"nom":"Urvumet","prenom":"Antoine","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":69,"nom":"Jusvançon","prenom":"Olivier","appartenance":"Brasserie Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":70,"nom":"Dulvançon","prenom":"Simon","appartenance":"Menuiserie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":71,"nom":"Gorzard","prenom":"Chloé","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":72,"nom":"Pruveux","prenom":"Gabriel","appartenance":"Papeterie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":73,"nom":"Clovière","prenom":"Manon","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":74,"nom":"Obrouze","prenom":"François","appartenance":"Tannerie Olbrecq","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":75,"nom":"Tolveval","prenom":"Karim","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":76,"nom":"Dulvumet","prenom":"Geneviève","appartenance":"Menuiserie Quimvaux","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":77,"nom":"Bérêçàuvïlle-Dœûmôntëÿ","prenom":"Hugo","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":78,"nom":"Gorzeron","prenom":"Claire","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":79,"nom":"Luskumet","prenom":"Hélène","appartenance":"Brasserie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":80,"nom":"Fralvande","prenom":"Louis","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":81,"nom":"Selvault","prenom":"Émilie","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":82,"nom":"Neskumet","prenom":"Léa","appartenance":"Verrerie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":83,"nom":"Korvichon","prenom":"Isabelle","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":84,"nom":"Obrande","prenom":"Michel","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":85,"nom":"Luskeval","prenom":"Isabelle","appartenance":"Brasserie Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":86,"nom":"Tolvoubre","prenom":"Fatima","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":87,"nom":"Gorzière","prenom":"Zoé","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":88,"nom":"Korvignac","prenom":"Sylvie","appartenance":"Fabrique Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":89,"nom":"Obreval","prenom":"Antoine","appartenance":"Ateliers Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":90,"nom":"Korveron","prenom":"Michel","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":91,"nom":"Tolvumet","prenom":"Camille","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":92,"nom":"Dulvesson","prenom":"Marie","appartenance":"Ateliers Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":93,"nom":"Jusvard","prenom":"Youssef","appartenance":"Tannerie Aubrenc","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":94,"nom":"Tolvignac","prenom":"Guillaume","appartenance":"Menuiserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":95,"nom":"Mirvotte","prenom":"Gabriel","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":96,"nom":"Urvard","prenom":"Marie","appartenance":"Filature Wassorel","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":97,"nom":"Bralmesson","prenom":"Marie","appartenance":"Coopérative Dravenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":98,"nom":"Neskignac","prenom":"Paul","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":99,"nom":"Bralmeux","prenom":"Samuel","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":100,"nom":"Ivrotte","prenom":"Raphaël","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":101,"nom":"Luskouze","prenom":"Marc","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":102,"nom":"Jusvotte","prenom":"Sylvie","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":103,"nom":"Bralmoubre","prenom":"Véronique","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":104,"nom":"Selvard","prenom":"Pierre","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":105,"nom":"Hulveux","prenom":"Marie","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":106,"nom":"Pruvotte","prenom":"Sophie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":107,"nom":"Fralveval","prenom":"Karim","appartenance":"Coopérative Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":108,"nom":"Gorzignac","prenom":"Philippe","appartenance":"Imprimerie Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":109,"nom":"Selvouze","prenom":"Étienne","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":110,"nom":"Hulvichon","prenom":"Louis","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":111,"nom":"Urvoubre","prenom":"Sylvie","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":112,"nom":"Pruveval","prenom":"Pierre","appartenance":"Comptoir Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":113,"nom":"Hulvotte","prenom":"Guillaume","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":114,"nom":"Abrevignac","prenom":"Geneviève","appartenance":"Brasserie Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":115,"nom":"Abrevault","prenom":"Gabriel","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":116,"nom":"Bralmignac","prenom":"Amina","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":117,"nom":"Bralmançon","prenom":"Olivier","appartenance":"Comptoir Lavorneau","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":118,"nom":"Luskoubre","prenom":"Marc","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":119,"nom":"Abrevoubre","prenom":"Geneviève","appartenance":"Menuiserie Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":120,"nom":"Korvoubre","prenom":"Raphaël","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":121,"nom":"Mirvard","prenom":"Pierre","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":122,"nom":"Jusvumet","prenom":"Camille","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":123,"nom":"Cloveval","prenom":"Samuel","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":124,"nom":"Selvière","prenom":"Antoine","appartenance":"Coopérative Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":125,"nom":"Jusvouze","prenom":"Véronique","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":126,"nom":"Neskivet","prenom":"Paul","appartenance":"Comptoir Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":127,"nom":"Dulvande","prenom":"Samuel","appartenance":"Verrerie Pravette","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":128,"nom":"Bralmault","prenom":"Antoine","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":129,"nom":"Abrevière","prenom":"Émilie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":130,"nom":"Selveval","prenom":"Lucie","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":131,"nom":"Tolvichon","prenom":"Olivier","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":132,"nom":"Hulvard","prenom":"Fatima","appartenance":"Ateliers Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":133,"nom":"Ivreux","prenom":"Alexandre","appartenance":"Tannerie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":134,"nom":"Korveval","prenom":"Félix","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":135,"nom":"Urvichon","prenom":"Karim","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":136,"nom":"Dulvichon","prenom":"Émilie","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":137,"nom":"Tolvouze","prenom":"Luc","appartenance":"Fabrique Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":138,"nom":"Jusvivet","prenom":"Claire","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":139,"nom":"Quilmault","prenom":"Amina","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":140,"nom":"Ivrouze","prenom":"François","appartenance":"Fonderie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":141,"nom":"Gorzumet","prenom":"Hélène","appartenance":"Fonderie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":142,"nom":"Ivrault","prenom":"Sophie","appartenance":"Coopérative Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":143,"nom":"Bralmière","prenom":"Louise","appartenance":"Tannerie Quimvaux","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":144,"nom":"Korvande","prenom":"Nicolas","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":145,"nom":"Ivrumet","prenom":"Geneviève","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":146,"nom":"Obresson","prenom":"Geneviève","appartenance":"Coopérative Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":147,"nom":"Obrançon","prenom":"Guillaume","appartenance":"Papeterie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":148,"nom":"Urvignac","prenom":"Fatima","appartenance":"Ateliers Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":149,"nom":"Bralmotte","prenom":"Claire","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":150,"nom":"Hulvignac","prenom":"Félix","appartenance":"Imprimerie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":151,"nom":"Clovotte","prenom":"François","appartenance":"Coopérative Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":152,"nom":"Dulvoubre","prenom":"Philippe","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":153,"nom":"Rozveron","prenom":"Samuel","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":154,"nom":"Hulvoubre","prenom":"Pierre","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":155,"nom":"Urvouze","prenom":"Émilie","appartenance":"Comptoir Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":156,"nom":"Korvançon","prenom":"Félix","appartenance":"Fonderie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":157,"nom":"Neskande","prenom":"Émilie","appartenance":"Coopérative Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":158,"nom":"Mirvumet","prenom":"Chantal","appartenance":"Menuiserie Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":159,"nom":"Korvotte","prenom":"Alexandre","appartenance":"Menuiserie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":160,"nom":"Eskeval","prenom":"Zoé","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":161,"nom":"Neskard","prenom":"Camille","appartenance":"Menuiserie Gravelune","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":162,"nom":"Tolveron","prenom":"Manon","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":163,"nom":"Voskoubre","prenom":"Nathalie","appartenance":"Fonderie Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":164,"nom":"Pruvière","prenom":"Sophie","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":165,"nom":"Obrichon","prenom":"Paul","appartenance":"Ateliers Rossolde","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":166,"nom":"Abrevançon","prenom":"Fatima","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":167,"nom":"Eskière","prenom":"Nathalie","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":168,"nom":"Ivreron","prenom":"Caroline","appartenance":"Imprimerie Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":169,"nom":"Neskoubre","prenom":"Véronique","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":170,"nom":"Urvivet","prenom":"Anne","appartenance":"Fonderie Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":171,"nom":"Hulvançon","prenom":"Olivier","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":172,"nom":"Clovançon","prenom":"Karim","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":173,"nom":"Quilmotte","prenom":"Zoé","appartenance":"Ateliers Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":174,"nom":"Eskouze","prenom":"Félix","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":175,"nom":"Rozvichon","prenom":"Amina","appartenance":"Verrerie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":176,"nom":"Fralvière","prenom":"Simon","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":177,"nom":"Vandergrolcquesmirtavelle-Ostrovaubelindigues","prenom":"Marc","appartenance":"Tannerie Aubrenc","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":178,"nom":"Hulvière","prenom":"Pierre","appartenance":"Papeterie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":179,"nom":"Jusvault","prenom":"Raphaël","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":180,"nom":"Dulvivet","prenom":"Guillaume","appartenance":"Filature Olbrecq","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":181,"nom":"Fralvoubre","prenom":"Pierre","appartenance":"Imprimerie Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":182,"nom":"Selveux","prenom":"Hélène","appartenance":"Brasserie Lavorneau","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":183,"nom":"Rozvançon","prenom":"Caroline","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":184,"nom":"Korvière","prenom":"Olivier","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":185,"nom":"Ivrichon","prenom":"Nicolas","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":186,"nom":"Korvumet","prenom":"Félix","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":187,"nom":"Voskande","prenom":"Félix","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":188,"nom":"Selvumet","prenom":"Guillaume","appartenance":"Papeterie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":189,"nom":"Urvotte","prenom":"Michel","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":190,"nom":"Eskault","prenom":"Émilie","appartenance":"Coopérative Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":191,"nom":"Bralmeval","prenom":"Hélène","appartenance":"Brasserie Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":192,"nom":"Hulvesson","prenom":"Guillaume","appartenance":"Comptoir Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":193,"nom":"Jusvoubre","prenom":"Caroline","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":194,"nom":"Luskeux","prenom":"Félix","appartenance":"Tannerie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":195,"nom":"Pruvouze","prenom":"Antoine","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":196,"nom":"Quilmignac","prenom":"Julie","appartenance":"Menuiserie Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":197,"nom":"Tolvançon","prenom":"Étienne","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":198,"nom":"Voskumet","prenom":"Hugo","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":199,"nom":"Quilmumet","prenom":"Manon","appartenance":"Fonderie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":200,"nom":"Voskouze","prenom":"Marc","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":201,"nom":"Abrevard","prenom":"Samuel","appartenance":"Filature Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":202,"nom":"Luskichon","prenom":"Guillaume","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":203,"nom":"Clovesson","prenom":"Manon","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":204,"nom":"Hulvumet","prenom":"Simon","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":205,"nom":"Voskeron","prenom":"Lucie","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":206,"nom":"Selvignac","prenom":"Thomas","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":207,"nom":"Neskeron","prenom":"Jean","appartenance":"Filature Zelvaine","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":208,"nom":"Pruvivet","prenom":"Manon","appartenance":"Menuiserie Aubrenc","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":209,"nom":"Clovard","prenom":"Sophie","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":210,"nom":"Gorzeval","prenom":"Étienne","appartenance":"Papeterie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":211,"nom":"Ivroubre","prenom":"Philippe","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":212,"nom":"Luskault","prenom":"Gabriel","appartenance":"Verrerie Gravelune","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":213,"nom":"Korvard","prenom":"Émilie","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":214,"nom":"Quilmichon","prenom":"Thomas","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":215,"nom":"Eskançon","prenom":"Alexandre","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":216,"nom":"Luskançon","prenom":"Émilie","appartenance":"Ateliers Tavrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":217,"nom":"Dulvault","prenom":"Marc","appartenance":"Verrerie Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":218,"nom":"Rozveval","prenom":"Hélène","appartenance":"Comptoir Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":219,"nom":"Pruvançon","prenom":"Julie","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":220,"nom":"Mirveron","prenom":"Louis","appartenance":"Filature Wassorel","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":221,"nom":"Eskard","prenom":"François","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":222,"nom":"Bralmouze","prenom":"Hugo","appartenance":"Tannerie Falvigne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":223,"nom":"Ivrançon","prenom":"Louis","appartenance":"Coopérative Morvalle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":224,"nom":"Clovivet","prenom":"Karim","appartenance":"Fonderie Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":225,"nom":"Rozvande","prenom":"Léa","appartenance":"Verrerie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":226,"nom":"Hulvouze","prenom":"Amina","appartenance":"Coopérative Dravenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":227,"nom":"Eskesson","prenom":"Étienne","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":228,"nom":"Abreveval","prenom":"Philippe","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":229,"nom":"Quilmeux","prenom":"Émilie","appartenance":"Ateliers Falvigne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":230,"nom":"Ivreval","prenom":"Gabriel","appartenance":"Verrerie Vaubrenne","courriel":null,"titrePressenti":"animation","notes":null,"exclu":false}, + {"id":231,"nom":"Voskotte","prenom":"Simon","appartenance":"Fabrique Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":232,"nom":"Obrignac","prenom":"Chantal","appartenance":"Imprimerie Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":233,"nom":"Eskotte","prenom":"Guillaume","appartenance":"Verrerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":234,"nom":"Obrard","prenom":"Léa","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":235,"nom":"Gorzivet","prenom":"Guillaume","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":236,"nom":"Mirvançon","prenom":"Geneviève","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":237,"nom":"Selvotte","prenom":"Julie","appartenance":"Comptoir Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":238,"nom":"Mirvesson","prenom":"Amina","appartenance":"Brasserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":239,"nom":"Luskignac","prenom":"Isabelle","appartenance":"Papeterie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":240,"nom":"Obrumet","prenom":"Chloé","appartenance":"Comptoir Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":241,"nom":"Abrevesson","prenom":"Anne","appartenance":"Fonderie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":242,"nom":"Rozvumet","prenom":"Fatima","appartenance":"Menuiserie Hostrenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":243,"nom":"Mirvoubre","prenom":"Guillaume","appartenance":"Comptoir Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":244,"nom":"Rozvoubre","prenom":"Véronique","appartenance":"Fonderie Xandrelle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":245,"nom":"Gorzotte","prenom":"Claire","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":246,"nom":"Ivrière","prenom":"Antoine","appartenance":"Fabrique Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":247,"nom":"Selvichon","prenom":"Isabelle","appartenance":"Tannerie Nervanche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":248,"nom":"Urvault","prenom":"Isabelle","appartenance":"Comptoir Yzorne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":249,"nom":"Voskesson","prenom":"Julie","appartenance":"Brasserie Belvarre","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":250,"nom":"Abrevotte","prenom":"Chantal","appartenance":"Ateliers Ivoranche","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":251,"nom":"Neskotte","prenom":"Amina","appartenance":"Fabrique Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":252,"nom":"Voskançon","prenom":"Étienne","appartenance":"Menuiserie Olbrecq","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":253,"nom":"Fralveron","prenom":"Zoé","appartenance":"Menuiserie Sorbagne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":254,"nom":"Rozvouze","prenom":"Marc","appartenance":"Ateliers Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":255,"nom":"Abreveux","prenom":"Sylvie","appartenance":"Comptoir Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":256,"nom":"Pruvignac","prenom":"Karim","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":257,"nom":"Mirveval","prenom":"Zoé","appartenance":"Fabrique Kalvenne","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":258,"nom":"Fralvignac","prenom":"Philippe","appartenance":"Menuiserie Estrolle","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":259,"nom":"Gorzeux","prenom":"Gabriel","appartenance":"Menuiserie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":260,"nom":"Urveval","prenom":"Jean","appartenance":"Menuiserie Quimvaux","courriel":null,"titrePressenti":null,"notes":null,"exclu":false} + ], + "tables": [ + {"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}}, + {"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}}, + {"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":600,"y":0}}, + {"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":900,"y":0}}, + {"id":5,"numero":5,"sieges":null,"forme":"ronde","position":{"x":1200,"y":0}}, + {"id":6,"numero":6,"sieges":null,"forme":"ronde","position":{"x":1500,"y":0}}, + {"id":7,"numero":7,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}}, + {"id":8,"numero":8,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}}, + {"id":9,"numero":9,"sieges":null,"forme":"ronde","position":{"x":600,"y":300}}, + {"id":10,"numero":10,"sieges":null,"forme":"ronde","position":{"x":900,"y":300}}, + {"id":11,"numero":11,"sieges":null,"forme":"ronde","position":{"x":1200,"y":300}}, + {"id":12,"numero":12,"sieges":null,"forme":"ronde","position":{"x":1500,"y":300}}, + {"id":13,"numero":13,"sieges":null,"forme":"ronde","position":{"x":0,"y":600}}, + {"id":14,"numero":14,"sieges":null,"forme":"ronde","position":{"x":300,"y":600}}, + {"id":15,"numero":15,"sieges":null,"forme":"ronde","position":{"x":600,"y":600}}, + {"id":16,"numero":16,"sieges":null,"forme":"ronde","position":{"x":900,"y":600}}, + {"id":17,"numero":17,"sieges":null,"forme":"ronde","position":{"x":1200,"y":600}}, + {"id":18,"numero":18,"sieges":null,"forme":"ronde","position":{"x":1500,"y":600}}, + {"id":19,"numero":19,"sieges":null,"forme":"ronde","position":{"x":0,"y":900}}, + {"id":20,"numero":20,"sieges":null,"forme":"ronde","position":{"x":300,"y":900}}, + {"id":21,"numero":21,"sieges":null,"forme":"ronde","position":{"x":600,"y":900}}, + {"id":22,"numero":22,"sieges":null,"forme":"ronde","position":{"x":900,"y":900}}, + {"id":23,"numero":23,"sieges":null,"forme":"ronde","position":{"x":1200,"y":900}}, + {"id":24,"numero":24,"sieges":null,"forme":"ronde","position":{"x":1500,"y":900}}, + {"id":25,"numero":25,"sieges":null,"forme":"ronde","position":{"x":0,"y":1200}}, + {"id":26,"numero":26,"sieges":null,"forme":"ronde","position":{"x":300,"y":1200}}, + {"id":27,"numero":27,"sieges":null,"forme":"ronde","position":{"x":600,"y":1200}}, + {"id":28,"numero":28,"sieges":null,"forme":"ronde","position":{"x":900,"y":1200}}, + {"id":29,"numero":29,"sieges":null,"forme":"ronde","position":{"x":1200,"y":1200}}, + {"id":30,"numero":30,"sieges":7,"forme":"ronde","position":{"x":1500,"y":1200}}, + {"id":31,"numero":31,"sieges":7,"forme":"ronde","position":{"x":0,"y":1500}}, + {"id":32,"numero":32,"sieges":7,"forme":"ronde","position":{"x":300,"y":1500}}, + {"id":33,"numero":33,"sieges":7,"forme":"ronde","position":{"x":600,"y":1500}} + ], + "reservations": [ + {"participant":2,"table":17,"siege":1,"portee":"tous","tour":null}, + {"participant":8,"table":22,"siege":1,"portee":"tous","tour":null}, + {"participant":18,"table":6,"siege":1,"portee":"tous","tour":null}, + {"participant":34,"table":3,"siege":1,"portee":"tous","tour":null}, + {"participant":48,"table":15,"siege":1,"portee":"tous","tour":null}, + {"participant":49,"table":32,"siege":1,"portee":"tous","tour":null}, + {"participant":54,"table":26,"siege":1,"portee":"tous","tour":null}, + {"participant":55,"table":1,"siege":1,"portee":"tous","tour":null}, + {"participant":62,"table":25,"siege":1,"portee":"tous","tour":null}, + {"participant":74,"table":31,"siege":1,"portee":"tous","tour":null}, + {"participant":76,"table":30,"siege":1,"portee":"tous","tour":null}, + {"participant":90,"table":8,"siege":1,"portee":"tous","tour":null}, + {"participant":93,"table":21,"siege":1,"portee":"tous","tour":null}, + {"participant":96,"table":14,"siege":1,"portee":"tous","tour":null}, + {"participant":97,"table":27,"siege":1,"portee":"tous","tour":null}, + {"participant":99,"table":33,"siege":1,"portee":"tous","tour":null}, + {"participant":106,"table":12,"siege":1,"portee":"tous","tour":null}, + {"participant":115,"table":24,"siege":1,"portee":"tous","tour":null}, + {"participant":128,"table":4,"siege":1,"portee":"tous","tour":null}, + {"participant":129,"table":29,"siege":1,"portee":"tous","tour":null}, + {"participant":138,"table":20,"siege":1,"portee":"tous","tour":null}, + {"participant":161,"table":16,"siege":1,"portee":"tous","tour":null}, + {"participant":162,"table":5,"siege":1,"portee":"tous","tour":null}, + {"participant":166,"table":13,"siege":1,"portee":"tous","tour":null}, + {"participant":177,"table":10,"siege":1,"portee":"tous","tour":null}, + {"participant":180,"table":18,"siege":1,"portee":"tous","tour":null}, + {"participant":187,"table":7,"siege":1,"portee":"tous","tour":null}, + {"participant":197,"table":11,"siege":1,"portee":"tous","tour":null}, + {"participant":202,"table":2,"siege":1,"portee":"tous","tour":null}, + {"participant":206,"table":28,"siege":1,"portee":"tous","tour":null}, + {"participant":211,"table":19,"siege":1,"portee":"tous","tour":null}, + {"participant":222,"table":23,"siege":1,"portee":"tous","tour":null}, + {"participant":230,"table":9,"siege":1,"portee":"tous","tour":null} + ], + "titres": [ + {"table":1,"siege":1,"libelle":"animation"}, + {"table":2,"siege":1,"libelle":"animation"}, + {"table":3,"siege":1,"libelle":"animation"}, + {"table":4,"siege":1,"libelle":"animation"}, + {"table":5,"siege":1,"libelle":"animation"}, + {"table":6,"siege":1,"libelle":"animation"}, + {"table":7,"siege":1,"libelle":"animation"}, + {"table":8,"siege":1,"libelle":"animation"}, + {"table":9,"siege":1,"libelle":"animation"}, + {"table":10,"siege":1,"libelle":"animation"}, + {"table":11,"siege":1,"libelle":"animation"}, + {"table":12,"siege":1,"libelle":"animation"}, + {"table":13,"siege":1,"libelle":"animation"}, + {"table":14,"siege":1,"libelle":"animation"}, + {"table":15,"siege":1,"libelle":"animation"}, + {"table":16,"siege":1,"libelle":"animation"}, + {"table":17,"siege":1,"libelle":"animation"}, + {"table":18,"siege":1,"libelle":"animation"}, + {"table":19,"siege":1,"libelle":"animation"}, + {"table":20,"siege":1,"libelle":"animation"}, + {"table":21,"siege":1,"libelle":"animation"}, + {"table":22,"siege":1,"libelle":"animation"}, + {"table":23,"siege":1,"libelle":"animation"}, + {"table":24,"siege":1,"libelle":"animation"}, + {"table":25,"siege":1,"libelle":"animation"}, + {"table":26,"siege":1,"libelle":"animation"}, + {"table":27,"siege":1,"libelle":"animation"}, + {"table":28,"siege":1,"libelle":"animation"}, + {"table":29,"siege":1,"libelle":"animation"}, + {"table":30,"siege":1,"libelle":"animation"}, + {"table":31,"siege":1,"libelle":"animation"}, + {"table":32,"siege":1,"libelle":"animation"}, + {"table":33,"siege":1,"libelle":"animation"} + ], + "propositions": [], + "retenu": null + } +} diff --git a/src/demo/livrees/petite-conflit.gtt.json b/src/demo/livrees/petite-conflit.gtt.json new file mode 100644 index 0000000..9c320c2 --- /dev/null +++ b/src/demo/livrees/petite-conflit.gtt.json @@ -0,0 +1,32 @@ +{ + "entete": {"format":1,"produitVersion":"2026.10.05.01","revision":1,"comptes":{"participants":12,"tables":4,"reservations":0,"titres":0,"propositions":0,"retenu":0}}, + "charge": { + "evenement": {"id":"demo:petite-conflit","nom":"Petite démonstration, conflit inévitable","date":null,"siegesParDefaut":3,"tours":4,"unite":"cm","etat":"brouillon","filiation":null}, + "reglages": {"separerAppartenances":true,"nouveauxVoisins":true,"nouvelleTable":true,"varierAppartenances":true,"attribuerSieges":false,"generation":{"nombre":5,"arret":200000,"historique":1000}}, + "prochainsIds": {"participant":13,"table":5,"proposition":1}, + "participants": [ + {"id":1,"nom":"Bralmumet","prenom":"Caroline","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":2,"nom":"Neskault","prenom":"Hugo","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":3,"nom":"Ivrotte","prenom":"Youssef","appartenance":"Verrerie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":4,"nom":"Selvière","prenom":"Louis","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":5,"nom":"Selvançon","prenom":"Youssef","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":6,"nom":"Quilmoubre","prenom":"Amina","appartenance":"Verrerie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":7,"nom":"Voskotte","prenom":"Julie","appartenance":"Verrerie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":8,"nom":"Neskivet","prenom":"Paul","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":9,"nom":"Bralmignac","prenom":"Michel","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":10,"nom":"Tolveval","prenom":"Zoé","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":11,"nom":"Urvard","prenom":"Luc","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":12,"nom":"Mirveval","prenom":"Véronique","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false} + ], + "tables": [ + {"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}}, + {"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}}, + {"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}}, + {"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}} + ], + "reservations": [], + "titres": [], + "propositions": [], + "retenu": null + } +} diff --git a/src/demo/livrees/petite.gtt.json b/src/demo/livrees/petite.gtt.json new file mode 100644 index 0000000..b5aa42b --- /dev/null +++ b/src/demo/livrees/petite.gtt.json @@ -0,0 +1,32 @@ +{ + "entete": {"format":1,"produitVersion":"2026.10.05.01","revision":1,"comptes":{"participants":12,"tables":4,"reservations":0,"titres":0,"propositions":0,"retenu":0}}, + "charge": { + "evenement": {"id":"demo:petite","nom":"Petite démonstration","date":null,"siegesParDefaut":3,"tours":4,"unite":"cm","etat":"brouillon","filiation":null}, + "reglages": {"separerAppartenances":true,"nouveauxVoisins":true,"nouvelleTable":true,"varierAppartenances":true,"attribuerSieges":false,"generation":{"nombre":5,"arret":200000,"historique":1000}}, + "prochainsIds": {"participant":13,"table":5,"proposition":1}, + "participants": [ + {"id":1,"nom":"Bralmumet","prenom":"Caroline","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":2,"nom":"Neskault","prenom":"Hugo","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":3,"nom":"Ivrotte","prenom":"Youssef","appartenance":"Verrerie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":4,"nom":"Selvière","prenom":"Louis","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":5,"nom":"Selvançon","prenom":"Youssef","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":6,"nom":"Quilmoubre","prenom":"Amina","appartenance":"Verrerie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":7,"nom":"Voskotte","prenom":"Julie","appartenance":"Verrerie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":8,"nom":"Neskivet","prenom":"Paul","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":9,"nom":"Bralmignac","prenom":"Michel","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":10,"nom":"Tolveval","prenom":"Zoé","appartenance":"Verrerie Ulvardes","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":11,"nom":"Urvard","prenom":"Luc","appartenance":"Tannerie Cassorgue","courriel":null,"titrePressenti":null,"notes":null,"exclu":false}, + {"id":12,"nom":"Mirveval","prenom":"Véronique","appartenance":"Ateliers Jolvane","courriel":null,"titrePressenti":null,"notes":null,"exclu":false} + ], + "tables": [ + {"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}}, + {"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}}, + {"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}}, + {"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}} + ], + "reservations": [], + "titres": [], + "propositions": [], + "retenu": null + } +} diff --git a/src/demo/noms.js b/src/demo/noms.js index b35df84..d697a19 100644 --- a/src/demo/noms.js +++ b/src/demo/noms.js @@ -8,7 +8,10 @@ // un patronyme distinct, chaque appartenance un libellé distinct, sans boucle // de rattrapage. Les fragments sont forgés pour ne désigner personne ; // catalogue.long.test.js refuse qu'un patronyme complet ou une racine -// apparaisse ailleurs dans le projet. +// apparaisse ailleurs dans le projet, hors des fichiers que +// scripts/engendrer_livrables.js engendre de ces listes et qui les portent +// par construction : les démonstrations livrées et les deux CSV de +// démonstration (§ 15.6). // // L'ordre de chaque liste fait partie du contrat des tirages (§ 15.5, // point 3) : un tirage désigne un rang, et changer un ordre change les