[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves des placements du fichier d'état (§ 8.9, § 9, § 4.4, § 5.7) : la
// conversion entre la proposition du moteur et la forme positionnelle, la
// numérotation des générations qui s'accumulent, le contrôle de cohérence
// sur un fichier abîmé, aux capacités égales ou non, ce que ce contrôle
// n'attrape pas, la dérive d'une proposition sur une configuration changée,
// le partage d'une charge lue entre propositions gardées et écartées, sous
// un compteur relevé au-delà de chaque identifiant lu, la forme nommée, et
// des résultats qui ne dépendent que des arguments. Les charges d'épreuve
// reprennent la petite démonstration (§ 15.3) et son plan parfait ; leurs
// noms sont inventés.
import assert from 'node:assert/strict' ;
import { describe , test } from '../../test/lanceur.js' ;
import { CATALOGUE , PLAN _PARFAIT _PETITE } from '../demo/catalogue.js' ;
import { indexerPlan , normaliser } from '../moteur/configuration.js' ;
import { ErreurConfiguration } from '../moteur/erreurs.js' ;
import { HISTORIQUE _PAR _DEFAUT , rechercher , regenerer } from '../moteur/recherche.js' ;
import { VERSION } from '../version.genere.js' ;
import { serialiser } from './canonique.js' ;
import { analyser , configurationDepuisCharge , creerCharge } from './document.js' ;
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
import { ErreurStockage } from './erreurs.js' ;
[ADD] quality page base: Qualité tab, detail, charts, course companion
The quality page gets its section, its data and its building blocks.
The engine details a placement (percentages floored, worst case as a
count, three table-mix counts with their orders, per-round floors); a
Qualité tab, loaded as its own chunk, follows the proposition the plan
shows; charts and their twin tables share one component set. The search
course goes to a companion file written only when propositions change,
keyed by proposition identity, timestamp and placement fingerprint, so a
gesture costs what it did and a stale course never reaches the page.
Checked: node 4929, navigateur 1016, node-long 128; main chunk 689 ko.
--- FR ---
[ADD] base de la qualité : onglet, détail, graphiques, compagnon
La page de qualité reçoit sa section, ses données et ses briques. Le
moteur détaille un placement (pourcentages arrondis vers le bas, pire cas
en compte, trois comptes du mélange par table et leurs ordres, planchers
par tour) ; un onglet Qualité, chargé à part, suit la proposition que
montre le plan ; graphiques et tableaux jumeaux partagent leurs
composants. La course de la recherche va dans un fichier compagnon écrit
quand les propositions changent, clé : identité, horodatage, empreinte du
placement ; un geste coûte ce qu'il coûtait, aucune course périmée.
Vérifié : node 4929, navigateur 1016, longues 128 ; entrée à 689 ko.
Assisted-by: Claude Opus 5.5
2026-10-12 07:42:34 -04:00
import {
courseDepuisFichier ,
courseVersFichier ,
derive ,
examiner ,
fauteDeCourse ,
fautes ,
formeNommee ,
planDepuisFichier ,
versFichier ,
} from './placements.js' ;
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
const ENTETE = Object . freeze ( { revision : 1 , produitVersion : VERSION . affichee } ) ;
// Fige une valeur à toute profondeur et la rend : une fonction qui
// écrirait dans ce qu'elle reçoit lève alors TypeError.
function geler ( valeur ) {
if ( typeof valeur === 'object' && valeur !== null ) {
for ( const enfant of Object . values ( valeur ) ) geler ( enfant ) ;
Object . freeze ( valeur ) ;
}
return valeur ;
}
// La configuration de la petite démonstration (§ 15.3), neuve à chaque
// appel : 12 personnes, 4 tables de 3 sièges, 4 tours, aucune réservation.
const petite = ( ) => CATALOGUE . find ( ( { cle } ) => cle === 'petite' ) . construire ( ) ;
// Trois propositions du moteur sur la petite démonstration, figées.
const GENERATION = geler ( rechercher ( petite ( ) , { graine : 5 , arret : 2_000 , nombre : 3 } ) ) ;
// Le plan parfait de la petite démonstration, en proposition du moteur.
const PARFAITE = Object . freeze ( { id : 1 , graine : 48_271 , arret : 20_000 , historique : 1_000 , plan : PLAN _PARFAIT _PETITE } ) ;
// La forme positionnelle attendue de PARFAITE, sièges non attribués, écrite
// à la main : une copie neuve à chaque appel.
function fichierParfait ( ) {
return {
id : 1 ,
graine : 48_271 ,
arret : 20_000 ,
historique : 1_000 ,
produitVersion : VERSION . affichee ,
siegesAttribues : false ,
tables : [ 1 , 2 , 3 , 4 ] ,
capacites : [ 3 , 3 , 3 , 3 ] ,
tours : 4 ,
participants : [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ] ,
placement : [
{ sieges : [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] , reserve : [ ] } ,
{ sieges : [ [ 6 , 9 , 12 ] , [ 3 , 8 , 11 ] , [ 2 , 5 , 10 ] , [ 1 , 4 , 7 ] ] , reserve : [ ] } ,
{ sieges : [ [ 4 , 8 , 10 ] , [ 2 , 7 , 12 ] , [ 1 , 6 , 11 ] , [ 3 , 5 , 9 ] ] , reserve : [ ] } ,
{ sieges : [ [ 5 , 7 , 11 ] , [ 1 , 9 , 10 ] , [ 3 , 4 , 12 ] , [ 2 , 6 , 8 ] ] , reserve : [ ] } ,
] ,
} ;
}
// fichierParfait, cohérent lui aussi, ses tables déclarées dans l'ordre 3,
// 1, 2, 4, chaque tour portant ses listes dans cet ordre, et ses
// participants déclarés à rebours : un ordre de sortie qui suivrait celui
// de la déclaration s'y voit.
function fichierPermute ( ) {
const p = fichierParfait ( ) ;
const ordre = [ 2 , 0 , 1 , 3 ] ;
p . tables = ordre . map ( ( rang ) => p . tables [ rang ] ) ;
p . capacites = ordre . map ( ( rang ) => p . capacites [ rang ] ) ;
for ( const tour of p . placement ) tour . sieges = ordre . map ( ( rang ) => tour . sieges [ rang ] ) ;
p . participants . reverse ( ) ;
return p ;
}
// fichierParfait, cohérent lui aussi, dont le tour 1 laisse les personnes 6
// et 3 à la réserve, écrite dans cet ordre, hors de l'ordre croissant.
function fichierAReserve ( ) {
const p = fichierParfait ( ) ;
p . placement [ 0 ] = { sieges : [ [ 1 , 2 ] , [ 4 , 5 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] , reserve : [ 6 , 3 ] } ;
return p ;
}
// fichierParfait, cohérent lui aussi, la table 4 portée à quatre sièges et
// déclarée la première : tables 4, 1, 2, 3, capacités 4, 3, 3, 3. Au tour 1,
// la table 3 cède la personne 9 à la table 4, pleine à sa propre capacité,
// au-delà de celle de ses voisines. Une capacité lue au rang de
// l'identifiant moins un, et non au rang de la déclaration, donnerait 3 à la
// table 4 et 4 à la table 1.
function fichierInegal ( ) {
const p = fichierParfait ( ) ;
const ordre = [ 3 , 0 , 1 , 2 ] ;
p . tables = ordre . map ( ( rang ) => p . tables [ rang ] ) ;
p . capacites = [ 4 , 3 , 3 , 3 ] ;
for ( const tour of p . placement ) tour . sieges = ordre . map ( ( rang ) => tour . sieges [ rang ] ) ;
p . placement [ 0 ] . sieges = [ [ 9 , 10 , 11 , 12 ] , [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 ] ] ;
return p ;
}
// Noms et prénoms inventés des personnes 1 à 12 de la charge d'épreuve, puis
// ceux de la personne 13, que des épreuves ajoutent.
const PERSONNES = Object . freeze ( [
[ 'Aubépine' , 'Agathe' ] ,
[ 'Bruyère' , 'Basile' ] ,
[ 'Chardon' , 'Céleste' ] ,
[ 'Doucette' , 'Désiré' ] ,
[ 'Églantine' , 'Edmée' ] ,
[ 'Fougère' , 'Firmin' ] ,
[ 'Giroflée' , 'Gisèle' ] ,
[ 'Houblon' , 'Honoré' ] ,
[ 'Isatis' , 'Ida' ] ,
[ 'Jonquille' , 'Joachim' ] ,
[ 'Lavande' , 'Lazare' ] ,
[ 'Mélisse' , 'Mathurin' ] ,
] ) ;
const TREIZIEME = Object . freeze ( [ 'Nigelle' , 'Noémie' ] ) ;
// Appartenances inventées, données aux groupes de la démonstration dans
// l'ordre où ils apparaissent, par identifiant croissant.
const CERCLES = Object . freeze ( [ 'Cercle Aulne' , 'Cercle Bouleau' , 'Cercle Charme' ] ) ;
const personne = ( id , nom , prenom , appartenance = null ) => ( {
id ,
nom ,
prenom ,
appartenance ,
courriel : null ,
titrePressenti : null ,
notes : null ,
exclu : false ,
} ) ;
const table = ( id , numero , sieges = null ) => ( { id , numero , sieges , forme : 'ronde' , position : { x : 300 * id , y : 0 } } ) ;
// La charge de la petite démonstration, sans proposition : ses personnes
// sous des noms inventés et leurs groupes, ses tables aux sièges du défaut,
// 3, ses 4 tours.
function chargePetite ( ) {
const configuration = petite ( ) ;
const charge = creerCharge ( { id : 'evt-pinsons' , nom : 'Soirée des Pinsons' , siegesParDefaut : 3 , tours : 4 } ) ;
const cercleDe = new Map ( ) ;
charge . participants = configuration . participants . map ( ( { id , appartenance } ) => {
if ( ! cercleDe . has ( appartenance ) ) cercleDe . set ( appartenance , CERCLES [ cercleDe . size ] ) ;
const [ nom , prenom ] = PERSONNES [ id - 1 ] ;
return personne ( id , nom , prenom , cercleDe . get ( appartenance ) ) ;
} ) ;
charge . tables = configuration . tables . map ( ( { id , numero } ) => table ( id , numero ) ) ;
charge . prochainsIds = { participant : 13 , table : 5 , proposition : 1 } ;
return charge ;
}
// chargePetite avec les propositions de GENERATION, converties sans
// attribution de sièges sur sa propre configuration et numérotées à partir
// de prochainsIds.proposition, qui avance au-delà de la dernière.
function chargeAvecGeneration ( ) {
const charge = chargePetite ( ) ;
const configuration = configurationDepuisCharge ( charge ) ;
const decalage = charge . prochainsIds . proposition - 1 ;
charge . propositions = GENERATION . map ( ( proposition ) =>
versFichier ( proposition , configuration , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage } ) ,
) ;
charge . prochainsIds . proposition = decalage + GENERATION . length + 1 ;
charge . evenement . etat = 'propose' ;
return charge ;
}
// Le retenu tiré d'une proposition de fichier : son identifiant à la clé
// proposition, une copie de son plan.
const retenuDe = ( { id , siegesAttribues , tables , capacites , tours , participants , placement } ) =>
structuredClone ( { proposition : id , siegesAttribues , tables , capacites , tours , participants , placement } ) ;
// La charge écrite dans son fichier, puis relue : ce que trouve l'ouverture.
const relire = ( charge ) => analyser ( serialiser ( charge , ENTETE ) ) . charge ;
// Le fichier de la charge, abîmé : abimer reçoit le JSON du texte écrit et
// le modifie, puis le texte qui en résulte est relu par l'analyse, qui ne
// contrôle des propositions et du retenu que leur conteneur.
function abimerFichier ( charge , abimer ) {
const ecrit = JSON . parse ( serialiser ( charge , ENTETE ) ) ;
abimer ( ecrit ) ;
return analyser ( JSON . stringify ( ecrit ) ) . charge ;
}
// Exige que appel lève TypeError, et que son message réponde à motif.
function leveTypeError ( appel , motif , message ) {
assert . throws ( appel , ( erreur ) => erreur instanceof TypeError && motif . test ( erreur . message ) , message ) ;
}
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
// Code et détails de l'ErreurStockage que lève appel, ou un texte qui dit
// l'écart : une autre erreur, aucun refus.
function refusDe ( appel ) {
try {
appel ( ) ;
} catch ( erreur ) {
if ( ! ( erreur instanceof ErreurStockage ) ) return ` ${ erreur ? . name } au lieu d'une ErreurStockage : ${ erreur ? . message } ` ;
return { code : erreur . code , details : erreur . details } ;
}
return 'aucun refus' ;
}
// Le refus d'un identifiant qui ne laisserait au compteur des propositions
// aucune valeur au-delà.
const COMPTEUR _SATURE = Object . freeze ( { code : 'COMPTEUR_SATURE' , details : Object . freeze ( { compteur : 'proposition' } ) } ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
// Une ligne attendue de la forme nommée : sans table, une ligne de la
// réserve, sauf quand enReserve dit le contraire.
const ligne = ( tour , idTable , numero , siege , participant , nom , prenom , enReserve = idTable === null ) => ( {
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
tour ,
table : idTable ,
numero ,
siege ,
participant ,
nom ,
prenom ,
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
reserve : enReserve ,
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
} ) ;
describe ( 'versFichier et planDepuisFichier : la proposition du moteur et sa forme positionnelle (§ 8.9)' , ( ) => {
test ( 'le plan parfait de la petite démonstration donne la forme positionnelle attendue, et planDepuisFichier rend le plan' , ( ) => {
const fichier = versFichier ( PARFAITE , petite ( ) , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ) ;
assert . deepStrictEqual ( fichier , fichierParfait ( ) ) ;
assert . deepStrictEqual ( planDepuisFichier ( fichier ) , PLAN _PARFAIT _PETITE ) ;
} ) ;
test ( "chaque proposition de deux générations, sous l'historique par défaut du moteur puis sous un autre : graine, arrêt, historique et produitVersion portés, les trois premiers régénèrent le plan, listes de table croissantes, le plan rendu tel quel" , ( ) => {
// GENERATION suit l'historique que le moteur prend par défaut ; la
// seconde génération en demande un autre : sur elle seule, un fichier
// qui porte le défaut au lieu de l'historique reçu se distingue, et sa
// régénération rend un autre plan (§ 8.9).
const seconde = rechercher ( petite ( ) , { graine : 5 , arret : 500 , nombre : 1 , historique : 37 } ) ;
assert . notEqual ( HISTORIQUE _PAR _DEFAUT , 37 ) ;
assert . deepStrictEqual ( seconde . map ( ( { historique } ) => historique ) , [ 37 ] ) ;
assert . equal ( GENERATION . length , 3 ) ;
for ( const proposition of [ ... GENERATION , ... seconde ] ) {
for ( const attribuerSieges of [ false , true ] ) {
const fichier = versFichier ( proposition , petite ( ) , { produitVersion : VERSION . affichee , attribuerSieges , decalage : 0 } ) ;
const { id , graine , arret , historique } = proposition ;
assert . deepStrictEqual (
[ fichier . id , fichier . graine , fichier . arret , fichier . historique , fichier . produitVersion ] ,
[ id , graine , arret , historique , VERSION . affichee ] ,
) ;
assert . equal ( fichier . siegesAttribues , attribuerSieges ) ;
assert . deepStrictEqual ( fichier . tables , [ 1 , 2 , 3 , 4 ] ) ;
assert . deepStrictEqual ( fichier . capacites , [ 3 , 3 , 3 , 3 ] ) ;
assert . equal ( fichier . tours , 4 ) ;
assert . deepStrictEqual ( fichier . participants , [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ] ) ;
assert . equal ( fichier . placement . length , 4 ) ;
for ( const { sieges , reserve } of fichier . placement ) {
for ( const liste of sieges ) assert . deepStrictEqual ( liste , [ ... liste ] . sort ( ( a , b ) => a - b ) ) ;
assert . deepStrictEqual ( reserve , [ ] ) ;
}
assert . deepStrictEqual ( fautes ( fichier ) , [ ] ) ;
assert . deepStrictEqual ( planDepuisFichier ( fichier ) , proposition . plan ) ;
assert . deepStrictEqual ( regenerer ( petite ( ) , fichier ) , proposition . plan ) ;
}
}
} ) ;
test ( "sièges non attribués : chaque liste de table se trie par identifiant croissant ; attribués : l'ordre des sièges est gardé ; la réserve se trie dans les deux cas" , ( ) => {
const configuration = petite ( ) ;
configuration . participants . push ( { id : 13 , nom : TREIZIEME [ 0 ] , prenom : TREIZIEME [ 1 ] , appartenance : null } ) ;
const inverses = PLAN _PARFAIT _PETITE . tours . map ( ( listes ) => listes . map ( ( liste ) => [ ... liste ] . reverse ( ) ) ) ;
inverses [ 0 ] [ 3 ] = [ 11 , 10 ] ;
const proposition = {
... PARFAITE ,
plan : { tables : [ 1 , 2 , 3 , 4 ] , tours : inverses , reserves : [ [ 13 , 12 ] , [ 13 ] , [ 13 ] , [ 13 ] ] } ,
} ;
const reserves = [ [ 12 , 13 ] , [ 13 ] , [ 13 ] , [ 13 ] ] ;
const nonAttribues = versFichier ( proposition , configuration , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ) ;
assert . equal ( nonAttribues . siegesAttribues , false ) ;
assert . deepStrictEqual ( nonAttribues . participants , [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ] ) ;
const tries = PLAN _PARFAIT _PETITE . tours . map ( ( listes ) => listes . map ( ( liste ) => [ ... liste ] ) ) ;
tries [ 0 ] [ 3 ] = [ 10 , 11 ] ;
assert . deepStrictEqual (
nonAttribues . placement ,
tries . map ( ( sieges , r ) => ( { sieges , reserve : reserves [ r ] } ) ) ,
) ;
const attribues = versFichier ( proposition , configuration , { produitVersion : VERSION . affichee , attribuerSieges : true , decalage : 0 } ) ;
assert . equal ( attribues . siegesAttribues , true ) ;
assert . deepStrictEqual (
attribues . placement ,
inverses . map ( ( sieges , r ) => ( { sieges , reserve : reserves [ r ] } ) ) ,
) ;
assert . deepStrictEqual ( planDepuisFichier ( attribues ) , planDepuisFichier ( nonAttribues ) ) ;
} ) ;
test ( "les tables dans l'ordre du plan, chacune avec sa capacité dans la configuration, quel que soit l'ordre où la configuration les range" , ( ) => {
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ;
const configuration = petite ( ) ;
configuration . tables [ 3 ] . capacite = 4 ;
configuration . tables . reverse ( ) ;
const [ proposition ] = rechercher ( configuration , { graine : 5 , arret : 2_000 , nombre : 1 } ) ;
assert . deepStrictEqual ( proposition . plan . tables , [ 4 , 3 , 2 , 1 ] ) ;
const engendre = versFichier ( proposition , configuration , options ) ;
assert . deepStrictEqual ( [ engendre . tables , engendre . capacites ] , [ [ 4 , 3 , 2 , 1 ] , [ 4 , 3 , 3 , 3 ] ] ) ;
assert . deepStrictEqual ( planDepuisFichier ( engendre ) , proposition . plan ) ;
const parfait = versFichier ( PARFAITE , configuration , options ) ;
assert . deepStrictEqual ( [ parfait . tables , parfait . capacites ] , [ [ 1 , 2 , 3 , 4 ] , [ 3 , 3 , 3 , 4 ] ] ) ;
assert . deepStrictEqual ( planDepuisFichier ( parfait ) , PLAN _PARFAIT _PETITE ) ;
// Les participants se déclarent par identifiant croissant, même quand
// la configuration les range à rebours.
const renversee = petite ( ) ;
renversee . participants . reverse ( ) ;
const [ tiree ] = rechercher ( renversee , { graine : 5 , arret : 2_000 , nombre : 1 } ) ;
assert . deepStrictEqual ( versFichier ( tiree , renversee , options ) . participants , [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ] ) ;
} ) ;
test ( "planDepuisFichier rend chaque réserve par identifiant croissant, comme le moteur les rend, quel que soit l'ordre écrit" , ( ) => {
const p = fichierAReserve ( ) ;
assert . deepStrictEqual ( fautes ( p ) , [ ] ) ;
assert . deepStrictEqual ( planDepuisFichier ( p ) . reserves [ 0 ] , [ 3 , 6 ] ) ;
} ) ;
test ( "versFichier ne modifie ni la proposition ni la configuration, et ne partage aucune liste avec elles ; planDepuisFichier non plus avec le fichier" , ( ) => {
const configuration = geler ( petite ( ) ) ;
for ( const proposition of GENERATION ) {
const fichier = versFichier ( proposition , configuration , { produitVersion : VERSION . affichee , attribuerSieges : true , decalage : 0 } ) ;
assert . notEqual ( fichier . tables , proposition . plan . tables ) ;
fichier . placement . forEach ( ( { sieges , reserve } , r ) => {
assert . notEqual ( reserve , proposition . plan . reserves [ r ] ) ;
sieges . forEach ( ( liste , i ) => assert . notEqual ( liste , proposition . plan . tours [ r ] [ i ] ) ) ;
} ) ;
const gele = geler ( fichier ) ;
const plan = planDepuisFichier ( gele ) ;
assert . notEqual ( plan . tables , gele . tables ) ;
plan . tours . forEach ( ( listes , r ) => {
assert . notEqual ( plan . reserves [ r ] , gele . placement [ r ] . reserve ) ;
listes . forEach ( ( liste , i ) => assert . notEqual ( liste , gele . placement [ r ] . sieges [ i ] ) ) ;
} ) ;
}
} ) ;
test ( 'versFichier refuse un drapeau, une version ou un décalage qui manquent ou sortent de leur domaine, et un plan que la configuration contredit : TypeError' , ( ) => {
// Une troisième proposition : le décalage − 2 lui donnerait l'identifiant
// 1, que le contrôle de forme admet.
const troisieme = { ... PARFAITE , id : 3 } ;
for ( const options of [
{ produitVersion : VERSION . affichee , decalage : 0 } ,
{ produitVersion : VERSION . affichee , attribuerSieges : 'non' , decalage : 0 } ,
{ attribuerSieges : false , decalage : 0 } ,
{ produitVersion : '' , attribuerSieges : false , decalage : 0 } ,
] ) {
assert . throws ( ( ) => versFichier ( troisieme , petite ( ) , options ) , TypeError , JSON . stringify ( options ) ) ;
}
// Un décalage hors de son domaine se refuse en son nom, avant toute
// forme.
for ( const decalage of [ undefined , - 2 , 0.5 , '0' ] ) {
leveTypeError (
( ) => versFichier ( troisieme , petite ( ) , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage } ) ,
/décalage/ ,
` décalage ${ JSON . stringify ( decalage ) ? ? 'absent' } ` ,
) ;
}
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ;
const avecTreize = petite ( ) ;
avecTreize . participants . push ( { id : 13 , nom : TREIZIEME [ 0 ] , appartenance : null } ) ;
leveTypeError ( ( ) => versFichier ( PARFAITE , avecTreize , options ) , /"MANQUANT"/ , 'personne 13 non placée' ) ;
const tableReduite = petite ( ) ;
tableReduite . tables [ 3 ] . capacite = 2 ;
leveTypeError ( ( ) => versFichier ( PARFAITE , tableReduite , options ) , /"LONGUEUR"/ , 'table 4 réduite' ) ;
const cinqTours = petite ( ) ;
cinqTours . tours = 5 ;
leveTypeError ( ( ) => versFichier ( PARFAITE , cinqTours , options ) , /"LONGUEUR"/ , 'cinq tours' ) ;
const sansTable = petite ( ) ;
sansTable . tables . pop ( ) ;
leveTypeError ( ( ) => versFichier ( PARFAITE , sansTable , options ) , /"FORME"/ , 'table 4 absente' ) ;
} ) ;
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
test ( "versFichier refuse sous un code l'identifiant qui ne laisserait au compteur aucune valeur au-delà, 2^31 − 1 et plus : ErreurStockage COMPTEUR_SATURE, qui nomme le compteur des propositions ; 2^31 − 2, le dernier qui lui en laisse une, s'écrit" , ( ) => {
// Une troisième proposition, que son décalage porte comme la première à
// l'identifiant 2^31 − 1.
const troisieme = { ... PARFAITE , id : 3 } ;
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ;
assert . equal ( versFichier ( PARFAITE , petite ( ) , { ... options , decalage : 2 * * 31 - 3 } ) . id , 2 * * 31 - 2 ) ;
const refus = [
[ PARFAITE , 2 * * 31 - 2 ] ,
[ troisieme , 2 * * 31 - 4 ] ,
[ PARFAITE , 2 * * 31 - 1 ] ,
] . map ( ( [ proposition , decalage ] ) => [
decalage + proposition . id ,
refusDe ( ( ) => versFichier ( proposition , petite ( ) , { ... options , decalage } ) ) ,
] ) ;
assert . deepStrictEqual ( refus , [
[ 2 * * 31 - 1 , COMPTEUR _SATURE ] ,
[ 2 * * 31 - 1 , COMPTEUR _SATURE ] ,
[ 2 * * 31 , COMPTEUR _SATURE ] ,
] ) ;
} ) ;
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
test ( "le fichier écrit se relit : examiner garde chaque proposition telle qu'écrite, sans faute ni dérive" , ( ) => {
const charge = chargeAvecGeneration ( ) ;
const examen = examiner ( relire ( charge ) ) ;
assert . deepStrictEqual ( examen . fautives , [ ] ) ;
assert . deepStrictEqual ( examen . derives , [ ] ) ;
assert . deepStrictEqual ( examen . retenu , { fautes : [ ] , raisons : [ ] } ) ;
assert . deepStrictEqual ( examen . charge . propositions , charge . propositions ) ;
examen . charge . propositions . forEach ( ( proposition , rang ) => {
assert . deepStrictEqual ( planDepuisFichier ( proposition ) , GENERATION [ rang ] . plan ) ;
} ) ;
} ) ;
test ( "des générations qui s'accumulent (§ 5.7) : chacune décalée de prochainsIds.proposition − 1, qui avance ensuite ; après un effacement qui épargne le retenu, aucun identifiant ne revient, et examiner garde chaque proposition relue" , ( ) => {
const charge = chargePetite ( ) ;
const configuration = configurationDepuisCharge ( charge ) ;
// L'appelant décale une génération de prochainsIds.proposition − 1,
// puis porte le compteur au-delà du dernier identifiant écrit.
const generer = ( graine ) => {
const decalage = charge . prochainsIds . proposition - 1 ;
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage } ;
const propositions = rechercher ( configuration , { graine , arret : 2_000 , nombre : 3 } ) ;
for ( const proposition of propositions ) charge . propositions . push ( versFichier ( proposition , configuration , options ) ) ;
charge . prochainsIds . proposition = decalage + propositions . length + 1 ;
} ;
generer ( 5 ) ;
generer ( 9 ) ;
charge . evenement . etat = 'propose' ;
assert . deepStrictEqual ( charge . propositions . map ( ( { id } ) => id ) , [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
const examen = examiner ( relire ( charge ) ) ;
assert . deepStrictEqual ( examen . fautives , [ ] ) ;
assert . deepStrictEqual ( examen . charge . propositions , charge . propositions ) ;
// Effacer la liste en épargnant le retenu (§ 5.7) garde la proposition
// 2 seule ; la génération suivante prend 7, 8 et 9, aucun des
// identifiants effacés.
charge . retenu = retenuDe ( charge . propositions [ 1 ] ) ;
charge . propositions = [ charge . propositions [ 1 ] ] ;
charge . evenement . etat = 'retenu' ;
generer ( 13 ) ;
assert . deepStrictEqual ( charge . propositions . map ( ( { id } ) => id ) , [ 2 , 7 , 8 , 9 ] ) ;
const apres = examiner ( relire ( charge ) ) ;
assert . deepStrictEqual ( apres . fautives , [ ] ) ;
assert . deepStrictEqual ( apres . charge . propositions , charge . propositions ) ;
assert . deepStrictEqual ( apres . retenu , { fautes : [ ] , raisons : [ ] } ) ;
} ) ;
} ) ;
describe ( 'fautes : la corruption délibérée (§ 8.9, points 1 à 3)' , ( ) => {
// Les quatre façons d'abîmer une proposition de la petite démonstration,
// chacune dans son tour : chaque fonction abîme la proposition reçue et
// rend la faute que le contrôle doit nommer.
const QUATRE _FACONS = [
[ 'une longueur fausse' , ( p ) => {
const { sieges } = p . placement [ 0 ] ;
sieges [ 1 ] . push ( sieges [ 0 ] . shift ( ) ) ;
return { code : 'LONGUEUR' , tour : 1 , table : 2 , declare : 3 , ecrit : 4 } ;
} ] ,
[ 'un identifiant dupliqué' , ( p ) => {
const { sieges , reserve } = p . placement [ 1 ] ;
reserve . push ( sieges [ 0 ] [ 0 ] ) ;
return { code : 'DOUBLON' , tour : 2 , participant : sieges [ 0 ] [ 0 ] } ;
} ] ,
[ 'un identifiant inconnu' , ( p ) => {
p . placement [ 2 ] . reserve . push ( 13 ) ;
return { code : 'INCONNU' , tour : 3 , participant : 13 } ;
} ] ,
[ 'un identifiant manquant sans entrée de réserve' , ( p ) => {
const perdu = p . placement [ 3 ] . sieges [ 2 ] . pop ( ) ;
return { code : 'MANQUANT' , tour : 4 , participant : perdu } ;
} ] ,
] ;
test ( 'trois propositions, la deuxième abîmée de quatre façons tour à tour : fautes nomme chacune avec ses coordonnées, examiner écarte la seule fautive et garde les deux autres' , ( ) => {
const charge = chargeAvecGeneration ( ) ;
assert . deepStrictEqual ( charge . propositions [ 1 ] . tables , [ 1 , 2 , 3 , 4 ] ) ;
for ( const [ libelle , abimer ] of QUATRE _FACONS ) {
let attendue ;
const lue = abimerFichier ( charge , ( ecrit ) => {
attendue = abimer ( ecrit . charge . propositions [ 1 ] ) ;
} ) ;
const abimee = lue . propositions [ 1 ] ;
assert . equal ( abimee . id , 2 , libelle ) ;
assert . notDeepStrictEqual ( abimee , charge . propositions [ 1 ] , libelle ) ;
assert . deepStrictEqual ( fautes ( abimee ) , [ attendue ] , libelle ) ;
const examen = examiner ( lue ) ;
assert . deepStrictEqual ( examen . fautives , [ { rang : 1 , id : 2 , fautes : [ attendue ] } ] , libelle ) ;
assert . deepStrictEqual ( examen . charge . propositions , [ charge . propositions [ 0 ] , charge . propositions [ 2 ] ] , libelle ) ;
assert . deepStrictEqual ( examen . derives , [ ] , libelle ) ;
}
} ) ;
test ( "les quatre défauts dans la même proposition sont nommés dans l'ordre des tours" , ( ) => {
const abimee = structuredClone ( chargeAvecGeneration ( ) . propositions [ 1 ] ) ;
const attendues = QUATRE _FACONS . map ( ( [ , abimer ] ) => abimer ( abimee ) ) ;
assert . deepStrictEqual ( fautes ( abimee ) , [ attendues [ 0 ] , attendues [ 1 ] , attendues [ 2 ] , attendues [ 3 ] ] ) ;
assert . deepStrictEqual ( attendues . map ( ( { tour } ) => tour ) , [ 1 , 2 , 3 , 4 ] ) ;
} ) ;
test ( "dans un tour : le nombre de listes, chaque table dans l'ordre déclaré, puis doublons, inconnus et manquants par identifiant croissant ; le nombre de tours avant tout" , ( ) => {
const p = fichierParfait ( ) ;
p . tours = 5 ;
p . placement [ 1 ] = { sieges : [ [ 6 , 9 , 12 , 3 ] , [ 8 ] , [ 2 , 10 , 14 , 9 ] , [ 1 , 4 , 7 ] , [ 20 ] ] , reserve : [ 15 , 1 , 13 , 15 ] } ;
assert . deepStrictEqual ( fautes ( p ) , [
{ code : 'LONGUEUR' , tour : null , table : null , declare : 5 , ecrit : 4 } ,
{ code : 'LONGUEUR' , tour : 2 , table : null , declare : 4 , ecrit : 5 } ,
{ code : 'LONGUEUR' , tour : 2 , table : 1 , declare : 3 , ecrit : 4 } ,
{ code : 'LONGUEUR' , tour : 2 , table : 3 , declare : 3 , ecrit : 4 } ,
{ code : 'DOUBLON' , tour : 2 , participant : 1 } ,
{ code : 'DOUBLON' , tour : 2 , participant : 9 } ,
{ code : 'INCONNU' , tour : 2 , participant : 13 } ,
{ code : 'INCONNU' , tour : 2 , participant : 14 } ,
{ code : 'INCONNU' , tour : 2 , participant : 15 } ,
{ code : 'INCONNU' , tour : 2 , participant : 20 } ,
{ code : 'MANQUANT' , tour : 2 , participant : 5 } ,
{ code : 'MANQUANT' , tour : 2 , participant : 11 } ,
] ) ;
// Plus de tours écrits que déclarés : la même faute, dans l'autre sens.
const enTrop = { ... fichierParfait ( ) , tours : 3 } ;
assert . deepStrictEqual ( fautes ( enTrop ) , [ { code : 'LONGUEUR' , tour : null , table : null , declare : 3 , ecrit : 4 } ] ) ;
} ) ;
test ( "un identifiant se nomme une fois par tour : inconnu écrit deux fois, déclaré écrit trois fois ; une liste de moins que les tables nomme aussi ses absents" , ( ) => {
const p = fichierParfait ( ) ;
p . placement [ 0 ] . reserve = [ 13 , 2 , 13 , 2 ] ;
p . placement [ 2 ] . sieges . pop ( ) ;
assert . deepStrictEqual ( fautes ( p ) , [
{ code : 'DOUBLON' , tour : 1 , participant : 2 } ,
{ code : 'INCONNU' , tour : 1 , participant : 13 } ,
{ code : 'LONGUEUR' , tour : 3 , table : null , declare : 4 , ecrit : 3 } ,
{ code : 'MANQUANT' , tour : 3 , participant : 3 } ,
{ code : 'MANQUANT' , tour : 3 , participant : 5 } ,
{ code : 'MANQUANT' , tour : 3 , participant : 9 } ,
] ) ;
} ) ;
test ( "les tables suivent l'ordre déclaré, les identifiants l'ordre croissant, quel que soit l'ordre de la déclaration des participants" , ( ) => {
const permute = fichierPermute ( ) ;
assert . deepStrictEqual ( permute . tables , [ 3 , 1 , 2 , 4 ] ) ;
assert . deepStrictEqual ( fautes ( permute ) , [ ] ) ;
const longues = fichierPermute ( ) ;
longues . placement [ 0 ] . sieges = [ [ 7 , 8 , 9 , 4 ] , [ 1 , 2 , 3 , 5 ] , [ 6 ] , [ 10 , 11 , 12 ] ] ;
assert . deepStrictEqual ( fautes ( longues ) , [
{ code : 'LONGUEUR' , tour : 1 , table : 3 , declare : 3 , ecrit : 4 } ,
{ code : 'LONGUEUR' , tour : 1 , table : 1 , declare : 3 , ecrit : 4 } ,
] ) ;
const melees = fichierPermute ( ) ;
melees . placement [ 0 ] . sieges [ 0 ] = [ 7 , 8 ] ;
melees . placement [ 0 ] . sieges [ 1 ] = [ 1 , 3 ] ;
melees . placement [ 0 ] . reserve = [ 5 , 3 ] ;
assert . deepStrictEqual ( fautes ( melees ) , [
{ code : 'DOUBLON' , tour : 1 , participant : 3 } ,
{ code : 'DOUBLON' , tour : 1 , participant : 5 } ,
{ code : 'MANQUANT' , tour : 1 , participant : 2 } ,
{ code : 'MANQUANT' , tour : 1 , participant : 9 } ,
] ) ;
} ) ;
test ( "capacités inégales, tables déclarées hors de l'ordre des identifiants : chaque liste se mesure à la capacité de son rang dans la déclaration, et la proposition se relit sans être écartée" , ( ) => {
const inegal = fichierInegal ( ) ;
assert . deepStrictEqual ( fautes ( inegal ) , [ ] ) ;
// La table 4 au-delà de ses quatre sièges, la table 1 au-delà de ses
// trois mais en deçà de quatre ; la table 3 vide n'est pas fautive.
const debordees = fichierInegal ( ) ;
debordees . placement [ 0 ] . sieges = [ [ 8 , 9 , 10 , 11 , 12 ] , [ 1 , 2 , 3 , 7 ] , [ 4 , 5 , 6 ] , [ ] ] ;
assert . deepStrictEqual ( fautes ( debordees ) , [
{ code : 'LONGUEUR' , tour : 1 , table : 4 , declare : 4 , ecrit : 5 } ,
{ code : 'LONGUEUR' , tour : 1 , table : 1 , declare : 3 , ecrit : 4 } ,
] ) ;
const charge = chargePetite ( ) ;
charge . tables [ 3 ] . sieges = 4 ;
charge . propositions = [ inegal ] ;
charge . prochainsIds . proposition = 2 ;
charge . evenement . etat = 'propose' ;
const examen = examiner ( relire ( charge ) ) ;
assert . deepStrictEqual ( examen . fautives , [ ] ) ;
assert . deepStrictEqual ( examen . derives , [ ] ) ;
assert . deepStrictEqual ( examen . charge . propositions , [ inegal ] ) ;
} ) ;
test ( "une table moins remplie que sa capacité n'est pas fautive : la longueur déclarée borne la liste, une salle peut avoir des places vides" , ( ) => {
const configuration = petite ( ) ;
configuration . participants [ 4 ] . exclu = true ;
const [ proposition ] = rechercher ( configuration , { graine : 9 , arret : 1_000 , nombre : 1 } ) ;
const fichier = versFichier ( proposition , configuration , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ) ;
assert . deepStrictEqual ( fichier . participants , [ 1 , 2 , 3 , 4 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ] ) ;
for ( const { sieges } of fichier . placement ) {
assert . deepStrictEqual ( sieges . map ( ( liste ) => liste . length ) . sort ( ( a , b ) => a - b ) , [ 2 , 3 , 3 , 3 ] ) ;
}
assert . deepStrictEqual ( fautes ( fichier ) , [ ] ) ;
const versLaReserve = fichierParfait ( ) ;
versLaReserve . placement [ 0 ] = { sieges : [ [ 1 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] , reserve : [ 2 ] } ;
assert . deepStrictEqual ( fautes ( versLaReserve ) , [ ] ) ;
} ) ;
test ( "la forme se contrôle d'abord : la première valeur hors de sa règle est nommée FORME, par son chemin dans la proposition" , ( ) => {
const cas = [
[ 'un nombre' , ( ) => 42 , '' ] ,
[ 'null' , ( ) => null , '' ] ,
[ 'une liste' , ( ) => [ ] , '' ] ,
[ 'identifiant 0' , ( p ) => { p . id = 0 ; return p ; } , 'id' ] ,
[ 'identifiant en chaîne' , ( p ) => { p . id = '1' ; return p ; } , 'id' ] ,
[ 'graine négative' , ( p ) => { p . graine = - 1 ; return p ; } , 'graine' ] ,
[ 'graine de 2^32' , ( p ) => { p . graine = 2 * * 32 ; return p ; } , 'graine' ] ,
[ 'arrêt nul' , ( p ) => { p . arret = 0 ; return p ; } , 'arret' ] ,
[ 'historique fractionnaire' , ( p ) => { p . historique = 1.5 ; return p ; } , 'historique' ] ,
[ 'version vide' , ( p ) => { p . produitVersion = '' ; return p ; } , 'produitVersion' ] ,
[ 'drapeau en chaîne' , ( p ) => { p . siegesAttribues = 'non' ; return p ; } , 'siegesAttribues' ] ,
[ 'tables en objet' , ( p ) => { p . tables = { } ; return p ; } , 'tables' ] ,
[ 'identifiant de table 0' , ( p ) => { p . tables [ 3 ] = 0 ; return p ; } , 'tables[3]' ] ,
[ "capacité d'un siège" , ( p ) => { p . capacites [ 2 ] = 1 ; return p ; } , 'capacites[2]' ] ,
[ 'zéro tour' , ( p ) => { p . tours = 0 ; return p ; } , 'tours' ] ,
[ 'participant en chaîne' , ( p ) => { p . participants [ 1 ] = '2' ; return p ; } , 'participants[1]' ] ,
[ 'placement en objet' , ( p ) => { p . placement = { } ; return p ; } , 'placement' ] ,
[ 'tour en liste' , ( p ) => { p . placement [ 0 ] = [ ] ; return p ; } , 'placement[0]' ] ,
[ 'sièges en chaîne' , ( p ) => { p . placement [ 1 ] . sieges = 'x' ; return p ; } , 'placement[1].sieges' ] ,
[ 'liste de table en nombre' , ( p ) => { p . placement [ 2 ] . sieges [ 1 ] = 7 ; return p ; } , 'placement[2].sieges[1]' ] ,
[ 'occupant fractionnaire' , ( p ) => { p . placement [ 3 ] . sieges [ 0 ] [ 2 ] = 2.5 ; return p ; } , 'placement[3].sieges[0][2]' ] ,
[ 'occupant au-delà des entiers exacts' , ( p ) => { p . placement [ 3 ] . sieges [ 1 ] [ 0 ] = 2 * * 53 ; return p ; } , 'placement[3].sieges[1][0]' ] ,
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
// Un identifiant reste sous 2^31.
[ 'identifiant 2^31' , ( p ) => { p . id = 2 * * 31 ; return p ; } , 'id' ] ,
[ 'identifiant de table 2^31' , ( p ) => { p . tables [ 3 ] = 2 * * 31 ; return p ; } , 'tables[3]' ] ,
[ 'participant déclaré 2^31' , ( p ) => { p . participants [ 11 ] = 2 * * 31 ; return p ; } , 'participants[11]' ] ,
[ 'occupant 2^31' , ( p ) => { p . placement [ 3 ] . sieges [ 1 ] [ 0 ] = 2 * * 31 ; return p ; } , 'placement[3].sieges[1][0]' ] ,
[ 'en réserve, 2^31' , ( p ) => { p . placement [ 2 ] . reserve = [ 2 * * 31 ] ; return p ; } , 'placement[2].reserve[0]' ] ,
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
[ 'réserve absente' , ( p ) => { delete p . placement [ 0 ] . reserve ; return p ; } , 'placement[0].reserve' ] ,
[ 'graine absente' , ( p ) => { delete p . graine ; return p ; } , 'graine' ] ,
[ 'deux clés inconnues : la plus petite' , ( p ) => { p . zeta = 1 ; p . alpha = 2 ; return p ; } , 'alpha' ] ,
[ "une clé inconnue qui ne s'écrit pas comme un identifiant" , ( p ) => { p [ 'clé inconnue' ] = 1 ; return p ; } , '["clé inconnue"]' ] ,
[ 'une clé inconnue dans un tour' , ( p ) => { p . placement [ 1 ] . note = 'x' ; return p ; } , 'placement[1].note' ] ,
[ 'un champ fautif avant une clé inconnue' , ( p ) => { p . aaa = 1 ; p . tours = 0 ; return p ; } , 'tours' ] ,
[ 'une table déclarée deux fois' , ( p ) => { p . tables [ 2 ] = 2 ; return p ; } , 'tables[2]' ] ,
[ 'des capacités moins nombreuses que les tables' , ( p ) => { p . capacites . pop ( ) ; return p ; } , 'capacites' ] ,
[ 'des capacités plus nombreuses que les tables' , ( p ) => { p . capacites . push ( 3 ) ; return p ; } , 'capacites' ] ,
[ 'un participant déclaré deux fois' , ( p ) => { p . participants [ 3 ] = 3 ; return p ; } , 'participants[3]' ] ,
[ 'une forme fautive dans un placement incohérent : la forme seule' , ( p ) => {
p . placement [ 0 ] . sieges [ 0 ] . pop ( ) ;
p . historique = 0 ;
return p ;
} , 'historique' ] ,
] ;
const ecarts = cas . flatMap ( ( [ libelle , abimer , chemin ] ) => {
const obtenues = fautes ( abimer ( fichierParfait ( ) ) ) ;
const attendues = [ { code : 'FORME' , chemin } ] ;
return JSON . stringify ( obtenues ) === JSON . stringify ( attendues )
? [ ]
: [ ` ${ libelle } : ${ JSON . stringify ( obtenues ) } au lieu de ${ JSON . stringify ( attendues ) } ` ] ;
} ) ;
assert . deepStrictEqual ( ecarts , [ ] ) ;
} ) ;
test ( "le retenu se contrôle sous sa propre forme : la clé proposition au lieu de l'identifiant, ni graine, ni arrêt, ni historique, ni version" , ( ) => {
const retenu = retenuDe ( fichierParfait ( ) ) ;
assert . deepStrictEqual ( fautes ( retenu ) , [ ] ) ;
assert . deepStrictEqual ( fautes ( { ... retenuDe ( fichierParfait ( ) ) , graine : 1 } ) , [ { code : 'FORME' , chemin : 'graine' } ] ) ;
const sansDrapeau = retenuDe ( fichierParfait ( ) ) ;
delete sansDrapeau . siegesAttribues ;
assert . deepStrictEqual ( fautes ( sansDrapeau ) , [ { code : 'FORME' , chemin : 'siegesAttribues' } ] ) ;
assert . deepStrictEqual ( fautes ( { ... retenuDe ( fichierParfait ( ) ) , proposition : 0 } ) , [ { code : 'FORME' , chemin : 'proposition' } ] ) ;
retenu . placement [ 1 ] . sieges [ 3 ] . pop ( ) ;
assert . deepStrictEqual ( fautes ( retenu ) , [ { code : 'MANQUANT' , tour : 2 , participant : 7 } ] ) ;
} ) ;
} ) ;
describe ( "ce que le contrôle n'attrape pas, et ne prétend pas attraper (§ 8.9, point 2)" , ( ) => {
test ( "une permutation dans une table, et l'échange du contenu de deux tables de même capacité, gardent le multiensemble et les longueurs : fautes rend [], examiner garde la proposition" , ( ) => {
for ( const siegesAttribues of [ true , false ] ) {
const reference = { ... fichierParfait ( ) , siegesAttribues } ;
const permutee = structuredClone ( reference ) ;
permutee . placement [ 0 ] . sieges [ 0 ] = [ 3 , 1 , 2 ] ;
const echangee = structuredClone ( reference ) ;
const { sieges } = echangee . placement [ 1 ] ;
[ sieges [ 0 ] , sieges [ 1 ] ] = [ sieges [ 1 ] , sieges [ 0 ] ] ;
for ( const abimee of [ permutee , echangee ] ) {
assert . notDeepStrictEqual ( abimee , reference ) ;
assert . deepStrictEqual ( fautes ( abimee ) , [ ] ) ;
const charge = chargePetite ( ) ;
charge . propositions = [ abimee ] ;
charge . prochainsIds . proposition = 2 ;
const examen = examiner ( charge ) ;
assert . deepStrictEqual ( examen . fautives , [ ] ) ;
assert . deepStrictEqual ( examen . derives , [ ] ) ;
assert . deepStrictEqual ( examen . charge . propositions , [ abimee ] ) ;
}
}
} ) ;
} ) ;
describe ( 'derive : la péremption délibérée (§ 8.9, point 3 ; § 9 ; § 4.4)' , ( ) => {
test ( 'exclure 5, supprimer la table 2, réduire la table 3, passer à 3 tours, ajouter 13 : chaque raison nommée, triées par code puis identifiant ; examiner conserve chaque proposition et le retenu' , ( ) => {
const charge = chargeAvecGeneration ( ) ;
charge . retenu = retenuDe ( charge . propositions [ 1 ] ) ;
charge . evenement . etat = 'retenu' ;
charge . participants [ 4 ] . exclu = true ;
charge . tables = charge . tables . filter ( ( { id } ) => id !== 2 ) ;
charge . tables . find ( ( { id } ) => id === 3 ) . sieges = 2 ;
charge . evenement . tours = 3 ;
charge . participants . push ( personne ( 13 , ... TREIZIEME ) ) ;
charge . prochainsIds . participant = 14 ;
const raisons = [
{ code : 'CAPACITE_CHANGEE' , table : 3 , avant : 3 , maintenant : 2 } ,
{ code : 'PARTICIPANT_EXCLU' , participant : 5 } ,
{ code : 'PARTICIPANT_NON_PLACE' , participant : 13 } ,
{ code : 'TABLE_SUPPRIMEE' , table : 2 } ,
{ code : 'TOURS_CHANGES' , avant : 4 , maintenant : 3 } ,
] ;
const lue = relire ( charge ) ;
for ( const proposition of lue . propositions ) assert . deepStrictEqual ( derive ( proposition , lue ) , raisons ) ;
const examen = examiner ( lue ) ;
assert . deepStrictEqual ( examen . fautives , [ ] ) ;
assert . deepStrictEqual ( examen . derives , [ 1 , 2 , 3 ] . map ( ( id ) => ( { id , raisons } ) ) ) ;
assert . deepStrictEqual ( examen . charge . propositions , charge . propositions ) ;
assert . deepStrictEqual ( examen . retenu , { fautes : [ ] , raisons } ) ;
assert . equal ( examen . charge . retenu , lue . retenu ) ;
} ) ;
test ( "chaque changement seul donne sa seule raison ; ce qui ne touche ni les personnes placées, ni le mobilier, ni les tours n'est pas une dérive" , ( ) => {
const cas = [
[ 'exclure la personne 5' , ( c ) => { c . participants [ 4 ] . exclu = true ; } ,
[ { code : 'PARTICIPANT_EXCLU' , participant : 5 } ] ] ,
[ 'supprimer la personne 7' , ( c ) => { c . participants . splice ( 6 , 1 ) ; } ,
[ { code : 'PARTICIPANT_SUPPRIME' , participant : 7 } ] ] ,
[ 'ajouter la personne 13' , ( c ) => { c . participants . push ( personne ( 13 , ... TREIZIEME ) ) ; } ,
[ { code : 'PARTICIPANT_NON_PLACE' , participant : 13 } ] ] ,
[ 'ajouter la personne 13, exclue' , ( c ) => { c . participants . push ( { ... personne ( 13 , ... TREIZIEME ) , exclu : true } ) ; } ,
[ ] ] ,
[ 'supprimer la table 2' , ( c ) => { c . tables . splice ( 1 , 1 ) ; } ,
[ { code : 'TABLE_SUPPRIMEE' , table : 2 } ] ] ,
[ 'ajouter la table 5' , ( c ) => { c . tables . push ( table ( 5 , 5 ) ) ; } ,
[ { code : 'TABLE_AJOUTEE' , table : 5 } ] ] ,
[ 'réduire la table 3 à deux sièges' , ( c ) => { c . tables [ 2 ] . sieges = 2 ; } ,
[ { code : 'CAPACITE_CHANGEE' , table : 3 , avant : 3 , maintenant : 2 } ] ] ,
[ 'agrandir la table 1 à quatre sièges' , ( c ) => { c . tables [ 0 ] . sieges = 4 ; } ,
[ { code : 'CAPACITE_CHANGEE' , table : 1 , avant : 3 , maintenant : 4 } ] ] ,
[ 'passer le défaut à quatre sièges' , ( c ) => { c . evenement . siegesParDefaut = 4 ; } ,
[ 1 , 2 , 3 , 4 ] . map ( ( id ) => ( { code : 'CAPACITE_CHANGEE' , table : id , avant : 3 , maintenant : 4 } ) ) ] ,
[ 'surcharger la table 4 à la valeur du défaut' , ( c ) => { c . tables [ 3 ] . sieges = 3 ; } ,
[ ] ] ,
[ 'passer à cinq tours' , ( c ) => { c . evenement . tours = 5 ; } ,
[ { code : 'TOURS_CHANGES' , avant : 4 , maintenant : 5 } ] ] ,
[ 'passer à trois tours' , ( c ) => { c . evenement . tours = 3 ; } ,
[ { code : 'TOURS_CHANGES' , avant : 4 , maintenant : 3 } ] ] ,
[ "renommer, changer d'appartenance, renuméroter, déplacer et changer de forme, attribuer les sièges" , ( c ) => {
c . participants [ 0 ] . nom = 'Aubier' ;
c . participants [ 1 ] . appartenance = null ;
c . tables [ 1 ] . numero = 7 ;
c . tables [ 2 ] . position = { x : - 40 , y : 125.5 } ;
c . tables [ 3 ] . forme = 'carree' ;
c . reglages . attribuerSieges = true ;
} , [ ] ] ,
] ;
for ( const [ libelle , changer , raisons ] of cas ) {
const charge = chargeAvecGeneration ( ) ;
changer ( charge ) ;
for ( const proposition of charge . propositions ) {
assert . deepStrictEqual ( derive ( proposition , charge ) , raisons , libelle ) ;
}
const examen = examiner ( charge ) ;
assert . deepStrictEqual ( examen . fautives , [ ] , libelle ) ;
assert . equal ( examen . charge . propositions . length , 3 , libelle ) ;
const attendues = raisons . length === 0 ? [ ] : [ 1 , 2 , 3 ] . map ( ( id ) => ( { id , raisons } ) ) ;
assert . deepStrictEqual ( examen . derives , attendues , libelle ) ;
}
} ) ;
test ( "les raisons d'un même code se rangent par identifiant, quel que soit l'ordre de la déclaration ou de la charge" , ( ) => {
const p = fichierPermute ( ) ;
const charge = chargePetite ( ) ;
charge . evenement . siegesParDefaut = 4 ;
charge . tables = charge . tables . filter ( ( { id } ) => id !== 3 && id !== 1 ) ;
charge . tables . push ( table ( 6 , 6 ) , table ( 5 , 5 ) ) ;
charge . participants [ 8 ] . exclu = true ;
charge . participants [ 1 ] . exclu = true ;
charge . participants = charge . participants . filter ( ( { id } ) => id !== 11 && id !== 4 ) ;
charge . participants . push ( personne ( 14 , 'Ortie' , 'Ovide' ) , personne ( 13 , ... TREIZIEME ) ) ;
assert . deepStrictEqual ( derive ( p , charge ) , [
{ code : 'CAPACITE_CHANGEE' , table : 2 , avant : 3 , maintenant : 4 } ,
{ code : 'CAPACITE_CHANGEE' , table : 4 , avant : 3 , maintenant : 4 } ,
{ code : 'PARTICIPANT_EXCLU' , participant : 2 } ,
{ code : 'PARTICIPANT_EXCLU' , participant : 9 } ,
{ code : 'PARTICIPANT_NON_PLACE' , participant : 13 } ,
{ code : 'PARTICIPANT_NON_PLACE' , participant : 14 } ,
{ code : 'PARTICIPANT_SUPPRIME' , participant : 4 } ,
{ code : 'PARTICIPANT_SUPPRIME' , participant : 11 } ,
{ code : 'TABLE_AJOUTEE' , table : 5 } ,
{ code : 'TABLE_AJOUTEE' , table : 6 } ,
{ code : 'TABLE_SUPPRIMEE' , table : 1 } ,
{ code : 'TABLE_SUPPRIMEE' , table : 3 } ,
] ) ;
} ) ;
test ( "capacités inégales, tables déclarées hors de l'ordre des identifiants : chaque capacité déclarée se compare à celle de sa table, et seule celle qui change est nommée" , ( ) => {
const p = fichierInegal ( ) ;
const charge = chargePetite ( ) ;
charge . tables [ 3 ] . sieges = 4 ;
assert . deepStrictEqual ( derive ( p , charge ) , [ ] ) ;
charge . tables [ 3 ] . sieges = 5 ;
assert . deepStrictEqual ( derive ( p , charge ) , [ { code : 'CAPACITE_CHANGEE' , table : 4 , avant : 4 , maintenant : 5 } ] ) ;
charge . tables [ 3 ] . sieges = 4 ;
charge . evenement . siegesParDefaut = 4 ;
assert . deepStrictEqual (
derive ( p , charge ) ,
[ 1 , 2 , 3 ] . map ( ( id ) => ( { code : 'CAPACITE_CHANGEE' , table : id , avant : 3 , maintenant : 4 } ) ) ,
) ;
} ) ;
test ( "une personne exclue à la génération puis réintégrée n'est pas placée : PARTICIPANT_NON_PLACE" , ( ) => {
const configuration = petite ( ) ;
configuration . participants [ 4 ] . exclu = true ;
const [ proposition ] = rechercher ( configuration , { graine : 9 , arret : 1_000 , nombre : 1 } ) ;
const fichier = versFichier ( proposition , configuration , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ) ;
const charge = chargePetite ( ) ;
charge . participants [ 4 ] . exclu = true ;
assert . deepStrictEqual ( derive ( fichier , charge ) , [ ] ) ;
charge . participants [ 4 ] . exclu = false ;
assert . deepStrictEqual ( derive ( fichier , charge ) , [ { code : 'PARTICIPANT_NON_PLACE' , participant : 5 } ] ) ;
} ) ;
test ( 'sans dérive, le plan passe indexerPlan sur la configuration courante ; une table ajoutée est une dérive, que le moteur refuserait' , ( ) => {
const charge = chargeAvecGeneration ( ) ;
const instance = normaliser ( configurationDepuisCharge ( charge ) ) ;
for ( const proposition of charge . propositions ) {
assert . deepStrictEqual ( derive ( proposition , charge ) , [ ] ) ;
indexerPlan ( instance , planDepuisFichier ( proposition ) ) ;
}
charge . tables . push ( table ( 5 , 5 ) ) ;
const agrandie = normaliser ( configurationDepuisCharge ( charge ) ) ;
for ( const proposition of charge . propositions ) {
assert . deepStrictEqual ( derive ( proposition , charge ) , [ { code : 'TABLE_AJOUTEE' , table : 5 } ] ) ;
assert . throws (
( ) => indexerPlan ( agrandie , planDepuisFichier ( proposition ) ) ,
( erreur ) => erreur instanceof ErreurConfiguration && erreur . code === 'PLAN_TABLE_ABSENTE' && erreur . details . table === 5 ,
) ;
}
} ) ;
} ) ;
describe ( "examiner : le partage d'une charge lue (§ 8.9, point 3)" , ( ) => {
test ( "un retenu fautif est signalé et reste dans la charge, sans raison de dérive : la reprise par le .precedent n'est pas de son ressort" , ( ) => {
const charge = chargeAvecGeneration ( ) ;
const retenu = retenuDe ( charge . propositions [ 0 ] ) ;
const perdu = retenu . placement [ 1 ] . sieges [ 3 ] . pop ( ) ;
charge . retenu = retenu ;
charge . evenement . etat = 'retenu' ;
charge . participants [ 4 ] . exclu = true ;
const lue = relire ( charge ) ;
const examen = examiner ( lue ) ;
assert . deepStrictEqual ( examen . retenu , { fautes : [ { code : 'MANQUANT' , tour : 2 , participant : perdu } ] , raisons : [ ] } ) ;
assert . equal ( examen . charge . retenu , lue . retenu ) ;
assert . deepStrictEqual ( examen . fautives , [ ] ) ;
assert . deepStrictEqual ( examen . charge . propositions , charge . propositions ) ;
const raisons = [ { code : 'PARTICIPANT_EXCLU' , participant : 5 } ] ;
assert . deepStrictEqual ( examen . derives , [ 1 , 2 , 3 ] . map ( ( id ) => ( { id , raisons } ) ) ) ;
} ) ;
test ( "un retenu de forme quelconque, que l'analyse admet, est fautif sous la forme du retenu" , ( ) => {
const sansCle = abimerFichier ( chargeAvecGeneration ( ) , ( ecrit ) => {
ecrit . charge . retenu = { inconnue : [ ] } ;
ecrit . entete . comptes . retenu = 1 ;
} ) ;
assert . deepStrictEqual ( examiner ( sansCle ) . retenu , { fautes : [ { code : 'FORME' , chemin : 'proposition' } ] , raisons : [ ] } ) ;
const charge = chargeAvecGeneration ( ) ;
const sansProposition = retenuDe ( charge . propositions [ 0 ] ) ;
delete sansProposition . proposition ;
charge . retenu = sansProposition ;
assert . deepStrictEqual ( examiner ( charge ) . retenu , { fautes : [ { code : 'FORME' , chemin : 'proposition' } ] , raisons : [ ] } ) ;
assert . equal ( examiner ( charge ) . charge . retenu , sansProposition ) ;
} ) ;
test ( 'un retenu cohérent et périmé porte ses raisons, sans faute ; sans retenu, ni faute ni raison' , ( ) => {
const charge = chargeAvecGeneration ( ) ;
assert . deepStrictEqual ( examiner ( charge ) . retenu , { fautes : [ ] , raisons : [ ] } ) ;
charge . retenu = retenuDe ( charge . propositions [ 2 ] ) ;
assert . deepStrictEqual ( examiner ( charge ) . retenu , { fautes : [ ] , raisons : [ ] } ) ;
charge . participants [ 4 ] . exclu = true ;
assert . deepStrictEqual ( examiner ( charge ) . retenu , {
fautes : [ ] ,
raisons : [ { code : 'PARTICIPANT_EXCLU' , participant : 5 } ] ,
} ) ;
} ) ;
test ( "deux propositions cohérentes de même identifiant : la seconde dans l'ordre de la liste est fautive, IDENTIFIANT_REPETE, et le fichier s'ouvre sans elle ; une fautive n'écarte pas la cohérente qui porte son identifiant" , ( ) => {
const charge = chargeAvecGeneration ( ) ;
const [ p1 , p2 , p3 ] = charge . propositions ;
const homonyme = { ... structuredClone ( p3 ) , id : 2 } ;
charge . propositions = [ p1 , p2 , homonyme , p3 ] ;
const examen = examiner ( charge ) ;
assert . deepStrictEqual ( examen . fautives , [ { rang : 2 , id : 2 , fautes : [ { code : 'IDENTIFIANT_REPETE' } ] } ] ) ;
assert . deepStrictEqual ( examen . charge . propositions , [ p1 , p2 , p3 ] ) ;
assert . equal ( examen . charge . propositions [ 1 ] , p2 ) ;
// Au travers du fichier, que l'analyse admet avec ses deux propositions 2.
const lue = examiner ( relire ( charge ) ) ;
assert . deepStrictEqual ( lue . fautives . map ( ( { id , fautes } ) => [ id , fautes ] ) , [ [ 2 , [ { code : 'IDENTIFIANT_REPETE' } ] ] ] ) ;
assert . deepStrictEqual ( lue . charge . propositions . map ( ( { id } ) => id ) , [ 1 , 2 , 3 ] ) ;
const abimee = { ... structuredClone ( p1 ) , tours : 5 } ;
charge . propositions = [ abimee , p1 , p2 , p3 ] ;
const seconde = examiner ( charge ) ;
assert . deepStrictEqual ( seconde . fautives , [
{ rang : 0 , id : 1 , fautes : [ { code : 'LONGUEUR' , tour : null , table : null , declare : 5 , ecrit : 4 } ] } ,
] ) ;
assert . deepStrictEqual ( seconde . charge . propositions , [ p1 , p2 , p3 ] ) ;
// Après la cohérente, une fautive de même identifiant se nomme par sa
// propre faute, et non comme une répétition.
charge . propositions = [ p1 , abimee ] ;
assert . deepStrictEqual ( examiner ( charge ) . fautives , [
{ rang : 1 , id : 1 , fautes : [ { code : 'LONGUEUR' , tour : null , table : null , declare : 5 , ecrit : 4 } ] } ,
] ) ;
} ) ;
test ( "une proposition cohérente dont l'identifiant atteint prochainsIds.proposition est fautive, IDENTIFIANT_HORS_COMPTEUR, et le fichier s'ouvre sans elle ; un retenu dont la proposition d'origine l'atteint porte cette faute, reste, et garde ses raisons de dérive" , ( ) => {
const HORS = { code : 'IDENTIFIANT_HORS_COMPTEUR' } ;
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de la
// première. Un octet change dans le fichier écrit : chaque fois,
// l'analyse le lit, et examiner n'écarte que la proposition touchée.
const charge = chargeAvecGeneration ( ) ;
const [ p1 , p2 , p3 ] = charge . propositions ;
charge . retenu = retenuDe ( p1 ) ;
charge . evenement . etat = 'retenu' ;
const texte = serialiser ( charge , ENTETE ) ;
const unOctet = ( [ avant , apres ] ) => {
assert . equal ( texte . split ( avant ) . length , 2 , ` « ${ avant } » doit figurer une fois ` ) ;
return examiner ( analyser ( texte . replace ( avant , apres ) ) . charge ) ;
} ;
// L'identifiant de la troisième proposition, puis le compteur, réécrits.
const troisieme = ( id ) => [ '{"id":3,"graine"' , ` {"id": ${ id } ,"graine" ` ] ;
const compteur = ( prochain ) => [ '"proposition":4}' , ` "proposition": ${ prochain } } ` ] ;
const cas = [
[ 'la troisième au compteur' , troisieme ( 4 ) , [ { rang : 2 , id : 4 , fautes : [ HORS ] } ] , [ p1 , p2 ] ] ,
[ 'la troisième au-delà' , troisieme ( 9 ) , [ { rang : 2 , id : 9 , fautes : [ HORS ] } ] , [ p1 , p2 ] ] ,
[ 'la troisième répète la deuxième' , troisieme ( 2 ) ,
[ { rang : 2 , id : 2 , fautes : [ { code : 'IDENTIFIANT_REPETE' } ] } ] , [ p1 , p2 ] ] ,
[ 'la troisième à 0' , troisieme ( 0 ) , [ { rang : 2 , id : null , fautes : [ { code : 'FORME' , chemin : 'id' } ] } ] , [ p1 , p2 ] ] ,
[ 'le compteur abaissé à 2' , compteur ( 2 ) ,
[ { rang : 1 , id : 2 , fautes : [ HORS ] } , { rang : 2 , id : 3 , fautes : [ HORS ] } ] , [ p1 ] ] ,
] ;
for ( const [ libelle , octet , fautives , gardees ] of cas ) {
const examen = unOctet ( octet ) ;
assert . deepStrictEqual ( examen . fautives , fautives , libelle ) ;
assert . deepStrictEqual ( examen . charge . propositions , gardees , libelle ) ;
assert . deepStrictEqual ( examen . retenu , { fautes : [ ] , raisons : [ ] } , libelle ) ;
}
// Abaissé à 1, le compteur écarte chaque proposition, et le retenu, dont
// l'origine l'atteint, porte la faute sans être retiré. Son placement se
// tient : sa dérive se nomme encore.
const auPlusBas = unOctet ( compteur ( 1 ) ) ;
assert . deepStrictEqual (
auPlusBas . fautives . map ( ( { id , fautes } ) => [ id , fautes ] ) ,
[ [ 1 , [ HORS ] ] , [ 2 , [ HORS ] ] , [ 3 , [ HORS ] ] ] ,
) ;
assert . deepStrictEqual ( auPlusBas . charge . propositions , [ ] ) ;
assert . deepStrictEqual ( auPlusBas . retenu , { fautes : [ HORS ] , raisons : [ ] } ) ;
assert . deepStrictEqual ( auPlusBas . charge . retenu , charge . retenu ) ;
const exclue = structuredClone ( charge ) ;
exclue . participants [ 4 ] . exclu = true ;
exclue . prochainsIds . proposition = 1 ;
assert . deepStrictEqual ( examiner ( exclue ) . retenu , {
fautes : [ HORS ] ,
raisons : [ { code : 'PARTICIPANT_EXCLU' , participant : 5 } ] ,
} ) ;
// Une proposition qui se contredit se nomme par ses propres fautes,
// même quand son identifiant atteint aussi le compteur ; un retenu qui se
// contredit aussi, sans raison de dérive.
const abimee = { ... structuredClone ( p3 ) , id : 4 , tours : 5 } ;
const contradictoire = { ... charge , propositions : [ p1 , abimee ] , retenu : { ... retenuDe ( abimee ) , proposition : 4 } } ;
const longueur = { code : 'LONGUEUR' , tour : null , table : null , declare : 5 , ecrit : 4 } ;
const examen = examiner ( contradictoire ) ;
assert . deepStrictEqual ( examen . fautives , [ { rang : 1 , id : 4 , fautes : [ longueur ] } ] ) ;
assert . deepStrictEqual ( examen . retenu , { fautes : [ longueur ] , raisons : [ ] } ) ;
} ) ;
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
test ( "la charge rendue relève prochainsIds.proposition au-delà de chaque identifiant lu — d'une proposition écartée, quelle que soit sa faute, et de l'origine du retenu, cohérent ou non — ; le compteur ne recule jamais, ce qui n'est pas un identifiant, au-delà de 2^31 − 1 compris, n'y entre pas, et relevé, il s'arrête au plus grand identifiant, que l'analyse relit" , ( ) => {
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
const HORS = 'IDENTIFIANT_HORS_COMPTEUR' ;
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
const PLUS _GRAND = 2 * * 31 - 1 ;
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de la
// troisième. Chaque cas abîme la charge du fichier écrit, que l'analyse
// relit et qui se fige avant l'examen. Il nomme les écartées, [id,
// code], et les fautes du retenu, ce que le cas éprouve, puis le
// compteur attendu.
const base = chargeAvecGeneration ( ) ;
base . retenu = retenuDe ( base . propositions [ 2 ] ) ;
base . evenement . etat = 'retenu' ;
const cas = [
[ "le fichier tel qu'écrit" , ( ) => { } , [ ] , [ ] , 4 ] ,
[ 'le compteur porté à 9' , ( c ) => { c . prochainsIds . proposition = 9 ; } , [ ] , [ ] , 9 ] ,
[ 'le compteur abaissé à 2' , ( c ) => { c . prochainsIds . proposition = 2 ; } , [ [ 2 , HORS ] , [ 3 , HORS ] ] , [ HORS ] , 4 ] ,
[ 'le compteur abaissé à 1' , ( c ) => { c . prochainsIds . proposition = 1 ; } ,
[ [ 1 , HORS ] , [ 2 , HORS ] , [ 3 , HORS ] ] , [ HORS ] , 4 ] ,
[ 'la troisième à 9' , ( c ) => { c . propositions [ 2 ] . id = 9 ; } , [ [ 9 , HORS ] ] , [ ] , 10 ] ,
[ 'la troisième à 9, qui se contredit' , ( c ) => { Object . assign ( c . propositions [ 2 ] , { id : 9 , tours : 5 } ) ; } ,
[ [ 9 , 'LONGUEUR' ] ] , [ ] , 10 ] ,
[ 'la troisième répète la deuxième' , ( c ) => { c . propositions [ 2 ] . id = 2 ; } , [ [ 2 , 'IDENTIFIANT_REPETE' ] ] , [ ] , 4 ] ,
[ 'la troisième à 9.5' , ( c ) => { c . propositions [ 2 ] . id = 9.5 ; } , [ [ null , 'FORME' ] ] , [ ] , 4 ] ,
[ 'la troisième à 2^53' , ( c ) => { c . propositions [ 2 ] . id = 2 * * 53 ; } , [ [ null , 'FORME' ] ] , [ ] , 4 ] ,
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
[ 'la troisième à 2^31' , ( c ) => { c . propositions [ 2 ] . id = 2 * * 31 ; } , [ [ null , 'FORME' ] ] , [ ] , 4 ] ,
[ 'la troisième au plus grand identifiant' , ( c ) => { c . propositions [ 2 ] . id = PLUS _GRAND ; } ,
[ [ PLUS _GRAND , HORS ] ] , [ ] , PLUS _GRAND ] ,
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
[ "l'origine du retenu à 9" , ( c ) => { c . retenu . proposition = 9 ; } , [ ] , [ HORS ] , 10 ] ,
[ "l'origine du retenu à 9, qui se contredit" , ( c ) => { Object . assign ( c . retenu , { proposition : 9 , tours : 5 } ) ; } ,
[ ] , [ 'LONGUEUR' ] , 10 ] ,
[ "l'origine du retenu à 9, hors de sa règle" , ( c ) => {
Object . assign ( c . retenu , { proposition : 9 , toura : c . retenu . tours } ) ;
delete c . retenu . tours ;
} , [ ] , [ 'FORME' ] , 10 ] ,
[ "l'origine du retenu à 9.5" , ( c ) => { c . retenu . proposition = 9.5 ; } , [ ] , [ 'FORME' ] , 4 ] ,
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
[ "l'origine du retenu au plus grand entier exact" , ( c ) => { c . retenu . proposition = Number . MAX _SAFE _INTEGER ; } ,
[ ] , [ 'FORME' ] , 4 ] ,
[ "l'origine du retenu à 2^31" , ( c ) => { c . retenu . proposition = 2 * * 31 ; } , [ ] , [ 'FORME' ] , 4 ] ,
[ "l'origine du retenu au plus grand identifiant" , ( c ) => { c . retenu . proposition = PLUS _GRAND ; } ,
[ ] , [ HORS ] , PLUS _GRAND ] ,
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
] ;
// Le compteur que l'analyse relit dans le fichier de la charge rendue,
// ou le refus qu'elle lève.
const compteurRelu = ( charge ) => {
try {
return relire ( charge ) . prochainsIds . proposition ;
} catch ( erreur ) {
return ` ${ erreur . name } ${ erreur . message } ` ;
}
} ;
const observes = cas . map ( ( [ libelle , abimer ] ) => {
const lue = geler ( abimerFichier ( base , ( ecrit ) => abimer ( ecrit . charge ) ) ) ;
const examen = examiner ( lue ) ;
return [
libelle ,
examen . fautives . map ( ( { id , fautes : [ premiere ] } ) => [ id , premiere . code ] ) ,
examen . retenu . fautes . map ( ( { code } ) => code ) ,
examen . charge . prochainsIds ,
compteurRelu ( examen . charge ) ,
] ;
} ) ;
assert . deepStrictEqual (
observes ,
cas . map ( ( [ libelle , , ecartees , fautesDuRetenu , compteur ] ) => [
libelle ,
ecartees ,
fautesDuRetenu ,
{ participant : 13 , table : 5 , proposition : compteur } ,
compteur ,
] ) ,
) ;
} ) ;
[FIX] storage: compact seat patches, checked poses, 32-bit identifiers
Without seat attribution a table list stays sorted, so swapping two
people shifted every rank between them and a patch grew to over a
kilobyte; a same-length list of ids is now set whole when that is
lighter, and a swap costs two small operations (§ 8.9). A patch that
sets a malformed proposition or retained placement is refused at its
rank, instead of a TypeError at the next application. Identifiers stay
below 2^31, the engine's Int32Array bound: beyond, the record is a
form fault, and a proposition is set aside.
Checked: storage tests; reviewed, remaining findings minor.
--- FR ---
[FIX] stockage : correctifs compacts, poses contrôlées, id sous 2^31
Sans attribution des sièges, une liste de table reste triée : échanger
deux personnes décalait chaque rang entre elles, et un correctif passait
le kilo-octet ; une liste d'identifiants de même longueur se pose
désormais entière quand c'est plus léger, et un échange coûte deux
petites opérations (§ 8.9). Un correctif qui pose une proposition ou un
retenu mal formé est refusé à son rang, au lieu d'une TypeError à
l'application suivante. Les identifiants restent sous 2^31, borne des
Int32Array du moteur : au-delà, faute de forme, et proposition écartée.
Vérifié : épreuves du stockage ; revue, constats restants mineurs.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 19:22:02 -04:00
test ( "un identifiant au-delà de 2^31 − 1, écrit à la main à l'origine du retenu ou à une proposition, n'en est pas un : le retenu est fautif, la proposition écartée, le compteur reste à 4 ; la génération suivante, numérotée comme le dit versFichier, prend 4, 5 et 6, et le fichier se rouvre" , ( ) => {
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de la
// première. Une valeur change dans le texte écrit, que l'analyse lit
// sans entrer dans les propositions ni dans le retenu.
const suivante = rechercher ( petite ( ) , { graine : 13 , arret : 2_000 , nombre : 3 } ) ;
const charge = chargeAvecGeneration ( ) ;
charge . retenu = retenuDe ( charge . propositions [ 0 ] ) ;
charge . evenement . etat = 'retenu' ;
const texte = serialiser ( charge , ENTETE ) ;
const FORME _ORIGINE = { code : 'FORME' , chemin : 'proposition' } ;
const cas = [
[ "l'origine du retenu à 2^53 − 2" , '"retenu": {"proposition":1,' , 2 * * 53 - 2 , [ 1 , 2 , 3 ] , [ FORME _ORIGINE ] ] ,
[ "l'origine du retenu à 2^31" , '"retenu": {"proposition":1,' , 2 * * 31 , [ 1 , 2 , 3 ] , [ FORME _ORIGINE ] ] ,
[ 'la troisième proposition à 2^53 − 2' , '{"id":3,"graine"' , 2 * * 53 - 2 , [ 1 , 2 ] , [ ] ] ,
[ 'la troisième proposition à 2^31' , '{"id":3,"graine"' , 2 * * 31 , [ 1 , 2 ] , [ ] ] ,
] ;
const observes = [ ] ;
const attendus = [ ] ;
for ( const [ libelle , avant , valeur , gardees , fautesDuRetenu ] of cas ) {
assert . equal ( texte . split ( avant ) . length , 2 , ` « ${ avant } » doit figurer une fois ` ) ;
const ouverte = examiner ( analyser ( texte . replace ( avant , avant . replace ( /\d+,/ , ` ${ valeur } , ` ) ) ) . charge ) . charge ;
// L'appelant, comme le dit versFichier : la génération décalée de
// prochainsIds.proposition − 1, puis le compteur porté au-delà.
const decalage = ouverte . prochainsIds . proposition - 1 ;
const configuration = configurationDepuisCharge ( ouverte ) ;
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage } ;
const generee = {
... ouverte ,
prochainsIds : { ... ouverte . prochainsIds , proposition : decalage + suivante . length + 1 } ,
propositions : [
... ouverte . propositions ,
... suivante . map ( ( proposition ) => versFichier ( proposition , configuration , options ) ) ,
] ,
} ;
const rouverte = examiner ( relire ( generee ) ) ;
observes . push ( [
libelle ,
decalage ,
rouverte . charge . propositions . map ( ( { id } ) => id ) ,
rouverte . fautives ,
rouverte . retenu . fautes ,
rouverte . charge . prochainsIds . proposition ,
] ) ;
attendus . push ( [ libelle , 3 , [ ... gardees , 4 , 5 , 6 ] , [ ] , fautesDuRetenu , 7 ] ) ;
}
assert . deepStrictEqual ( observes , attendus ) ;
} ) ;
test ( "un identifiant de 2^31 − 1, le plus grand, écrit à la main à l'origine du retenu ou à une proposition, ou le compteur écrit à cette valeur, laisse à 2^31 − 1 le compteur de la charge rendue : la génération suivante, d'une proposition ou de trois, numérotée comme le dit versFichier, lève COMPTEUR_SATURE à sa première proposition, un code que l'appelant lit avant toute écriture" , ( ) => {
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de la
// première. Une valeur change dans le texte écrit, que l'analyse admet :
// elle ne lit des propositions et du retenu que le conteneur, et le
// compteur reste dans sa règle.
const PLUS _GRAND = 2 * * 31 - 1 ;
const suivante = rechercher ( petite ( ) , { graine : 13 , arret : 2_000 , nombre : 3 } ) ;
const charge = chargeAvecGeneration ( ) ;
charge . retenu = retenuDe ( charge . propositions [ 0 ] ) ;
charge . evenement . etat = 'retenu' ;
const texte = serialiser ( charge , ENTETE ) ;
const cas = [
[ "l'origine du retenu" , '"retenu": {"proposition":1,' , ` "retenu": {"proposition": ${ PLUS _GRAND } , ` ] ,
[ 'la troisième proposition' , '{"id":3,"graine"' , ` {"id": ${ PLUS _GRAND } ,"graine" ` ] ,
[ 'le compteur' , '"proposition":4}' , ` "proposition": ${ PLUS _GRAND } } ` ] ,
] ;
const observes = [ ] ;
const attendus = [ ] ;
for ( const [ libelle , avant , apres ] of cas ) {
assert . equal ( texte . split ( avant ) . length , 2 , ` « ${ avant } » doit figurer une fois ` ) ;
const ouverte = examiner ( analyser ( texte . replace ( avant , apres ) ) . charge ) . charge ;
// L'appelant, comme le dit versFichier : la génération décalée de
// prochainsIds.proposition − 1, chacune de ses propositions mise en
// forme avant qu'aucune ne s'écrive.
const decalage = ouverte . prochainsIds . proposition - 1 ;
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage } ;
const configuration = configurationDepuisCharge ( ouverte ) ;
for ( const nombre of [ 1 , 3 ] ) {
const generation = suivante . slice ( 0 , nombre ) ;
observes . push ( [
libelle ,
nombre ,
ouverte . prochainsIds . proposition ,
refusDe ( ( ) => generation . map ( ( proposition ) => versFichier ( proposition , configuration , options ) ) ) ,
] ) ;
attendus . push ( [ libelle , nombre , PLUS _GRAND , COMPTEUR _SATURE ] ) ;
}
}
assert . deepStrictEqual ( observes , attendus ) ;
} ) ;
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
test ( "un compteur abaissé à la main, puis une génération numérotée depuis la charge rendue, comme le dit versFichier : aucun identifiant écarté ne revient, et l'origine du retenu n'est jamais réattribuée" , ( ) => {
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de
// l'une d'elles. Le compteur, abaissé à 1, 2 ou 3 dans le fichier écrit,
// écarte celles qu'il ne couvre plus. La génération suivante, décalée de
// prochainsIds.proposition − 1 de la charge rendue, prend 4, 5 et 6 ; à
// la réouverture, l'identifiant de l'origine du retenu ne désigne que
// l'origine elle-même, quand elle est gardée, et rien sinon.
const suivante = rechercher ( petite ( ) , { graine : 13 , arret : 2_000 , nombre : 3 } ) ;
const observes = [ ] ;
const attendus = [ ] ;
for ( const abaisse of [ 1 , 2 , 3 ] ) {
for ( const rang of [ 0 , 1 , 2 ] ) {
const charge = chargeAvecGeneration ( ) ;
const origine = charge . propositions [ rang ] ;
charge . retenu = retenuDe ( origine ) ;
charge . evenement . etat = 'retenu' ;
const texte = serialiser ( charge , ENTETE ) ;
assert . equal ( texte . split ( '"proposition":4}' ) . length , 2 , 'le compteur doit figurer une fois' ) ;
const abime = texte . replace ( '"proposition":4}' , ` "proposition": ${ abaisse } } ` ) ;
const ouverte = examiner ( analyser ( abime ) . charge ) . charge ;
// L'appelant, comme le dit versFichier : la génération décalée de
// prochainsIds.proposition − 1, puis le compteur porté au-delà.
const configuration = configurationDepuisCharge ( ouverte ) ;
const decalage = ouverte . prochainsIds . proposition - 1 ;
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage } ;
const generee = {
... ouverte ,
prochainsIds : { ... ouverte . prochainsIds , proposition : decalage + suivante . length + 1 } ,
propositions : [
... ouverte . propositions ,
... suivante . map ( ( proposition ) => versFichier ( proposition , configuration , options ) ) ,
] ,
} ;
const rouverte = examiner ( relire ( generee ) ) ;
const libelle = ` compteur abaissé à ${ abaisse } , retenu de la proposition ${ origine . id } ` ;
observes . push ( [
libelle ,
rouverte . charge . propositions . map ( ( { id } ) => id ) ,
rouverte . fautives ,
rouverte . retenu ,
rouverte . charge . propositions . filter ( ( { id } ) => id === origine . id ) . map ( ( { graine } ) => graine ) ,
] ) ;
attendus . push ( [
libelle ,
[ ... [ 1 , 2 , 3 ] . filter ( ( id ) => id < abaisse ) , 4 , 5 , 6 ] ,
[ ] ,
{ fautes : [ ] , raisons : [ ] } ,
origine . id < abaisse ? [ origine . graine ] : [ ] ,
] ) ;
}
}
assert . deepStrictEqual ( observes , attendus ) ;
} ) ;
test ( "une proposition qui ne se lit pas est écartée seule : rang dans la liste, id null quand son identifiant n'en est pas un ; null, et un retenu égaré parmi les propositions, aussi" , ( ) => {
const charge = chargeAvecGeneration ( ) ;
const lue = abimerFichier ( charge , ( ecrit ) => {
const [ , deuxieme ] = ecrit . charge . propositions ;
ecrit . charge . propositions . unshift ( 42 , { id : 1 , inconnue : true } , { ... deuxieme , id : 'deux' } , null , retenuDe ( deuxieme ) ) ;
ecrit . entete . comptes . propositions = 8 ;
} ) ;
const examen = examiner ( lue ) ;
// Le retenu égaré passe la forme du retenu : seule la règle d'une
// proposition, que l'examen applique à chaque élément de la liste,
// l'écarte.
assert . deepStrictEqual ( fautes ( lue . propositions [ 4 ] ) , [ ] ) ;
assert . deepStrictEqual ( examen . fautives , [
{ rang : 0 , id : null , fautes : [ { code : 'FORME' , chemin : '' } ] } ,
{ rang : 1 , id : 1 , fautes : [ { code : 'FORME' , chemin : 'graine' } ] } ,
{ rang : 2 , id : null , fautes : [ { code : 'FORME' , chemin : 'id' } ] } ,
{ rang : 3 , id : null , fautes : [ { code : 'FORME' , chemin : '' } ] } ,
{ rang : 4 , id : null , fautes : [ { code : 'FORME' , chemin : 'id' } ] } ,
] ) ;
assert . deepStrictEqual ( examen . charge . propositions , charge . propositions ) ;
assert . deepStrictEqual ( examen . derives , [ ] ) ;
} ) ;
test ( "la charge rendue garde l'ordre de la liste ; les dérives se rangent par identifiant, les fautives par rang" , ( ) => {
const charge = chargeAvecGeneration ( ) ;
const [ p1 , p2 , p3 ] = charge . propositions ;
const abimee = { ... structuredClone ( p2 ) , id : 4 , tours : 5 } ;
charge . propositions = [ p3 , abimee , p1 , p2 ] ;
charge . participants [ 4 ] . exclu = true ;
const examen = examiner ( charge ) ;
const raisons = [ { code : 'PARTICIPANT_EXCLU' , participant : 5 } ] ;
assert . deepStrictEqual ( examen . charge . propositions , [ p3 , p1 , p2 ] ) ;
assert . deepStrictEqual ( examen . derives , [ 1 , 2 , 3 ] . map ( ( id ) => ( { id , raisons } ) ) ) ;
assert . deepStrictEqual ( examen . fautives . map ( ( { rang , id } ) => [ rang , id ] ) , [ [ 1 , 4 ] ] ) ;
} ) ;
test ( 'examiner ne modifie pas la charge reçue et rend une charge neuve, dont le reste est celui de la charge reçue' , ( ) => {
const charge = chargeAvecGeneration ( ) ;
charge . propositions [ 0 ] . placement [ 0 ] . reserve . push ( 13 ) ;
charge . retenu = retenuDe ( charge . propositions [ 1 ] ) ;
charge . participants [ 4 ] . exclu = true ;
const avant = structuredClone ( charge ) ;
geler ( charge ) ;
const examen = examiner ( charge ) ;
assert . deepStrictEqual ( charge , avant ) ;
assert . notEqual ( examen . charge , charge ) ;
assert . deepStrictEqual ( examen . charge , { ... avant , propositions : avant . propositions . slice ( 1 ) } ) ;
assert . equal ( examen . charge . participants , charge . participants ) ;
assert . equal ( examen . charge . retenu , charge . retenu ) ;
} ) ;
} ) ;
describe ( 'formeNommee : la lecture et le signalement (§ 8.9, point 4)' , ( ) => {
test ( 'le tour 1 du plan parfait : douze lignes, table 1 → 1, 2, 3, avec les noms et prénoms de la charge, siège null sans attribution' , ( ) => {
const lignes = formeNommee ( fichierParfait ( ) , chargePetite ( ) ) ;
assert . equal ( lignes . length , 48 ) ;
assert . deepStrictEqual ( lignes . filter ( ( { tour } ) => tour === 1 ) , [
ligne ( 1 , 1 , 1 , null , 1 , 'Aubépine' , 'Agathe' ) ,
ligne ( 1 , 1 , 1 , null , 2 , 'Bruyère' , 'Basile' ) ,
ligne ( 1 , 1 , 1 , null , 3 , 'Chardon' , 'Céleste' ) ,
ligne ( 1 , 2 , 2 , null , 4 , 'Doucette' , 'Désiré' ) ,
ligne ( 1 , 2 , 2 , null , 5 , 'Églantine' , 'Edmée' ) ,
ligne ( 1 , 2 , 2 , null , 6 , 'Fougère' , 'Firmin' ) ,
ligne ( 1 , 3 , 3 , null , 7 , 'Giroflée' , 'Gisèle' ) ,
ligne ( 1 , 3 , 3 , null , 8 , 'Houblon' , 'Honoré' ) ,
ligne ( 1 , 3 , 3 , null , 9 , 'Isatis' , 'Ida' ) ,
ligne ( 1 , 4 , 4 , null , 10 , 'Jonquille' , 'Joachim' ) ,
ligne ( 1 , 4 , 4 , null , 11 , 'Lavande' , 'Lazare' ) ,
ligne ( 1 , 4 , 4 , null , 12 , 'Mélisse' , 'Mathurin' ) ,
] ) ;
assert . deepStrictEqual (
lignes . map ( ( { tour , numero , participant } ) => [ tour , numero , participant ] ) ,
PLAN _PARFAIT _PETITE . tours . flatMap ( ( listes , r ) => listes . flatMap ( ( ids , i ) => ids . map ( ( id ) => [ r + 1 , i + 1 , id ] ) ) ) ,
) ;
} ) ;
test ( "sièges attribués : le siège est le rang dans la liste, à partir de 1, dans l'ordre écrit" , ( ) => {
const p = { ... fichierParfait ( ) , siegesAttribues : true } ;
p . placement [ 0 ] . sieges [ 0 ] = [ 3 , 1 , 2 ] ;
const lignes = formeNommee ( p , chargePetite ( ) ) . filter ( ( { tour , table : id } ) => tour === 1 && id === 1 ) ;
assert . deepStrictEqual ( lignes , [
ligne ( 1 , 1 , 1 , 1 , 3 , 'Chardon' , 'Céleste' ) ,
ligne ( 1 , 1 , 1 , 2 , 1 , 'Aubépine' , 'Agathe' ) ,
ligne ( 1 , 1 , 1 , 3 , 2 , 'Bruyère' , 'Basile' ) ,
] ) ;
} ) ;
test ( "lignes rangées par tour, numéro de table, rang : numéros égaux départagés par l'identifiant, une table supprimée après les autres sans numéro, la réserve en dernier, une personne supprimée sans nom" , ( ) => {
const p = fichierPermute ( ) ;
p . participants . push ( 13 ) ;
for ( const tour of p . placement ) tour . reserve = [ 13 ] ;
const charge = chargePetite ( ) ;
charge . participants . push ( personne ( 13 , ... TREIZIEME ) ) ;
charge . participants [ 6 ] . prenom = null ;
charge . participants . splice ( 11 , 1 ) ;
charge . prochainsIds . participant = 14 ;
charge . tables = [ table ( 1 , 3 ) , table ( 2 , 1 ) , table ( 3 , 1 ) ] ;
const lignes = formeNommee ( p , charge ) ;
assert . equal ( lignes . length , 52 ) ;
assert . deepStrictEqual ( lignes . filter ( ( { tour } ) => tour === 1 ) , [
ligne ( 1 , 2 , 1 , null , 4 , 'Doucette' , 'Désiré' ) ,
ligne ( 1 , 2 , 1 , null , 5 , 'Églantine' , 'Edmée' ) ,
ligne ( 1 , 2 , 1 , null , 6 , 'Fougère' , 'Firmin' ) ,
ligne ( 1 , 3 , 1 , null , 7 , 'Giroflée' , null ) ,
ligne ( 1 , 3 , 1 , null , 8 , 'Houblon' , 'Honoré' ) ,
ligne ( 1 , 3 , 1 , null , 9 , 'Isatis' , 'Ida' ) ,
ligne ( 1 , 1 , 3 , null , 1 , 'Aubépine' , 'Agathe' ) ,
ligne ( 1 , 1 , 3 , null , 2 , 'Bruyère' , 'Basile' ) ,
ligne ( 1 , 1 , 3 , null , 3 , 'Chardon' , 'Céleste' ) ,
ligne ( 1 , 4 , null , null , 10 , 'Jonquille' , 'Joachim' ) ,
ligne ( 1 , 4 , null , null , 11 , 'Lavande' , 'Lazare' ) ,
ligne ( 1 , 4 , null , null , 12 , null , null ) ,
ligne ( 1 , null , null , null , 13 , 'Nigelle' , 'Noémie' ) ,
] ) ;
assert . deepStrictEqual (
lignes . map ( ( { tour } ) => tour ) ,
[ 1 , 2 , 3 , 4 ] . flatMap ( ( tour ) => Array ( 13 ) . fill ( tour ) ) ,
) ;
} ) ;
test ( "la réserve suit les tables de son tour, ses personnes dans l'ordre écrit" , ( ) => {
const lignes = formeNommee ( fichierAReserve ( ) , chargePetite ( ) ) . filter ( ( { tour , table : id } ) => tour === 1 && id === null ) ;
assert . deepStrictEqual ( lignes , [
ligne ( 1 , null , null , null , 6 , 'Fougère' , 'Firmin' ) ,
ligne ( 1 , null , null , null , 3 , 'Chardon' , 'Céleste' ) ,
] ) ;
} ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
test ( "un placement incohérent se rend pour le signalement de son défaut, une ligne par identifiant écrit, ses fautes à côté (fautes) : doublon, inconnu, liste au-delà de sa capacité, liste en trop sans table, liste ou tour en moins" , ( ) => {
const charge = chargePetite ( ) ;
const casDe = ( abimer ) => {
const p = fichierParfait ( ) ;
abimer ( p ) ;
return p ;
} ;
const cas = [
[
'un assis aussi dans la réserve' ,
casDe ( ( p ) => { p . placement [ 0 ] . reserve = [ 2 ] ; } ) ,
[ 'DOUBLON' ] ,
( lignes ) => {
assert . equal ( lignes . length , 49 ) ;
assert . deepEqual ( lignes . filter ( ( { tour , participant } ) => tour === 1 && participant === 2 ) , [
ligne ( 1 , 1 , 1 , null , 2 , 'Bruyère' , 'Basile' ) ,
ligne ( 1 , null , null , null , 2 , 'Bruyère' , 'Basile' ) ,
] ) ;
} ,
] ,
[
"un inconnu à la place d'un déclaré" ,
casDe ( ( p ) => { p . placement [ 1 ] . sieges [ 0 ] [ 0 ] = 99 ; } ) ,
[ 'INCONNU' , 'MANQUANT' ] ,
( lignes ) => {
assert . equal ( lignes . length , 48 ) ;
assert . deepEqual ( lignes . filter ( ( { participant } ) => participant === 99 ) , [
ligne ( 2 , 1 , 1 , null , 99 , null , null ) ,
] ) ;
} ,
] ,
[
'une table au-delà de sa capacité, une liste en trop' ,
casDe ( ( p ) => {
p . siegesAttribues = true ;
p . placement [ 0 ] . sieges [ 0 ] . push ( 4 ) ;
p . placement [ 0 ] . sieges . push ( [ 5 , 6 ] ) ;
} ) ,
[ 'LONGUEUR' , 'LONGUEUR' , 'DOUBLON' , 'DOUBLON' , 'DOUBLON' ] ,
( lignes ) => {
assert . equal ( lignes . length , 51 ) ;
assert . deepEqual ( lignes . filter ( ( { tour } ) => tour === 1 ) . slice ( 0 , 4 ) . map ( ( { siege , participant } ) => [ siege , participant ] ) , [
[ 1 , 1 ] ,
[ 2 , 2 ] ,
[ 3 , 3 ] ,
[ 4 , 4 ] ,
] ) ;
assert . deepEqual ( lignes . filter ( ( { tour } ) => tour === 1 ) . slice ( - 2 ) , [
ligne ( 1 , null , null , 1 , 5 , 'Églantine' , 'Edmée' , false ) ,
ligne ( 1 , null , null , 2 , 6 , 'Fougère' , 'Firmin' , false ) ,
] ) ;
} ,
] ,
[
'une liste et un tour en moins' ,
casDe ( ( p ) => {
p . placement [ 0 ] . sieges . pop ( ) ;
p . placement . pop ( ) ;
} ) ,
[ 'LONGUEUR' , 'LONGUEUR' , 'MANQUANT' , 'MANQUANT' , 'MANQUANT' ] ,
( lignes ) => {
assert . equal ( lignes . length , 33 ) ;
assert . deepEqual ( [ ... new Set ( lignes . map ( ( { tour } ) => tour ) ) ] , [ 1 , 2 , 3 ] ) ;
assert . equal ( lignes . filter ( ( { tour , table } ) => tour === 1 && table === 4 ) . length , 0 ) ;
} ,
] ,
] ;
for ( const [ nom , p , codes , verifier ] of cas ) {
assert . deepEqual ( fautes ( p ) . map ( ( { code } ) => code ) , codes , nom ) ;
verifier ( formeNommee ( p , charge ) ) ;
}
} ) ;
test ( "planDepuisFichier et derive refusent une proposition fautive par TypeError, au lieu d'en rendre une lecture fausse ; formeNommee ne refuse qu'une forme fausse ; fautes ne lève jamais" , ( ) => {
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
const charge = chargePetite ( ) ;
const abimee = fichierParfait ( ) ;
abimee . placement [ 0 ] . sieges [ 0 ] . pop ( ) ;
for ( const [ nom , appel ] of [
[ 'planDepuisFichier' , ( ) => planDepuisFichier ( abimee ) ] ,
[ 'derive' , ( ) => derive ( abimee , charge ) ] ,
] ) {
leveTypeError ( appel , /"MANQUANT"/ , nom ) ;
}
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
assert . equal ( formeNommee ( abimee , charge ) . length , 47 ) ;
[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop
count, history length and building version, then per round the ordered
tables and the reserve. A proposition that contradicts itself — wrong
length, repeated, unknown or missing id, an id already taken — is named
and left out, and the event still opens. One that no longer matches the
event — person excluded or added, table removed, capacity or rounds
changed — is kept and reported as drift. A named form serves reading.
Checked: 42 tests on the small demonstration; reviewed, mutants killed.
--- FR ---
[ADD] stockage : placements positionnels, fautes et dérives
Les placements se rangent par position : pour chaque proposition sa
graine, son compte d'arrêt, la longueur d'historique et la version qui
l'a produite, puis par tour les tables ordonnées et la réserve. Une
proposition qui se contredit — longueur fausse, identifiant répété,
inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement
s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou
ajoutée, table retirée, capacité ou tours changés — est gardée et
signalée comme dérive. Une forme nommée sert à la lecture.
Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:20 -04:00
for ( const illisible of [ 42 , undefined , { ... fichierParfait ( ) , placement : 'x' } ] ) {
leveTypeError ( ( ) => planDepuisFichier ( illisible ) , /"FORME"/ , String ( illisible ) ) ;
leveTypeError ( ( ) => derive ( illisible , charge ) , /"FORME"/ , String ( illisible ) ) ;
leveTypeError ( ( ) => formeNommee ( illisible , charge ) , /"FORME"/ , String ( illisible ) ) ;
}
assert . deepStrictEqual ( fautes ( undefined ) , [ { code : 'FORME' , chemin : '' } ] ) ;
} ) ;
} ) ;
// Exécute fonction pendant que l'horloge et les sources d'aléa lèvent à leur
// lecture, puis les rétablit telles qu'elles étaient, propres ou héritées ;
// rend ce que rend fonction.
function sansHorlogeNiAlea ( fonction ) {
const interdite = ( nom ) => ( ) => {
throw new Error ( ` ${ nom } lu ` ) ;
} ;
const remplacees = [
[ Math , 'random' ] ,
[ Date , 'now' ] ,
[ performance , 'now' ] ,
[ crypto , 'getRandomValues' ] ,
[ crypto , 'randomUUID' ] ,
] . map ( ( [ objet , nom ] ) => ( { objet , nom , propre : Object . hasOwn ( objet , nom ) , valeur : objet [ nom ] } ) ) ;
const DateReelle = globalThis . Date ;
for ( const { objet , nom } of remplacees ) objet [ nom ] = interdite ( nom ) ;
function DateInterdite ( ) {
throw new Error ( 'Date lu' ) ;
}
DateInterdite . now = interdite ( 'Date.now' ) ;
globalThis . Date = DateInterdite ;
try {
return fonction ( ) ;
} finally {
globalThis . Date = DateReelle ;
for ( const { objet , nom , propre , valeur } of remplacees ) {
if ( propre ) objet [ nom ] = valeur ;
else delete objet [ nom ] ;
}
}
}
// La valeur refaite à toute profondeur, les clés de chaque objet à rebours.
function aRebours ( valeur ) {
if ( Array . isArray ( valeur ) ) return valeur . map ( aRebours ) ;
if ( typeof valeur !== 'object' || valeur === null ) return valeur ;
const copie = { } ;
for ( const cle of Object . keys ( valeur ) . reverse ( ) ) copie [ cle ] = aRebours ( valeur [ cle ] ) ;
return copie ;
}
describe ( 'déterminisme (§ 8.8, § 14.7)' , ( ) => {
// Les résultats de chaque fonction du module sur une charge où se trouvent
// une proposition fautive, une dérive et un retenu, en un seul texte.
function resultats ( charge ) {
const configuration = petite ( ) ;
const [ , coherente ] = charge . propositions ;
const examen = examiner ( charge ) ;
return JSON . stringify ( [
versFichier ( GENERATION [ 0 ] , configuration , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ) ,
charge . propositions . map ( ( proposition ) => fautes ( proposition ) ) ,
derive ( coherente , charge ) ,
formeNommee ( coherente , charge ) ,
planDepuisFichier ( coherente ) ,
[ examen . fautives , examen . derives , examen . retenu , examen . charge . propositions . map ( ( { id } ) => id ) ] ,
] ) ;
}
function chargeMelee ( ) {
const charge = chargeAvecGeneration ( ) ;
charge . propositions [ 0 ] . placement [ 0 ] . reserve . push ( 13 , 2 ) ;
charge . propositions [ 2 ] . placement [ 3 ] . sieges [ 1 ] . pop ( ) ;
charge . retenu = retenuDe ( charge . propositions [ 1 ] ) ;
charge . participants [ 4 ] . exclu = true ;
return charge ;
}
test ( 'ni horloge ni aléa : la sortie de chaque fonction ne dépend que de ses arguments' , ( ) => {
// Le filet est éprouvé d'abord : chaque source lève pendant qu'il est tendu.
for ( const lecture of [
( ) => Math . random ( ) ,
( ) => Date . now ( ) ,
( ) => new Date ( ) ,
( ) => performance . now ( ) ,
( ) => crypto . getRandomValues ( new Uint8Array ( 1 ) ) ,
( ) => crypto . randomUUID ( ) ,
] ) {
assert . throws ( ( ) => sansHorlogeNiAlea ( lecture ) , / lu$/ ) ;
}
assert . equal ( typeof Date . now ( ) , 'number' ) ;
assert . equal ( sansHorlogeNiAlea ( ( ) => resultats ( chargeMelee ( ) ) ) , resultats ( chargeMelee ( ) ) ) ;
} ) ;
test ( "l'ordre des clés reçues n'entre dans aucun résultat, ni dans la clé inconnue nommée" , ( ) => {
const charge = chargeMelee ( ) ;
const inversee = aRebours ( charge ) ;
assert . notEqual ( JSON . stringify ( inversee ) , JSON . stringify ( charge ) ) ;
assert . equal ( resultats ( inversee ) , resultats ( charge ) ) ;
const p = fichierParfait ( ) ;
const zetaPuisAlpha = { ... p , zeta : 1 , alpha : 2 } ;
const alphaPuisZeta = { ... p , alpha : 2 , zeta : 1 } ;
assert . deepStrictEqual ( fautes ( zetaPuisAlpha ) , [ { code : 'FORME' , chemin : 'alpha' } ] ) ;
assert . deepStrictEqual ( fautes ( alphaPuisZeta ) , [ { code : 'FORME' , chemin : 'alpha' } ] ) ;
} ) ;
} ) ;
[ADD] storage: empty chair in a table list when seats are assigned
The operator plans, the room is not tracked live: removing a person from
the retained plan must free her chair and leave every other seat where it
is. With seats assigned, a table list may now hold null before its last
occupant, seat 1 included, and never end on it; without assignment no
list holds null. Validation, canonical form, patches and the named form
handle it, and the delivered demos keep their bytes.
Checked: 1549 node and 58 node-long tests; depot.js, journal.js at 100 %.
--- FR ---
[ADD] stockage : chaise vide dans une liste de table aux sièges attribués
L'opérateur planifie, la salle ne se suit pas en direct : retirer une
personne du plan retenu doit libérer sa chaise et laisser tout autre siège
à sa place. Sièges attribués, une liste de table peut porter null avant son
dernier occupant, siège 1 compris, jamais à la fin ; sans attribution,
aucune liste ne porte null. Contrôle, forme canonique, correctifs et forme
nommée le traitent, et les démonstrations livrées gardent leurs octets.
Vérifié : 1549 node et 58 node-long ; depot.js, journal.js à 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 02:31:26 -04:00
// fichierParfait, sièges attribués, dont le tour 1 laisse vide le siège 2
// de la table 1 : sa liste vaut [12, null, 7], et la personne 2 attend en
// réserve. Une copie neuve à chaque appel.
function fichierAChaiseVide ( ) {
const p = fichierParfait ( ) ;
p . siegesAttribues = true ;
p . placement [ 0 ] = { sieges : [ [ 12 , null , 7 ] , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 10 , 11 , 3 ] ] , reserve : [ 2 ] } ;
return p ;
}
describe ( 'le marqueur de chaise vide : null avant le dernier occupant, sièges attribués (§ 8.9)' , ( ) => {
test ( "une liste [12, null, 7] est cohérente, proposition comme retenu : le null compte dans la longueur, n'est ni DOUBLON ni INCONNU, et planDepuisFichier le retire" , ( ) => {
const p = fichierAChaiseVide ( ) ;
assert . deepStrictEqual ( fautes ( p ) , [ ] ) ;
assert . deepStrictEqual ( fautes ( retenuDe ( p ) ) , [ ] ) ;
assert . deepStrictEqual ( planDepuisFichier ( p ) . tours [ 0 ] , [ [ 7 , 12 ] , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 3 , 10 , 11 ] ] ) ;
assert . deepStrictEqual ( planDepuisFichier ( retenuDe ( p ) ) . tours [ 0 ] [ 0 ] , [ 7 , 12 ] ) ;
// Deux chaises vides au même tour, à deux tables : aucune n'est un
// doublon de l'autre.
const deux = fichierAChaiseVide ( ) ;
deux . placement [ 0 ] = { sieges : [ [ 12 , null , 7 ] , [ 4 , null , 6 ] , [ 1 , 8 , 9 ] , [ 10 , 11 , 3 ] ] , reserve : [ 2 , 5 ] } ;
assert . deepStrictEqual ( fautes ( deux ) , [ ] ) ;
assert . deepStrictEqual ( planDepuisFichier ( deux ) . tours [ 0 ] . slice ( 0 , 2 ) , [ [ 7 , 12 ] , [ 4 , 6 ] ] ) ;
// Le null occupe un siège : quatre éléments sur une capacité de 3.
const longue = fichierAChaiseVide ( ) ;
longue . placement [ 0 ] = { sieges : [ [ 12 , null , 7 , 9 ] , [ 4 , 5 , 6 ] , [ 1 , 8 ] , [ 10 , 11 , 3 ] ] , reserve : [ 2 ] } ;
assert . deepStrictEqual ( fautes ( longue ) , [ { code : 'LONGUEUR' , tour : 1 , table : 1 , declare : 3 , ecrit : 4 } ] ) ;
} ) ;
test ( "une chaise vide au siège 1, [null, 12, 7] ou [null, null, 7], est cohérente, proposition comme retenu : planDepuisFichier la retire, et formeNommee garde le siège de chaque occupant" , ( ) => {
const cas = [
[ [ null , 12 , 7 ] , [ 2 ] , [ 7 , 12 ] , [ ligne ( 1 , 1 , 1 , 2 , 12 , 'Mélisse' , 'Mathurin' ) , ligne ( 1 , 1 , 1 , 3 , 7 , 'Giroflée' , 'Gisèle' ) ] ] ,
[ [ null , null , 7 ] , [ 2 , 12 ] , [ 7 ] , [ ligne ( 1 , 1 , 1 , 3 , 7 , 'Giroflée' , 'Gisèle' ) ] ] ,
] ;
assert . ok ( cas . length > 0 ) ;
for ( const [ liste , reserve , occupants , assis ] of cas ) {
const p = fichierAChaiseVide ( ) ;
p . placement [ 0 ] = { sieges : [ liste , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 10 , 11 , 3 ] ] , reserve } ;
assert . deepStrictEqual ( fautes ( p ) , [ ] , JSON . stringify ( liste ) ) ;
assert . deepStrictEqual ( fautes ( retenuDe ( p ) ) , [ ] , JSON . stringify ( liste ) ) ;
assert . deepStrictEqual ( planDepuisFichier ( p ) . tours [ 0 ] [ 0 ] , occupants ) ;
assert . deepStrictEqual ( planDepuisFichier ( retenuDe ( p ) ) . tours [ 0 ] [ 0 ] , occupants ) ;
const lignes = formeNommee ( p , chargePetite ( ) ) ;
assert . deepStrictEqual ( lignes . filter ( ( { tour , table : id } ) => tour === 1 && id === 1 ) , assis ) ;
assert . deepStrictEqual (
lignes . filter ( ( { tour , reserve : enReserve } ) => tour === 1 && enReserve ) . map ( ( { participant } ) => participant ) ,
reserve ,
) ;
}
} ) ;
test ( "sans attribution, ou en fin de liste, null est une faute FORME qui nomme la chaise ; seul sous sa règle, il ne s'admet nulle part ailleurs dans un placement" , ( ) => {
const cas = [
[ 'sièges non attribués' , ( p ) => { p . siegesAttribues = false ; } , 'placement[0].sieges[0][1]' ] ,
[ 'une liste qui finit par null' , ( p ) => {
p . placement [ 0 ] = { sieges : [ [ 12 , null ] , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 10 , 11 , 3 ] ] , reserve : [ 2 , 7 ] } ;
} , 'placement[0].sieges[0][1]' ] ,
[ 'une liste réduite à null' , ( p ) => {
p . placement [ 1 ] . sieges [ 3 ] = [ null ] ;
p . placement [ 1 ] . reserve = [ 1 , 4 , 7 ] ;
} , 'placement[1].sieges[3][0]' ] ,
[ 'une chaise vide admise au tour 1, puis une fin nulle au tour 3' , ( p ) => {
p . placement [ 2 ] . sieges [ 0 ] = [ 4 , 8 , null ] ;
p . placement [ 2 ] . reserve = [ 10 ] ;
} , 'placement[2].sieges[0][2]' ] ,
[ 'une fin nulle au dernier tour, à la dernière table' , ( p ) => {
p . placement [ 3 ] . sieges [ 3 ] = [ 2 , 6 , null ] ;
p . placement [ 3 ] . reserve = [ 8 ] ;
} , 'placement[3].sieges[3][2]' ] ,
[ 'sièges non attribués, une chaise vide au siège 1' , ( p ) => {
p . siegesAttribues = false ;
p . placement [ 0 ] = { sieges : [ [ null , 7 , 12 ] , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 3 , 10 , 11 ] ] , reserve : [ 2 ] } ;
} , 'placement[0].sieges[0][0]' ] ,
[FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.
--- FR ---
[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines
Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00
[ 'sièges non attribués, deux chaises vides : la première est nommée' , ( p ) => {
p . siegesAttribues = false ;
p . placement [ 0 ] = { sieges : [ [ null , null , 7 ] , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 3 , 10 , 11 ] ] , reserve : [ 2 , 12 ] } ;
} , 'placement[0].sieges[0][0]' ] ,
[ADD] storage: empty chair in a table list when seats are assigned
The operator plans, the room is not tracked live: removing a person from
the retained plan must free her chair and leave every other seat where it
is. With seats assigned, a table list may now hold null before its last
occupant, seat 1 included, and never end on it; without assignment no
list holds null. Validation, canonical form, patches and the named form
handle it, and the delivered demos keep their bytes.
Checked: 1549 node and 58 node-long tests; depot.js, journal.js at 100 %.
--- FR ---
[ADD] stockage : chaise vide dans une liste de table aux sièges attribués
L'opérateur planifie, la salle ne se suit pas en direct : retirer une
personne du plan retenu doit libérer sa chaise et laisser tout autre siège
à sa place. Sièges attribués, une liste de table peut porter null avant son
dernier occupant, siège 1 compris, jamais à la fin ; sans attribution,
aucune liste ne porte null. Contrôle, forme canonique, correctifs et forme
nommée le traitent, et les démonstrations livrées gardent leurs octets.
Vérifié : 1549 node et 58 node-long ; depot.js, journal.js à 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 02:31:26 -04:00
[ 'null dans la réserve' , ( p ) => { p . placement [ 0 ] . reserve = [ 2 , null ] ; } , 'placement[0].reserve[1]' ] ,
[ 'null parmi les participants' , ( p ) => { p . participants [ 11 ] = null ; } , 'participants[11]' ] ,
[ 'null parmi les tables' , ( p ) => { p . tables [ 3 ] = null ; } , 'tables[3]' ] ,
] ;
assert . ok ( cas . length > 0 ) ;
const ecarts = cas . flatMap ( ( [ libelle , abimer , chemin ] ) =>
[ fichierAChaiseVide ( ) , retenuDe ( fichierAChaiseVide ( ) ) ] . flatMap ( ( p ) => {
abimer ( p ) ;
const rendu = fautes ( p ) ;
const attendu = [ { code : 'FORME' , chemin } ] ;
return JSON . stringify ( rendu ) === JSON . stringify ( attendu ) ? [ ] : [ ` ${ libelle } : ${ JSON . stringify ( rendu ) } ` ] ;
} ) ,
) ;
assert . deepEqual ( ecarts , [ ] ) ;
leveTypeError ( ( ) => planDepuisFichier ( { ... fichierAChaiseVide ( ) , siegesAttribues : false } ) , /FORME/ ) ;
} ) ;
test ( 'formeNommee assied 12 au siège 1 et 7 au siège 3, sans ligne pour la chaise vide' , ( ) => {
const lignes = formeNommee ( fichierAChaiseVide ( ) , chargePetite ( ) ) ;
assert . equal ( lignes . length , 48 ) ;
assert . ok ( lignes . every ( ( { participant } ) => participant !== null ) ) ;
assert . deepStrictEqual ( lignes . filter ( ( { tour , table : id } ) => tour === 1 && id === 1 ) , [
ligne ( 1 , 1 , 1 , 1 , 12 , 'Mélisse' , 'Mathurin' ) ,
ligne ( 1 , 1 , 1 , 3 , 7 , 'Giroflée' , 'Gisèle' ) ,
] ) ;
assert . deepStrictEqual (
lignes . filter ( ( { tour , reserve } ) => tour === 1 && reserve ) ,
[ ligne ( 1 , null , null , null , 2 , 'Bruyère' , 'Basile' ) ] ,
) ;
} ) ;
test ( 'versFichier garde les chaises vides du plan reçu quand les sièges sont attribués, et lève sans attribution' , ( ) => {
const plan = {
tables : [ 1 , 2 , 3 , 4 ] ,
tours : [ [ [ 12 , null , 7 ] , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 10 , 11 , 3 ] ] , ... PLAN _PARFAIT _PETITE . tours . slice ( 1 ) ] ,
reserves : [ [ 2 ] , [ ] , [ ] , [ ] ] ,
} ;
const proposition = { ... PARFAITE , plan } ;
const options = { produitVersion : VERSION . affichee , decalage : 0 } ;
const fichier = versFichier ( proposition , petite ( ) , { ... options , attribuerSieges : true } ) ;
assert . deepStrictEqual ( fichier . placement [ 0 ] . sieges , [ [ 12 , null , 7 ] , [ 4 , 5 , 6 ] , [ 1 , 8 , 9 ] , [ 10 , 11 , 3 ] ] ) ;
assert . deepStrictEqual ( fichier , fichierAChaiseVide ( ) ) ;
leveTypeError ( ( ) => versFichier ( proposition , petite ( ) , { ... options , attribuerSieges : false } ) , /FORME/ ) ;
} ) ;
test ( "le fichier relu porte la liste telle quelle : examiner garde la proposition et le retenu sans faute, et écarte la proposition sans attribution qui porte null" , ( ) => {
const charge = chargePetite ( ) ;
const proposition = fichierAChaiseVide ( ) ;
charge . propositions = [ proposition , { ... fichierAChaiseVide ( ) , id : 2 , siegesAttribues : false } ] ;
charge . retenu = retenuDe ( proposition ) ;
charge . prochainsIds . proposition = 3 ;
charge . evenement . etat = 'retenu' ;
const relue = relire ( charge ) ;
assert . deepStrictEqual ( relue . retenu . placement [ 0 ] . sieges [ 0 ] , [ 12 , null , 7 ] ) ;
assert . deepStrictEqual ( relue . propositions [ 0 ] . placement [ 0 ] . sieges [ 0 ] , [ 12 , null , 7 ] ) ;
const examen = examiner ( relue ) ;
assert . deepStrictEqual ( examen . retenu , { fautes : [ ] , raisons : [ ] } ) ;
assert . deepStrictEqual ( examen . charge . propositions , [ fichierAChaiseVide ( ) ] ) ;
assert . deepStrictEqual ( examen . fautives , [
{ rang : 1 , id : 2 , fautes : [ { code : 'FORME' , chemin : 'placement[0].sieges[0][0]' } ] } ,
] ) ;
} ) ;
} ) ;
[ADD] quality page base: Qualité tab, detail, charts, course companion
The quality page gets its section, its data and its building blocks.
The engine details a placement (percentages floored, worst case as a
count, three table-mix counts with their orders, per-round floors); a
Qualité tab, loaded as its own chunk, follows the proposition the plan
shows; charts and their twin tables share one component set. The search
course goes to a companion file written only when propositions change,
keyed by proposition identity, timestamp and placement fingerprint, so a
gesture costs what it did and a stale course never reaches the page.
Checked: node 4929, navigateur 1016, node-long 128; main chunk 689 ko.
--- FR ---
[ADD] base de la qualité : onglet, détail, graphiques, compagnon
La page de qualité reçoit sa section, ses données et ses briques. Le
moteur détaille un placement (pourcentages arrondis vers le bas, pire cas
en compte, trois comptes du mélange par table et leurs ordres, planchers
par tour) ; un onglet Qualité, chargé à part, suit la proposition que
montre le plan ; graphiques et tableaux jumeaux partagent leurs
composants. La course de la recherche va dans un fichier compagnon écrit
quand les propositions changent, clé : identité, horodatage, empreinte du
placement ; un geste coûte ce qu'il coûtait, aucune course périmée.
Vérifié : node 4929, navigateur 1016, longues 128 ; entrée à 689 ko.
Assisted-by: Claude Opus 5.5
2026-10-12 07:42:34 -04:00
// --- La course de la recherche, hors du fichier d'état (§ 8.9, B31) ---------
// Une course inventée sur la petite démonstration, telle que le moteur la
// rend, à evalues mouvements évalués : un point de relevé à chaque
// puissance de dix strictement sous evalues. Le point de rang k reprend les
// tours du plan parfait décalés de k, chaque liste de table à rebours, hors
// de l'ordre croissant ; au premier point, la personne 12 attend en réserve
// au tour 1. Aucun appel à la recherche.
function courseInventee ( evalues ) {
const releves = [ ] ;
for ( let k = 0 , mouvements = 1 ; mouvements < evalues ; k += 1 , mouvements *= 10 ) {
const { tours } = PLAN _PARFAIT _PETITE ;
const decales = tours . map ( ( _ , r ) => tours [ ( r + k ) % tours . length ] . map ( ( liste ) => [ ... liste ] . reverse ( ) ) ) ;
const reserves = decales . map ( ( ) => [ ] ) ;
if ( k === 0 ) {
decales [ 0 ] [ 3 ] = decales [ 0 ] [ 3 ] . filter ( ( id ) => id !== 12 ) ;
reserves [ 0 ] = [ 12 ] ;
}
releves . push ( { mouvements , plan : { tables : [ 1 , 2 , 3 , 4 ] , tours : decales , reserves } } ) ;
}
return { evalues , releves } ;
}
// PARFAITE portant la course inventée à evalues mouvements, 1 234 par
// défaut : quatre points de relevé, à 1, 10, 100 et 1 000.
const parfaiteAvecCourse = ( evalues = 1_234 ) => ( { ... PARFAITE , ... courseInventee ( evalues ) } ) ;
// Le plan reçu, chaque liste de table et chaque réserve par identifiant
// croissant, sans chaise vide : la forme que rend planDepuisFichier.
const planRange = ( { tables , tours , reserves } ) => ( {
tables : [ ... tables ] ,
tours : tours . map ( ( listes ) => listes . map ( ( liste ) => liste . filter ( ( id ) => id !== null ) . sort ( ( a , b ) => a - b ) ) ) ,
reserves : reserves . map ( ( reserve ) => [ ... reserve ] . sort ( ( a , b ) => a - b ) ) ,
} ) ;
// Un plan de configuration forgé sans moteur : à chaque tour, les
// participants non exclus, par identifiant croissant décalé de 37 rangs par
// tour, remplissent les tables dans l'ordre de la configuration, chacune
// jusqu'à sa capacité, chaque liste triée ; ceux qui restent vont à la
// réserve. Il porte chaque participant une fois par tour, et sa forme de
// fichier pèse ce que pèse celle d'un plan du moteur.
function planForge ( configuration ) {
const ids = configuration . participants
. filter ( ( { exclu } ) => exclu !== true )
. map ( ( { id } ) => id )
. sort ( ( a , b ) => a - b ) ;
const tours = [ ] ;
const reserves = [ ] ;
for ( let r = 0 ; r < configuration . tours ; r += 1 ) {
const decalage = ( 37 * r ) % ids . length ;
const ordre = [ ... ids . slice ( decalage ) , ... ids . slice ( 0 , decalage ) ] ;
let rang = 0 ;
tours . push (
configuration . tables . map ( ( { capacite } ) => {
const liste = ordre . slice ( rang , rang + capacite ) . sort ( ( a , b ) => a - b ) ;
rang += liste . length ;
return liste ;
} ) ,
) ;
reserves . push ( ordre . slice ( rang ) . sort ( ( a , b ) => a - b ) ) ;
}
return { tables : configuration . tables . map ( ( { id } ) => id ) , tours , reserves } ;
}
// La proposition de fichier de PARFAITE, sous attribuerSieges, et sa course
// mise en forme de fichier, à evalues mouvements, 1 234 par défaut.
function avecSaCourse ( attribuerSieges = false , evalues = 1_234 ) {
const proposition = parfaiteAvecCourse ( evalues ) ;
const options = { produitVersion : VERSION . affichee , attribuerSieges , decalage : 0 } ;
const fichier = versFichier ( proposition , petite ( ) , options ) ;
return { fichier , course : courseVersFichier ( proposition , fichier , { attribuerSieges } ) } ;
}
describe ( "la course de la recherche hors du fichier d'état (§ 8.9, B31, D53)" , ( ) => {
test ( "versFichier n'écrit aucune course, même quand la proposition du moteur en porte une : la proposition de fichier est celle d'une proposition sans course" , ( ) => {
const options = { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ;
const avec = versFichier ( parfaiteAvecCourse ( ) , petite ( ) , options ) ;
assert . equal ( Object . hasOwn ( avec , 'course' ) , false ) ;
assert . deepStrictEqual ( avec , fichierParfait ( ) ) ;
assert . deepStrictEqual ( versFichier ( { ... PARFAITE , evalues : 0 , releves : [ ] } , petite ( ) , options ) , fichierParfait ( ) ) ;
} ) ;
test ( 'courseVersFichier met la course en forme de fichier : sans attribution, chaque placement de relevé rangé comme celui de la proposition ; avec, dans l’ ordre reçu ; la réserve rangée dans les deux cas' , ( ) => {
const { releves } = courseInventee ( 1_234 ) ;
const nonAttribues = avecSaCourse ( false ) . course ;
const attribues = avecSaCourse ( true ) . course ;
assert . equal ( nonAttribues . evalues , 1_234 ) ;
assert . deepStrictEqual ( nonAttribues . releves . map ( ( { mouvements } ) => mouvements ) , [ 1 , 10 , 100 , 1_000 ] ) ;
nonAttribues . releves . forEach ( ( { placement } , i ) => {
const { tours , reserves } = planRange ( releves [ i ] . plan ) ;
assert . deepStrictEqual ( placement , tours . map ( ( sieges , r ) => ( { sieges , reserve : reserves [ r ] } ) ) , ` relevé ${ i } ` ) ;
} ) ;
attribues . releves . forEach ( ( { placement } , i ) => {
const { tours , reserves } = releves [ i ] . plan ;
assert . deepStrictEqual ( placement , tours . map ( ( sieges , r ) => ( { sieges , reserve : reserves [ r ] } ) ) , ` relevé ${ i } ` ) ;
} ) ;
assert . deepStrictEqual ( Object . keys ( nonAttribues ) , [ 'evalues' , 'releves' ] ) ;
} ) ;
test ( 'sans evalues, courseVersFichier rend null ; à zéro mouvement évalué, {evalues: 0, releves: []}' , ( ) => {
const fichier = fichierParfait ( ) ;
assert . equal ( courseVersFichier ( PARFAITE , fichier , { attribuerSieges : false } ) , null ) ;
const nulle = courseVersFichier ( { ... PARFAITE , evalues : 0 , releves : [ ] } , fichier , { attribuerSieges : false } ) ;
assert . deepStrictEqual ( nulle , { evalues : 0 , releves : [ ] } ) ;
assert . equal ( fauteDeCourse ( fichier , nulle ) , null ) ;
} ) ;
test ( 'aller-retour courseVersFichier → JSON → courseDepuisFichier, sièges attribués ou non : la course reçue, en plans par identifiants rangés' , ( ) => {
for ( const attribuerSieges of [ false , true ] ) {
const { fichier , course } = avecSaCourse ( attribuerSieges ) ;
const relue = JSON . parse ( JSON . stringify ( course ) ) ;
assert . equal ( fauteDeCourse ( fichier , relue ) , null , ` attribuerSieges ${ attribuerSieges } ` ) ;
const { evalues , releves } = courseInventee ( 1_234 ) ;
assert . deepStrictEqual ( courseDepuisFichier ( fichier , relue ) , {
evalues ,
releves : releves . map ( ( { mouvements , plan } ) => ( { mouvements , plan : planRange ( plan ) } ) ) ,
} ) ;
}
const { course } = avecSaCourse ( false , 0 ) ;
assert . deepStrictEqual ( courseDepuisFichier ( fichierParfait ( ) , course ) , { evalues : 0 , releves : [ ] } ) ;
leveTypeError ( ( ) => courseDepuisFichier ( { ... fichierParfait ( ) , tours : 5 } , course ) , /placement fautif/ ) ;
const contredite = { ... avecSaCourse ( false ) . course , evalues : 20_001 } ;
leveTypeError ( ( ) => courseDepuisFichier ( fichierParfait ( ) , contredite ) , /course\.evalues/ ) ;
} ) ;
test ( 'courseDepuisFichier ne partage aucune liste avec la proposition ni avec la course' , ( ) => {
const { fichier , course } = avecSaCourse ( true ) ;
geler ( fichier ) ;
geler ( course ) ;
const { releves } = courseDepuisFichier ( fichier , course ) ;
releves . forEach ( ( { plan } , i ) => {
const { placement } = course . releves [ i ] ;
assert . notEqual ( plan . tables , fichier . tables ) ;
plan . tours . forEach ( ( listes , r ) => {
assert . notEqual ( plan . reserves [ r ] , placement [ r ] . reserve ) ;
listes . forEach ( ( liste , t ) => assert . notEqual ( liste , placement [ r ] . sieges [ t ] ) ) ;
} ) ;
} ) ;
} ) ;
test ( 'fauteDeCourse nomme par son chemin la première contradiction : forme, evalues au-delà de arret, abscisse manquante, en trop, hors d’ ordre ou au dernier point, placement de relevé qui contredit la déclaration' , ( ) => {
// Chaque cas abîme une copie de la course, sous le drapeau donné, et
// nomme le chemin attendu.
const cas = [
[ 'une durée dans un point' , false , ( c ) => { c . releves [ 1 ] . duree = 3 ; } , 'course.releves[1].duree' ] ,
[ 'une valeur mesurée' , false , ( c ) => { c . valeurs = { rencontres : 3 } ; } , 'course.valeurs' ] ,
[ 'evalues au-delà de arret' , false , ( c ) => { c . evalues = 20_001 ; } , 'course.evalues' ] ,
[ 'abscisse manquante au milieu' , false , ( c ) => { c . releves . splice ( 1 , 1 ) ; } , 'course.releves[1].mouvements' ] ,
[ 'abscisse manquante à la fin' , false , ( c ) => { c . releves . pop ( ) ; } , 'course.releves[3]' ] ,
[ 'abscisse en trop' , false , ( c ) => {
c . releves . push ( { ... structuredClone ( c . releves [ 3 ] ) , mouvements : 10_000 } ) ;
} , 'course.releves[4]' ] ,
[ 'abscisses hors d’ ordre' , false , ( c ) => {
const [ a , b ] = [ c . releves [ 1 ] , c . releves [ 2 ] ] ;
c . releves [ 1 ] = b ;
c . releves [ 2 ] = a ;
} , 'course.releves[1].mouvements' ] ,
[ 'abscisse qui n’ est pas une puissance de dix' , false , ( c ) => { c . releves [ 0 ] . mouvements = 5 ; } , 'course.releves[0].mouvements' ] ,
[ 'abscisse 1 000 sous evalues 1 000 : elle est le placement' , false , ( c ) => { c . evalues = 1_000 ; } , 'course.releves[3]' ] ,
[ 'abscisse 1 sous evalues 1' , false , ( c ) => {
c . evalues = 1 ;
c . releves . splice ( 1 ) ;
} , 'course.releves[0]' ] ,
[ 'un tour de moins' , false , ( c ) => { c . releves [ 1 ] . placement . pop ( ) ; } , 'course.releves[1].placement' ] ,
[ 'une liste de table de moins' , false , ( c ) => { c . releves [ 2 ] . placement [ 1 ] . sieges . pop ( ) ; } , 'course.releves[2].placement[1].sieges' ] ,
[ 'une table au-delà de sa capacité' , false , ( c ) => {
const { sieges } = c . releves [ 1 ] . placement [ 0 ] ;
sieges [ 2 ] . push ( sieges [ 3 ] . shift ( ) ) ;
} , 'course.releves[1].placement[0].sieges[2]' ] ,
[ 'un identifiant dupliqué : le second nommé' , false , ( c ) => {
const tour = c . releves [ 2 ] . placement [ 3 ] ;
tour . reserve . push ( tour . sieges [ 1 ] [ 0 ] ) ;
} , 'course.releves[2].placement[3].reserve[0]' ] ,
[ 'un identifiant inconnu' , false , ( c ) => { c . releves [ 3 ] . placement [ 2 ] . sieges [ 0 ] [ 1 ] = 13 ; } , 'course.releves[3].placement[2].sieges[0][1]' ] ,
[ 'un identifiant manquant sans entrée de réserve' , false , ( c ) => {
c . releves [ 0 ] . placement [ 0 ] . reserve = [ ] ;
} , 'course.releves[0].placement[0]' ] ,
[ 'une chaise vide sans attribution' , false , ( c ) => {
c . releves [ 0 ] . placement [ 0 ] . sieges [ 3 ] = [ null , 10 , 11 ] ;
c . releves [ 0 ] . placement [ 0 ] . reserve = [ 12 ] ;
} , 'course.releves[0].placement[0].sieges[3][0]' ] ,
[ 'une chaise vide en fin de liste, sièges attribués' , true , ( c ) => {
const tour = c . releves [ 1 ] . placement [ 2 ] ;
const [ premier ] = tour . sieges [ 0 ] . splice ( 0 , 1 ) ;
tour . sieges [ 0 ] . push ( null ) ;
tour . reserve = [ premier ] ;
} , 'course.releves[1].placement[2].sieges[0][2]' ] ,
] ;
assert . equal ( cas . length , 18 ) ;
const ecarts = cas . flatMap ( ( [ libelle , attribuerSieges , abimer , chemin ] ) => {
const { fichier , course } = avecSaCourse ( attribuerSieges ) ;
const abimee = structuredClone ( course ) ;
abimer ( abimee ) ;
const rendu = fauteDeCourse ( fichier , abimee ) ;
return rendu === chemin ? [ ] : [ ` ${ libelle } : ${ rendu } ` ] ;
} ) ;
assert . deepEqual ( ecarts , [ ] ) ;
for ( const valeur of [ undefined , null , 3 , 'x' , [ ] ] ) assert . equal ( fauteDeCourse ( fichierParfait ( ) , valeur ) , 'course' ) ;
} ) ;
test ( 'les bornes admises : evalues égal à arret, et ses points de relevé exactement les puissances de dix sous lui' , ( ) => {
for ( const evalues of [ 0 , 1 , 2 , 10 , 11 , 1_000 , 1_001 , 20_000 ] ) {
const { fichier , course } = avecSaCourse ( false , evalues ) ;
assert . equal ( course . evalues , evalues ) ;
assert . equal ( fauteDeCourse ( fichier , course ) , null , ` evalues ${ evalues } ` ) ;
}
assert . deepStrictEqual ( avecSaCourse ( false , 20_000 ) . course . releves . map ( ( { mouvements } ) => mouvements ) , [ 1 , 10 , 100 , 1_000 , 10_000 ] ) ;
} ) ;
test ( 'courseVersFichier lève TypeError pour une course qui se contredit, et pour un point de relevé sur d’ autres tables que le plan' , ( ) => {
const fichier = fichierParfait ( ) ;
const options = { attribuerSieges : false } ;
leveTypeError ( ( ) => courseVersFichier ( { ... parfaiteAvecCourse ( ) , evalues : 20_001 } , fichier , options ) , /course\.evalues/ ) ;
// Sans points de relevé, la course n'a pas de releves : la faute nomme
// la clé, et non un point.
for ( const evalues of [ 0 , 1 , 12 ] ) {
leveTypeError ( ( ) => courseVersFichier ( { ... PARFAITE , evalues } , fichier , options ) , /course\.releves\b(?!\[)/ ) ;
}
const autresTables = parfaiteAvecCourse ( ) ;
autresTables . releves [ 1 ] = { ... autresTables . releves [ 1 ] , plan : { ... autresTables . releves [ 1 ] . plan , tables : [ 2 , 1 , 3 , 4 ] } } ;
leveTypeError ( ( ) => courseVersFichier ( autresTables , fichier , options ) , /relevé/ ) ;
} ) ;
test ( 'la taille d’ une course sur la grande démonstration, à la forme réelle : six placements de relevé de 1 040 places, chacun du poids du placement de la proposition' , ( ) => {
const configuration = CATALOGUE . find ( ( { cle } ) => cle === 'grande' ) . construire ( ) ;
const plan = planForge ( configuration ) ;
const releves = [ 1 , 10 , 100 , 1_000 , 10_000 , 100_000 ] . map ( ( mouvements ) => ( { mouvements , plan } ) ) ;
const proposition = { id : 1 , graine : 1 , arret : 200_000 , historique : HISTORIQUE _PAR _DEFAUT , plan , evalues : 200_000 , releves } ;
const fichier = versFichier ( proposition , configuration , { produitVersion : VERSION . affichee , attribuerSieges : false , decalage : 0 } ) ;
const course = courseVersFichier ( proposition , fichier , { attribuerSieges : false } ) ;
assert . equal ( fichier . participants . length * fichier . tours , 1_040 ) ;
const placement = JSON . stringify ( fichier . placement ) . length ;
const octets = JSON . stringify ( course ) . length ;
// Six placements et leurs abscisses : la forme positionnelle, sans
// rien qui nomme une place.
assert . ok ( octets > 6 * placement && octets < 6.1 * placement , ` course ${ octets } o, placement ${ placement } o ` ) ;
assert . ok ( octets > 15_000 && octets < 40_000 , ` course ${ octets } o ` ) ;
} ) ;
} ) ;