diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d17e15..0529231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,6 +166,12 @@ page de qualité. - Le guide d'usage montre la page de qualité, ses vues et ses cas limites, captures à l'appui. +- Dans la liste des événements, la colonne du placement retenu montre ses + « Collisions cumulées (paire et tour) », le même nombre que la colonne du + tableau des propositions, au lieu de « Diff somme d'appartenance ». Le réglage + « Cible de la somme de redondance », qui ne servait qu'à cette colonne, quitte + la section des propositions : un fichier qui le porte s'ouvre sans message, et + le perd au premier enregistrement. **Version de format des fichiers** : 2. Les étiquettes, les objectifs, leurs règles, les cases nouvelles, le mode des objectifs et l'ordre des priorités diff --git a/GUIDE-USAGE.md b/GUIDE-USAGE.md index 76b4238..266180a 100644 --- a/GUIDE-USAGE.md +++ b/GUIDE-USAGE.md @@ -807,7 +807,6 @@ réglage du poste, pour la séance, automatique par défaut (étape 9). | Attribuer les sièges | Génération | décochée | | Objectifs des tables : chercher d'abord | Génération | le plus de places servies | | Ordre des priorités | Génération | écart au plafond a priori, puis excédent de collisions, puis collisions cumulées, puis tours hors objectif, puis rencontres répétées et retours, puis animateurs non rencontrés, puis redondance d'appartenance | -| Cible de la somme de redondance | Propositions | 1 000 | @@ -877,7 +876,6 @@ l’historique ; les noms des exemples sont inventés. | `changerDefaut` | Sièges par défaut : 10 ; 4 tables réalignées | | `changerTours` | Nombre de tours : 3 ; 2 réservations retirées | | `modifierReglage` | Séparer les appartenances : désactivé | -| `changerCibleRedondance` | Cible de la somme de redondance : 1200 | | `poserObjectif` | Table 3 : objectif de la soirée « finance », règle par défaut | | `retirerObjectif` | Table 3, tour 2 : objectif retiré | | `changerRegleObjectifs` | Règle par défaut des objectifs : toutes les étiquettes | diff --git a/doc/captures/manifeste.json b/doc/captures/manifeste.json index 00c765c..216cc90 100644 --- a/doc/captures/manifeste.json +++ b/doc/captures/manifeste.json @@ -18,7 +18,7 @@ {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"4fb4ffe86dad81d901ab14b0e6e64d902f2e15c713c82f81ffb6840ceeff4724","etape":"7","etat":"0229d0d8208ef262e3b3485bc2f606cad900fe487d038276d3733a5423cb6a5b","facteurEchelle":1,"fichier":"09-refus-sur-place","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"31c13e2a0c8b0091bc834c9f93e0fb7451a7c5aab8a567229277862526914075"},"instant":"2026-10-06T19:00:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":15.09,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":7.44,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":14.82,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-liste-texte","ratio":14.82,"role":"texte","seuil":4.5},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":13.81,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.81,"role":"texte","seuil":4.5},{"jeton":"--couleur-poignee","ratio":7.44,"role":"marque","seuil":3},{"jeton":"--couleur-refus","ratio":8.34,"role":"texte","seuil":4.5},{"jeton":"--couleur-selection","ratio":4.57,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":7.44,"role":"marque","seuil":3}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":14.94,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.89,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":14.62,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":15.21,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.5,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":11.07,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5},{"jeton":"--couleur-poignee","ratio":11.32,"role":"marque","seuil":3},{"jeton":"--couleur-refus","ratio":8.14,"role":"texte","seuil":4.5},{"jeton":"--couleur-selection","ratio":4.18,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":5.59,"role":"marque","seuil":3}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Écriture","Chaque geste s’enregistre aussitôt ; « Revenir en lecture » arrête l’écriture.","Revenir en lecture","Écriture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Soirée d’essai","13 participants","2 tables · 18 sièges","3 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Plan","Réservations","Tour affiché","1","2","3","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","9. (libre)","10. (libre)","1. (libre)","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","Table 1, siège 1","Le siège 1 est réservé à une autre personne à tous les tours : déplacer sa réservation libère la place.","Choisir une place","Fermer","1","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","Diagnostic","Chœur de l’Anse","4 personnes : au moins 6 collisions cumulées sur les 3 tours, portées par au moins 2 paires distinctes","Société des Hérons","4 personnes : au moins 6 collisions cumulées sur les 3 tours, portées par au moins 2 paires distinctes","Cercle des Saules","3 personnes : au moins 3 collisions cumulées sur les 3 tours, portées par au moins 1 paire distincte","Tout l'événement","au moins 8 appartenances recroisées par personne","plafond a priori 11 rencontres pour 1 ancré et 11 mobiles","2 retours imposés par 1 ancrage","Listes de travail","Titres de place non pourvus","table 2 : titre","animation","non pourvu","Titres pressentis sans réservation","Gaspard Marbrelle","— titre pressenti","animation","Réserve","Personnes en attente au tour 1","Gaspard Marbrelle","Ysaline Sablonnet","Anselme Givrecœur","Côme Fenouillet","Apolline Coudraie","Basile Lanterneau","Brunœil","Faustine Ormelin","Edgar Ardoisier","Bertille Cressonnier","Table 1, siège 1","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","Personne choisie :","Gaspard Marbrelle","Assigner","Retirer la réservation","Retirer le titre","Oublier le choix","Menu de","Basile Lanterneau","Déplacer","Échanger","Retirer du tour","Déplacer sa réservation","Exclure","Mettre en évidence","Ouvrir la fiche"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"5109fd63a112340134205ad495233ed17d9d2de83b790b1eae38f974a97e85c6","etape":"8","etat":"4d37d30748068354162752743c26aa7e0ab00d6e9b2aa1996b6f9fb410848280","facteurEchelle":1,"fichier":"10-diagnostic","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"633949b83c1d50135c859dad6dfd1815d54d0121963d86c7ff25c26a44904d2f"},"instant":"2026-10-06T19:00:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":13.78,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.81,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":11.07,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Lecture","Liste des événements","Clair","Sombre","À propos","Lecture","Rien ne change ; « Modifier » passe en écriture.","Modifier","Lecture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Grande démonstration 1","260 participants","33 tables · 260 sièges","4 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Diagnostic","Tout l'événement","écart d'itinéraire maximal : plancher 1, au moins 24 mobiles finissent sous leur plafond a priori","aucune relance ne le réduit : il tient aux tables pleines dont les capacités ou les ancrages diffèrent","plafond a priori 24 rencontres pour 4 ancrés","seules les capacités et les réservations le changent : aucune relance ne le dépasse","plafond a priori 28 rencontres pour 29 ancrés et 227 mobiles","seules les capacités et les réservations le changent : aucune relance ne le dépasse","99 retours imposés par 33 ancrages","les réservations les imposent : ils ne comptent pas parmi les retours que choisit le moteur"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"910ac5d3639d7c9ea4b52cfef4e402b5277fdf38379c9a0d196494f43727c5b5","etape":"9","etat":"1025fe8c346e3a503f00abcaf78cf004f33fe2c01a46bebd87da1bbf4d0b3f53","facteurEchelle":1,"fichier":"10a-ordre-priorites","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"bb31be5ff2aee95f48210375982ec1594f4f2d73aabd8f6afb17dbb928b76618"},"instant":"2026-10-06T19:00:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":14.16,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.81,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":11.07,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Écriture","Chaque geste s’enregistre aussitôt ; « Revenir en lecture » arrête l’écriture.","Revenir en lecture","Écriture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Soirée d’essai","13 participants","2 tables · 18 sièges","3 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Génération","Meilleure proposition du classement","Aucune proposition classée : sans proposition comparable, le classement n'en désigne aucune.","Réglages de la génération","Graine (nombre entier, de 0 à 4 294 967 295)","Propositions par génération (nombre de propositions)","Compte d'arrêt (mouvements évalués par proposition)","Historique d'acceptation (longueur, en mouvements)","Contraintes de la génération","Séparer les appartenances","Nouveaux voisins à chaque tour","Nouvelle table à chaque tour","Varier les appartenances rencontrées","Respecter les objectifs des tables","Objectifs des tables : chercher d'abord","le plus de places servies","Le plus grand total de tours servis : le critère compare les tours hors objectif.","chacun au moins une fois","D'abord le plus de personnes servies à un tour au moins, puis le plus grand total : le critère compare les personnes jamais servies, puis les tours hors objectif.","l'équité d'abord","Le minimum d'abord, puis le total : d'abord la personne la plus mal servie, mesurée contre ses propres tours possibles, puis le plus grand total. Le critère compare le plus de tours hors objectif d'une personne, puis les tours hors objectif.","Rencontrer le plus d'animateurs différents","Attribuer les sièges","Ordre des priorités","Deux propositions se comparent palier par palier : un palier ne départage que des propositions égales sur ceux qui le précèdent. Dans un palier de plusieurs critères, chacun compte selon son poids.","Ordre réglé pour l'événement.","Palier 1 sur 6","écart au plafond a priori","Descendre « écart au plafond a priori » d'un palier","Palier 2 sur 6","excédent de collisions","Poids de « excédent de collisions » (nombre entier)","Monter le palier de « excédent de collisions »","Descendre le palier de « excédent de collisions »","Séparer « excédent de collisions » en palier propre","tours hors objectif","Poids de « tours hors objectif » (nombre entier)","Monter le palier de « tours hors objectif »","Descendre le palier de « tours hors objectif »","Séparer « tours hors objectif » en palier propre","Palier 3 sur 6","collisions cumulées","Monter « collisions cumulées » d'un palier","Descendre « collisions cumulées » d'un palier","Joindre « collisions cumulées » au palier du dessus","Palier 4 sur 6","rencontres répétées et retours","Monter « rencontres répétées et retours » d'un palier","Descendre « rencontres répétées et retours » d'un palier","Palier 5 sur 6","animateurs non rencontrés","décoché","Monter « animateurs non rencontrés » d'un palier","Descendre « animateurs non rencontrés » d'un palier","Palier 6 sur 6","redondance d'appartenance","Monter « redondance d'appartenance » d'un palier","Joindre « redondance d'appartenance » au palier du dessus","« tours hors objectif » est au palier 2 sur 6, avec « excédent de collisions ».","Rétablir l'ordre par défaut","Cœurs de calcul","Cœurs automatiques (un par processeur logique du poste)","Valeur automatique sur ce poste : 4 cœurs, un par processeur logique, hyperthreads compris.","Une génération n'emploie jamais plus de cœurs que de propositions demandées. Ce réglage vaut pour cette séance du logiciel sur ce poste : il ne s'enregistre pas dans l'événement.","Aucun calcul n'est encore mesuré sur ce poste pendant cette séance : la durée d'une génération est inconnue.","Générer"],"theme":"clair","versionReelle":"2026.10.05.01"}, -{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"e27a55ff9a61913431bd646b12105d3478fb061b6ab63a7ff0ea6d5f26a7eb5b","etape":"9","etat":"b42451443701c80d433d632788edaab7dd1afea7a57add55a37fcedfdaee98ff","facteurEchelle":1,"fichier":"11-propositions","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"e1bc8ec4b87315960282b99459649f66b90ee4f839c2fd31303ef28ff5ba11d7"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":14.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.81,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":11.02,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Écriture","Chaque geste s’enregistre aussitôt ; « Revenir en lecture » arrête l’écriture.","Revenir en lecture","Écriture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Soirée d’essai","13 participants","2 tables · 18 sièges","3 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Propositions","Propositions classées","Effacer les propositions","Ordre du classement, chaque critère au plus faible d'abord :","écart au plafond a priori",", puis","ensemble,","excédent de collisions","(poids 1)",",","tours hors objectif","(poids 3)",", puis","collisions cumulées",", puis","rencontres répétées",", puis","redondance d'appartenance",", puis","l'identifiant croissant",".","Personnes rencontrées","Collisions d'appartenance","Rencontres répétées et retours","Diversité d'appartenance","Manque et plafond a priori","Objectifs des tables et animateurs","Rang","Proposition","Personnes distinctes rencontrées, minimum : tous","Personnes distinctes rencontrées, minimum : mobiles","Personnes distinctes rencontrées, minimum : ancrés","Collisions cumulées (paire et tour)","Paires distinctes de même appartenance","Excédent de collisions (retrouvailles au-delà de la première)","Rencontres répétées choisies (paires)","Rencontres d'une même paire, au plus","Retours choisis à une table déjà occupée","Appartenances rencontrées A(p), minimum","Redondance d'appartenance, somme Σ r(p)","Taux de diversité d(p), minimum","Plus grand manque (personnes)","Personnes à qui il en manque","Écart d'itinéraire maximal (personnes)","Écart au plafond a priori maximal (personnes)","Tours hors objectif (personne et tour)","Tours selon l'objectif s(p), minimum","Certificat","Imposé par les réservations","Commandes","Placement retenu","3","Tel que la proposition n° 3.","11","11","11","26","15","11","30","3","11","3","85","0,27","0","0","0","0","1","1","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","1","3","retenue","11","11","11","26","15","11","30","3","11","3","85","0,27","0","0","0","0","1","1","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","2","2","11","11","11","33","15","18","54","3","16","3","85","0,27","0","0","0","0","2","1","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","3","5","10","10","10","20","15","5","22","3","12","3","83","0,27","1","1","1","1","1","1","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","4","1","10","10","11","29","15","14","46","3","13","3","82","0,30","1","4","0","1","0","2","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","5","4","10","10","11","31","14","17","46","3","17","3","82","0,30","1","4","0","1","0","2","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","Cible de la somme de redondance","La liste des événements montre, pour le placement retenu, la différence sans signe entre cette cible et la somme Σ r(p) de la colonne « Redondance d'appartenance ». Un nombre entier, 0 ou plus."],"theme":"clair","versionReelle":"2026.10.05.01"}, +{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"51623e46e66ecc4d93173f11996e6b0110a6f79a622553b6c1ea3003779c0f0e","etape":"9","etat":"b42451443701c80d433d632788edaab7dd1afea7a57add55a37fcedfdaee98ff","facteurEchelle":1,"fichier":"11-propositions","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"e1bc8ec4b87315960282b99459649f66b90ee4f839c2fd31303ef28ff5ba11d7"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":14.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.81,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":11.02,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Écriture","Chaque geste s’enregistre aussitôt ; « Revenir en lecture » arrête l’écriture.","Revenir en lecture","Écriture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Soirée d’essai","13 participants","2 tables · 18 sièges","3 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Propositions","Propositions classées","Effacer les propositions","Ordre du classement, chaque critère au plus faible d'abord :","écart au plafond a priori",", puis","ensemble,","excédent de collisions","(poids 1)",",","tours hors objectif","(poids 3)",", puis","collisions cumulées",", puis","rencontres répétées",", puis","redondance d'appartenance",", puis","l'identifiant croissant",".","Personnes rencontrées","Collisions d'appartenance","Rencontres répétées et retours","Diversité d'appartenance","Manque et plafond a priori","Objectifs des tables et animateurs","Rang","Proposition","Personnes distinctes rencontrées, minimum : tous","Personnes distinctes rencontrées, minimum : mobiles","Personnes distinctes rencontrées, minimum : ancrés","Collisions cumulées (paire et tour)","Paires distinctes de même appartenance","Excédent de collisions (retrouvailles au-delà de la première)","Rencontres répétées choisies (paires)","Rencontres d'une même paire, au plus","Retours choisis à une table déjà occupée","Appartenances rencontrées A(p), minimum","Redondance d'appartenance, somme Σ r(p)","Taux de diversité d(p), minimum","Plus grand manque (personnes)","Personnes à qui il en manque","Écart d'itinéraire maximal (personnes)","Écart au plafond a priori maximal (personnes)","Tours hors objectif (personne et tour)","Tours selon l'objectif s(p), minimum","Certificat","Imposé par les réservations","Commandes","Placement retenu","3","Tel que la proposition n° 3.","11","11","11","26","15","11","30","3","11","3","85","0,27","0","0","0","0","1","1","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","1","3","retenue","11","11","11","26","15","11","30","3","11","3","85","0,27","0","0","0","0","1","1","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","2","2","11","11","11","33","15","18","54","3","16","3","85","0,27","0","0","0","0","2","1","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","3","5","10","10","10","20","15","5","22","3","12","3","83","0,27","1","1","1","1","1","1","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","4","1","10","10","11","29","15","14","46","3","13","3","82","0,30","1","4","0","1","0","2","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir","5","4","10","10","11","31","14","17","46","3","17","3","82","0,30","1","4","0","1","0","2","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Qualité","Retenir"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"86a1859ab516d61b63161a9fb476f1d9d64bf356a6a8bd5dd45232ba7d2d8096","etape":"10","etat":"15fb09087464cf5b429c79e33325a0cd64579f1c650d7fd7aeabf6c3cbf977c0","facteurEchelle":1,"fichier":"12-qualite-profil","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"bf3ddc675d33f81c2a5d3d2f13fef4a1a837d9839996d71ebdce9b5d4930b0bb"},"instant":"2026-10-06T19:03:02-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-graphique-axe","ratio":5.64,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-borne","ratio":4.83,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-marque","ratio":9.67,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-repere","ratio":6.15,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-serie-1","ratio":5.1,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-serie-2","ratio":5.57,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":14.16,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.81,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-graphique-axe","ratio":5.38,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-borne","ratio":6.17,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-marque","ratio":10.64,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-repere","ratio":7.01,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-serie-1","ratio":6.89,"role":"marque","seuil":3},{"jeton":"--couleur-graphique-serie-2","ratio":7.83,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":11.23,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Écriture","Chaque geste s’enregistre aussitôt ; « Revenir en lecture » arrête l’écriture.","Revenir en lecture","Écriture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Grande démonstration 1","260 participants","33 tables · 260 sièges","4 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Qualité","Placement mesuré :","Proposition n° 5 : un geste y pose une réservation","Revenir aux réservations","En-tête chiffré","Profil trié des rencontres","Collisions d'appartenance","Appartenances à chaque table","Paires répétées","Objectifs des tables","Animateurs rencontrés","Diversité par animateur","Performance du calcul","En-tête chiffré","Ce placement est-il bon, et qui s'y trouve mal servi ?","Population mesurée : 260 personnes.","Personnes rencontrées","Personnes distinctes rencontrées, minimum : tous","23","95 %","minimum 23 sur 24","chez François Ivrivet","plafond réalisé 24","plafond a priori 24","Personnes distinctes rencontrées, minimum : mobiles","24","Personnes distinctes rencontrées, minimum : ancrés","23","Collisions d'appartenance","Collisions cumulées (paire et tour)","52","Paires distinctes de même appartenance","52","Excédent de collisions (retrouvailles au-delà de la première)","0","Rencontres répétées et retours","Rencontres répétées choisies (paires)","103","0 imposée, à part","Rencontres d'une même paire, au plus","3","Retours choisis à une table déjà occupée","29","99 imposés, à part","Diversité d'appartenance","Appartenances rencontrées A(p), minimum","17","chez François Ivrivet","borne de diversité 24","Redondance d'appartenance, somme Σ r(p)","1 288","Taux de diversité d(p), minimum","0,64","Manque et plafond a priori","Plus grand manque (personnes)","3","chez Marc Luskouze","131 personnes au manque maximal","plafond réalisé 27","Personnes à qui il en manque","131","Écart d'itinéraire maximal (personnes)","4","plancher 1","Écart au plafond a priori maximal (personnes)","4","Objectifs des tables et animateurs","Animateurs rencontrés, minimum","3","ne classe pas","chez Zoé WAMMERWOLM-MAWWEMMEL","borne des animateurs 4","Animateurs non rencontrés","29","ne classe pas","atteignabilité inconnue","Profil trié des rencontres","Qui est le plus mal servi, de combien, et combien sont-ils ?","Profil trié des rencontres, en comptes de personnes","0","10","20","50","100","150","200","250","Personnes, de la moins servie à la mieux servie","rencontres","appartenances rencontrées","plafond réalisé","borne de diversité","rencontres","appartenances rencontrées","plafond réalisé","borne de diversité","ancré","Minimum 23 personnes rencontrées, 3 personnes au minimum, 131 personnes sous leur plafond réalisé.","Profil trié des rencontres, en pourcentage du plafond réalisé","0 %","50 %","100 %","50","100","150","200","250","Personnes, de la moins servie à la mieux servie","rencontres","100 %","rencontres","100 %","ancré","La personne au rang 1, François Ivrivet, rencontre","95 %","minimum 23 sur 24","Voir le tableau","Collisions d'appartenance","D'où vient le problème, et relancer peut-il y changer quelque chose ?","Collisions d'appartenance, par appartenance","0","2","4","6","8","Imprimerie Yzorne","effectif 12, 9 paires distinctes, excédent 0","Fabrique Sorbagne","effectif 11, 7 paires distinctes, excédent 0","Ateliers Falvigne","effectif 8, 3 paires distinctes, excédent 0","Ateliers Rossolde","effectif 11, 3 paires distinctes, excédent 0","Fonderie Estrolle","effectif 9, 3 paires distinctes, excédent 0","Verrerie Vaubrenne","effectif 10, 3 paires distinctes, excédent 0","Ateliers Tavrenne","effectif 8, 2 paires distinctes, excédent 0","Fonderie Xandrelle","effectif 6, 2 paires distinctes, excédent 0","Menuiserie Estrolle","effectif 9, 2 paires distinctes, excédent 0","Tannerie Nervanche","effectif 6, 2 paires distinctes, excédent 0","Ateliers Aubrenc","effectif 3, 1 paire distincte, excédent 0","Ateliers Cassorgue","effectif 3, 1 paire distincte, excédent 0","Brasserie Xandrelle","effectif 2, 1 paire distincte, excédent 0","Comptoir Gravelune","effectif 3, 1 paire distincte, excédent 0","Coopérative Jolvane","effectif 4, 1 paire distincte, excédent 0","Fabrique Kalvenne","effectif 3, 1 paire distincte, excédent 0","Filature Zelvaine","effectif 4, 1 paire distincte, excédent 0","Fonderie Sorbagne","effectif 5, 1 paire distincte, excédent 0","Menuiserie Gravelune","effectif 3, 1 paire distincte, excédent 0","Menuiserie Hostrenne","effectif 5, 1 paire distincte, excédent 0","Menuiserie Quimvaux","effectif 3, 1 paire distincte, excédent 0","Menuiserie Sorbagne","effectif 3, 1 paire distincte, excédent 0","Papeterie Hostrenne","effectif 2, 1 paire distincte, excédent 0","Papeterie Ulvardes","effectif 4, 1 paire distincte, excédent 0","Tannerie Belvarre","effectif 5, 1 paire distincte, excédent 0","Verrerie Aubrenc","effectif 2, 1 paire distincte, excédent 0","Ateliers Estrolle","effectif 3, 0 paire distincte, excédent 0","Ateliers Ivoranche","effectif 4, 0 paire distincte, excédent 0","Brasserie Belvarre","effectif 9, 0 paire distincte, excédent 0","Brasserie Dravenne","effectif 1, 0 paire distincte, excédent 0","Brasserie Lavorneau","effectif 2, 0 paire distincte, excédent 0","Brasserie Morvalle","effectif 2, 0 paire distincte, excédent 0","Brasserie Sorbagne","effectif 4, 0 paire distincte, excédent 0","Brasserie Wassorel","effectif 2, 0 paire distincte, excédent 0","Comptoir Ivoranche","effectif 1, 0 paire distincte, excédent 0","Comptoir Jolvane","effectif 3, 0 paire distincte, excédent 0","Comptoir Lavorneau","effectif 1, 0 paire distincte, excédent 0","Comptoir Sorbagne","effectif 4, 0 paire distincte, excédent 0","Comptoir Xandrelle","effectif 3, 0 paire distincte, excédent 0","Comptoir Yzorne","effectif 2, 0 paire distincte, excédent 0","Coopérative Dravenne","effectif 3, 0 paire distincte, excédent 0","Coopérative Gravelune","effectif 3, 0 paire distincte, excédent 0","Coopérative Morvalle","effectif 3, 0 paire distincte, excédent 0","Coopérative Rossolde","effectif 4, 0 paire distincte, excédent 0","Fabrique Ivoranche","effectif 3, 0 paire distincte, excédent 0","Fabrique Morvalle","effectif 1, 0 paire distincte, excédent 0","Fabrique Ulvardes","effectif 3, 0 paire distincte, excédent 0","Filature Dravenne","effectif 1, 0 paire distincte, excédent 0","Filature Olbrecq","effectif 2, 0 paire distincte, excédent 0","Filature Wassorel","effectif 3, 0 paire distincte, excédent 0","Fonderie Aubrenc","effectif 2, 0 paire distincte, excédent 0","Fonderie Ivoranche","effectif 2, 0 paire distincte, excédent 0","Fonderie Kalvenne","effectif 3, 0 paire distincte, excédent 0","Imprimerie Falvigne","effectif 3, 0 paire distincte, excédent 0","Imprimerie Hostrenne","effectif 1, 0 paire distincte, excédent 0","Menuiserie Aubrenc","effectif 1, 0 paire distincte, excédent 0","Menuiserie Ivoranche","effectif 1, 0 paire distincte, excédent 0","Menuiserie Jolvane","effectif 1, 0 paire distincte, excédent 0","Menuiserie Nervanche","effectif 1, 0 paire distincte, excédent 0","Menuiserie Olbrecq","effectif 4, 0 paire distincte, excédent 0","Menuiserie Tavrenne","effectif 1, 0 paire distincte, excédent 0","Menuiserie Ulvardes","effectif 6, 0 paire distincte, excédent 0","Menuiserie Wassorel","effectif 1, 0 paire distincte, excédent 0","Papeterie Estrolle","effectif 3, 0 paire distincte, excédent 0","Papeterie Wassorel","effectif 1, 0 paire distincte, excédent 0","Tannerie Aubrenc","effectif 3, 0 paire distincte, excédent 0","Tannerie Falvigne","effectif 1, 0 paire distincte, excédent 0","Tannerie Olbrecq","effectif 1, 0 paire distincte, excédent 0","Tannerie Quimvaux","effectif 1, 0 paire distincte, excédent 0","Verrerie Gravelune","effectif 1, 0 paire distincte, excédent 0","Verrerie Ivoranche","effectif 6, 0 paire distincte, excédent 0","Verrerie Nervanche","effectif 1, 0 paire distincte, excédent 0","Verrerie Olbrecq","effectif 2, 0 paire distincte, excédent 0","Verrerie Pravette","effectif 2, 0 paire distincte, excédent 0","reste","prouvé inévitable","reste","L'excédent le plus grand est celui de « Imprimerie Yzorne » : 0 collision.","Voir le tableau","Appartenances à chaque table","Où les mêmes cercles se retrouvent-ils à une même table ?","Ce que la grille compte","appartenances par siège","appartenances par personne assise","doublons parmi les affiliés","Appartenances à chaque table, tables par tours","Table 4","Table 12","Table 16","Table 20","Table 26","Table 33","Table 9","Table 10","Table 15","Table 24","Table 25","Table 29","Table 31","Table 32","Table 1","Table 2","Table 5","Table 6","Table 7","Table 8","Table 13","Table 14","Table 17","Table 18","Table 19","Table 21","Table 22","Table 23","Table 27","Table 28","Table 30","Table 3","Table 11","Tour 1","Tour 2","Tour 3","Tour 4","2","1","0","0","1","2","0","0","0","0","2","1","0","0","1","2","0","2","0","1","0","1","0","2","0","0","2","0","0","1","0","1","1","1","0","0","0","1","1","0","0","1","1","0","1","0","1","0","0","1","0","1","1","0","1","0","0","0","1","0","0","1","0","0","0","0","0","1","1","0","0","0","0","1","0","0","0","0","0","1","0","1","0","0","0","0","0","1","0","0","0","1","1","0","0","0","1","0","0","0","0","0","1","0","0","1","0","0","0","0","0","1","0","1","0","0","0","1","0","0","1","0","0","0","0","0","0","0","0","0","0","0","La table 4 porte la case la plus haute : 2.","Voir le tableau","Paires répétées","Qui revoit trop souvent la même personne ?","Paires 1 à 100 sur 103","Suivantes","Paires que la recherche réunit plus d'une fois","Paire","Rencontres de la paire","Tours","Samuel Cloveval","Menuiserie Ulvardes","Hélène Bralmeval","Brasserie Dravenne","3 fois","Voir au tour 3","Tour 1, table 23","Tour 3, table 15","Tour 4, table 32","Hugo Fralvançon","Brasserie Wassorel","Léa Fralvault","Fonderie Sorbagne","2 fois","Voir au tour 3","Tour 2, table 11","Tour 3, table 2","Hugo Fralvançon","Brasserie Wassorel","Louise Quilmançon","Menuiserie Estrolle","2 fois","Voir au tour 3","Tour 1, table 13","Tour 3, table 2","Zoé WAMMERWOLM-MAWWEMMEL","Menuiserie Ulvardes","Catherine Hulveron","Coopérative Rossolde","2 fois","Voir au tour 4","Tour 2, table 15","Tour 4, table 15","Catherine Urvesson","Fabrique Ulvardes","Olivier Bralmançon","Comptoir Lavorneau","2 fois","Voir au tour 4","Tour 2, table 29","Tour 4, table 6","Karim Hulvivet","Fonderie Xandrelle","Guillaume Luskichon","Brasserie Belvarre","2 fois","Voir au tour 3","Tour 1, table 2","Tour 3, table 2","Léa Fralvault","Fonderie Sorbagne","Lucie Tolvesson","Filature Zelvaine","2 fois","Voir au tour 2","Tour 1, table 15","Tour 2, table 11","Léa Fralvault","Fonderie Sorbagne","Samuel Abrevard","Filature Dravenne","2 fois","Voir au tour 4","Tour 2, table 11","Tour 4, table 4","Fatima Obrivet","Fabrique Sorbagne","Chantal Mirvumet","Menuiserie Gravelune","2 fois","Voir au tour 3","Tour 1, table 33","Tour 3, table 29","Philippe Voskeval","Menuiserie Estrolle","Émilie Korvard","Verrerie Vaubrenne","2 fois","Voir au tour 3","Tour 2, table 6","Tour 3, table 6","Zoé Dulveval","Menuiserie Quimvaux","Fatima Rozvumet","Menuiserie Hostrenne","2 fois","Voir au tour 4","Tour 2, table 15","Tour 4, table 13","Paul Korveux","Filature Olbrecq","Manon Quilmumet","Fonderie Sorbagne","2 fois","Voir au tour 3","Tour 1, table 15","Tour 3, table 5","Paul Korveux","Filature Olbrecq","Geneviève Mirvançon","Brasserie Belvarre","2 fois","Voir au tour 2","Tour 1, table 15","Tour 2, table 3","Thomas Rozvotte","Verrerie Ivoranche","Antoine Quilmeron","Fabrique Kalvenne","2 fois","Voir au tour 4","Tour 3, table 33","Tour 4, table 26","Isabelle Bralmumet","Verrerie Aubrenc","Félix Korveval","Tannerie Nervanche","2 fois","Voir au tour 2","Tour 1, table 19","Tour 2, table 28","Isabelle Bralmumet","Verrerie Aubrenc","Louis Mirveron","Filature Wassorel","2 fois","Voir au tour 4","Tour 3, table 22","Tour 4, table 16","Simon Mirvivet","Ateliers Ivoranche","Hélène Gorzumet","Fonderie Aubrenc","2 fois","Voir au tour 3","Tour 1, table 9","Tour 3, table 18","Lucie Voskeux","Coopérative Morvalle","Samuel Cloveval","Menuiserie Ulvardes","2 fois","Voir au tour 3","Tour 2, table 14","Tour 3, table 15","Fatima Korvouze","Ateliers Tavrenne","Olivier Jusvançon","Brasserie Morvalle","2 fois","Voir au tour 3","Tour 2, table 3","Tour 3, table 3","Étienne Fralvichon","Papeterie Ulvardes","Marc Luskouze","Ateliers Rossolde","2 fois","Voir au tour 4","Tour 1, table 21","Tour 4, table 33","Louise Quilmançon","Menuiserie Estrolle","Paul Obrichon","Ateliers Rossolde","2 fois","Voir au tour 2","Tour 1, table 13","Tour 2, table 21","Louise Quilmançon","Menuiserie Estrolle","Étienne Eskesson","Menuiserie Ulvardes","2 fois","Voir au tour 4","Tour 2, table 21","Tour 4, table 7","Pierre Abreveron","Ateliers Rossolde","Manon Tolveron","Brasserie Belvarre","2 fois","Voir au tour 3","Tour 1, table 5","Tour 3, table 5","Léa Tolvande","Ateliers Rossolde","Anne Abrevesson","Fonderie Estrolle","2 fois","Voir au tour 3","Tour 2, table 2","Tour 3, table 14","Antoine Quilmeron","Fabrique Kalvenne","Samuel Bralmeux","Ateliers Rossolde","2 fois","Voir au tour 3","Tour 2, table 33","Tour 3, table 33","Michel Eskeux","Papeterie Wassorel","Pierre Mirvard","Verrerie Ivoranche","2 fois","Voir au tour 4","Tour 1, table 1","Tour 4, table 21","Louise Hulvault","Coopérative Jolvane","Karim Fralveval","Coopérative Gravelune","2 fois","Voir au tour 4","Tour 3, table 24","Tour 4, table 4","Catherine Hulveron","Coopérative Rossolde","Alexandre Korvotte","Menuiserie Nervanche","2 fois","Voir au tour 4","Tour 3, table 15","Tour 4, table 15","François Ivrivet","Menuiserie Sorbagne","François Gorzichon","Ateliers Estrolle","2 fois","Voir au tour 4","Tour 3, table 32","Tour 4, table 32","Claire Fralvivet","Imprimerie Yzorne","Étienne Selvouze","Ateliers Tavrenne","2 fois","Voir au tour 3","Tour 1, table 14","Tour 3, table 27","Karim Gorzesson","Papeterie Ulvardes","Sophie Clovard","Brasserie Belvarre","2 fois","Voir au tour 4","Tour 1, table 20","Tour 4, table 12","Manon Urvançon","Ateliers Tavrenne","Guillaume Tolvignac","Menuiserie Sorbagne","2 fois","Voir au tour 4","Tour 3, table 1","Tour 4, table 1","Sylvie Quilmande","Imprimerie Yzorne","Antoine Bralmault","Fabrique Sorbagne","2 fois","Voir au tour 4","Tour 1, table 4","Tour 4, table 4","Sylvie Quilmande","Imprimerie Yzorne","Caroline Rozvançon","Brasserie Belvarre","2 fois","Voir au tour 2","Tour 1, table 4","Tour 2, table 14","Sylvie Quilmande","Imprimerie Yzorne","Hélène Rozveval","Comptoir Ivoranche","2 fois","Voir au tour 4","Tour 2, table 14","Tour 4, table 4","Amina Pruvesson","Fonderie Sorbagne","Amina Hulvouze","Coopérative Dravenne","2 fois","Voir au tour 4","Tour 1, table 18","Tour 4, table 16","Amina Pruvesson","Fonderie Sorbagne","Léa Obrard","Comptoir Sorbagne","2 fois","Voir au tour 2","Tour 1, table 18","Tour 2, table 31","Lucie Quilmière","Fabrique Ivoranche","Anne Urvivet","Fonderie Kalvenne","2 fois","Voir au tour 2","Tour 1, table 21","Tour 2, table 9","Louis Selvivet","Menuiserie Hostrenne","Guillaume Hulvotte","Verrerie Vaubrenne","2 fois","Voir au tour 4","Tour 2, table 25","Tour 4, table 25","Lucie Tolvesson","Filature Zelvaine","Manon Quilmumet","Fonderie Sorbagne","2 fois","Voir au tour 4","Tour 1, table 15","Tour 4, table 25","Olivier Quilmesson","Comptoir Gravelune","Jean Neskeron","Filature Zelvaine","2 fois","Voir au tour 3","Tour 2, table 33","Tour 3, table 9","Gabriel Neskault","Menuiserie Tavrenne","Geneviève Abrevignac","Brasserie Morvalle","2 fois","Voir au tour 4","Tour 2, table 20","Tour 4, table 18","Gabriel Neskault","Menuiserie Tavrenne","François Clovotte","Coopérative Rossolde","2 fois","Voir au tour 3","Tour 2, table 20","Tour 3, table 25","Gabriel Neskault","Menuiserie Tavrenne","Chantal Abrevotte","Ateliers Ivoranche","2 fois","Voir au tour 3","Tour 2, table 20","Tour 3, table 25","Olivier Jusvançon","Brasserie Morvalle","Hélène Selveux","Brasserie Lavorneau","2 fois","Voir au tour 4","Tour 1, table 24","Tour 4, table 25","Gabriel Pruveux","Papeterie Estrolle","Guillaume Obrançon","Papeterie Hostrenne","2 fois","Voir au tour 4","Tour 1, table 31","Tour 4, table 27","Manon Clovière","Ateliers Ivoranche","Marc Vandergrolcquesmirtavelle-Ostrovaubelindigues","Tannerie Aubrenc","2 fois","Voir au tour 4","Tour 3, table 10","Tour 4, table 10","François Obrouze","Tannerie Olbrecq","Camille Tolvumet","Fonderie Estrolle","2 fois","Voir au tour 3","Tour 2, table 31","Tour 3, table 31","Claire Gorzeron","Menuiserie Hostrenne","Guillaume Gorzivet","Fonderie Estrolle","2 fois","Voir au tour 4","Tour 1, table 32","Tour 4, table 8","Hélène Luskumet","Brasserie Xandrelle","Julie Quilmignac","Menuiserie Jolvane","2 fois","Voir au tour 3","Tour 2, table 23","Tour 3, table 8","Louis Fralvande","Ateliers Rossolde","Philippe Ivroubre","Ateliers Tavrenne","2 fois","Voir au tour 4","Tour 1, table 19","Tour 4, table 19","Isabelle Korvichon","Menuiserie Ulvardes","Marie Dulvesson","Ateliers Cassorgue","2 fois","Voir au tour 4","Tour 1, table 7","Tour 4, table 10","Isabelle Korvichon","Menuiserie Ulvardes","Karim Clovivet","Fonderie Kalvenne","2 fois","Voir au tour 3","Tour 2, table 18","Tour 3, table 23","Fatima Tolvoubre","Papeterie Ulvardes","Marc Luskouze","Ateliers Rossolde","2 fois","Voir au tour 4","Tour 3, table 8","Tour 4, table 33","Zoé Gorzière","Menuiserie Olbrecq","François Eskard","Fonderie Xandrelle","2 fois","Voir au tour 3","Tour 2, table 7","Tour 3, table 25","Antoine Obreval","Ateliers Cassorgue","Alexandre Ivreux","Tannerie Aubrenc","2 fois","Voir au tour 2","Tour 1, table 25","Tour 2, table 6","Antoine Obreval","Ateliers Cassorgue","Hugo Bralmouze","Tannerie Falvigne","2 fois","Voir au tour 4","Tour 3, table 23","Tour 4, table 23","Michel Korveron","Fonderie Sorbagne","Julie Quilmignac","Menuiserie Jolvane","2 fois","Voir au tour 4","Tour 3, table 8","Tour 4, table 8","Marie Dulvesson","Ateliers Cassorgue","Amina Hulvouze","Coopérative Dravenne","2 fois","Voir au tour 3","Tour 2, table 16","Tour 3, table 17","Youssef Jusvard","Tannerie Aubrenc","Simon Hulvumet","Ateliers Ivoranche","2 fois","Voir au tour 4","Tour 2, table 21","Tour 4, table 21","Gabriel Mirvotte","Tannerie Belvarre","Paul Neskivet","Comptoir Jolvane","2 fois","Voir au tour 4","Tour 3, table 21","Tour 4, table 7","Marc Luskouze","Ateliers Rossolde","Isabelle Selvichon","Tannerie Nervanche","2 fois","Voir au tour 3","Tour 1, table 21","Tour 3, table 8","Louis Hulvichon","Ateliers Falvigne","Nicolas Ivrichon","Fabrique Sorbagne","2 fois","Voir au tour 2","Tour 1, table 17","Tour 2, table 24","Pierre Pruveval","Comptoir Xandrelle","Julie Quilmignac","Menuiserie Jolvane","2 fois","Voir au tour 3","Tour 1, table 10","Tour 3, table 8","Geneviève Abrevignac","Brasserie Morvalle","Pierre Mirvard","Verrerie Ivoranche","2 fois","Voir au tour 3","Tour 1, table 1","Tour 3, table 7","Olivier Bralmançon","Comptoir Lavorneau","Véronique Jusvouze","Verrerie Vaubrenne","2 fois","Voir au tour 4","Tour 1, table 28","Tour 4, table 6","Marc Luskoubre","Verrerie Vaubrenne","Thomas Quilmichon","Fonderie Estrolle","2 fois","Voir au tour 4","Tour 1, table 33","Tour 4, table 5","Camille Jusvumet","Fonderie Estrolle","Guillaume Eskotte","Verrerie Nervanche","2 fois","Voir au tour 3","Tour 1, table 27","Tour 3, table 11","Antoine Selvière","Coopérative Dravenne","Jean Neskeron","Filature Zelvaine","2 fois","Voir au tour 3","Tour 1, table 11","Tour 3, table 9","Antoine Bralmault","Fabrique Sorbagne","Amina Rozvichon","Verrerie Olbrecq","2 fois","Voir au tour 3","Tour 2, table 4","Tour 3, table 4","Antoine Bralmault","Fabrique Sorbagne","Antoine Pruvouze","Imprimerie Yzorne","2 fois","Voir au tour 2","Tour 1, table 4","Tour 2, table 4","Émilie Abrevière","Imprimerie Yzorne","Fatima Hulvard","Ateliers Estrolle","2 fois","Voir au tour 4","Tour 2, table 29","Tour 4, table 29","Émilie Abrevière","Imprimerie Yzorne","Geneviève Obresson","Coopérative Gravelune","2 fois","Voir au tour 3","Tour 1, table 29","Tour 3, table 29","Émilie Abrevière","Imprimerie Yzorne","Chantal Mirvumet","Menuiserie Gravelune","2 fois","Voir au tour 4","Tour 3, table 29","Tour 4, table 29","Olivier Tolvichon","Fonderie Xandrelle","Étienne Gorzeval","Papeterie Ulvardes","2 fois","Voir au tour 3","Tour 1, table 33","Tour 3, table 6","Félix Korveval","Tannerie Nervanche","Thomas Selvignac","Brasserie Sorbagne","2 fois","Voir au tour 3","Tour 2, table 28","Tour 3, table 28","Émilie Dulvichon","Menuiserie Ulvardes","Lucie Voskeron","Verrerie Ivoranche","2 fois","Voir au tour 4","Tour 3, table 10","Tour 4, table 20","Émilie Dulvichon","Menuiserie Ulvardes","Julie Voskesson","Brasserie Belvarre","2 fois","Voir au tour 4","Tour 3, table 10","Tour 4, table 20","Claire Jusvivet","Imprimerie Yzorne","Hugo Voskumet","Ateliers Tavrenne","2 fois","Voir au tour 3","Tour 1, table 20","Tour 3, table 20","Amina Quilmault","Coopérative Jolvane","Philippe Abreveval","Ateliers Falvigne","2 fois","Voir au tour 3","Tour 1, table 3","Tour 3, table 4","Hélène Gorzumet","Fonderie Aubrenc","Émilie Quilmeux","Ateliers Falvigne","2 fois","Voir au tour 4","Tour 1, table 9","Tour 4, table 21","Sophie Ivrault","Coopérative Morvalle","Philippe Fralvignac","Menuiserie Estrolle","2 fois","Voir au tour 4","Tour 3, table 13","Tour 4, table 26","Nicolas Korvande","Comptoir Sorbagne","Chantal Obrignac","Imprimerie Yzorne","2 fois","Voir au tour 3","Tour 2, table 27","Tour 3, table 22","Fatima Urvignac","Ateliers Aubrenc","Claire Bralmotte","Ateliers Tavrenne","2 fois","Voir au tour 3","Tour 2, table 23","Tour 3, table 21","François Clovotte","Coopérative Rossolde","Chantal Abrevotte","Ateliers Ivoranche","2 fois","Voir au tour 3","Tour 2, table 20","Tour 3, table 25","Philippe Dulvoubre","Brasserie Belvarre","Émilie Quilmeux","Ateliers Falvigne","2 fois","Voir au tour 2","Tour 1, table 9","Tour 2, table 10","Chantal Mirvumet","Menuiserie Gravelune","Geneviève Mirvançon","Brasserie Belvarre","2 fois","Voir au tour 4","Tour 2, table 3","Tour 4, table 29","Camille Neskard","Menuiserie Gravelune","Amina Hulvouze","Coopérative Dravenne","2 fois","Voir au tour 4","Tour 2, table 16","Tour 4, table 16","Sophie Pruvière","Ateliers Rossolde","Raphaël Jusvault","Fabrique Sorbagne","2 fois","Voir au tour 2","Tour 1, table 16","Tour 2, table 2","Fatima Abrevançon","Tannerie Nervanche","Caroline Jusvoubre","Fabrique Sorbagne","2 fois","Voir au tour 2","Tour 1, table 13","Tour 2, table 13","Fatima Abrevançon","Tannerie Nervanche","Fatima Rozvumet","Menuiserie Hostrenne","2 fois","Voir au tour 4","Tour 1, table 13","Tour 4, table 13","Karim Clovançon","Fabrique Sorbagne","Caroline Rozvançon","Brasserie Belvarre","2 fois","Voir au tour 4","Tour 2, table 14","Tour 4, table 17","Félix Eskouze","Filature Zelvaine","Sylvie Abreveux","Comptoir Jolvane","2 fois","Voir au tour 3","Tour 1, table 3","Tour 3, table 19","Olivier Korvière","Ateliers Falvigne","Manon Pruvivet","Menuiserie Aubrenc","2 fois","Voir au tour 2","Tour 1, table 20","Tour 2, table 10","Félix Voskande","Ateliers Falvigne","Guillaume Mirvoubre","Comptoir Sorbagne","2 fois","Voir au tour 4","Tour 2, table 7","Tour 4, table 7","Lucie Voskeron","Verrerie Ivoranche","Julie Voskesson","Brasserie Belvarre","2 fois","Voir au tour 4","Tour 3, table 10","Tour 4, table 20","Thomas Selvignac","Brasserie Sorbagne","Étienne Voskançon","Menuiserie Olbrecq","2 fois","Voir au tour 4","Tour 1, table 28","Tour 4, table 28","Jean Neskeron","Filature Zelvaine","Zoé Mirveval","Fabrique Kalvenne","2 fois","Voir au tour 2","Tour 1, table 11","Tour 2, table 33","Sophie Clovard","Brasserie Belvarre","Marc Rozvouze","Ateliers Estrolle","2 fois","Voir au tour 4","Tour 3, table 1","Tour 4, table 12","Alexandre Eskançon","Menuiserie Hostrenne","François Eskard","Fonderie Xandrelle","2 fois","Voir au tour 3","Tour 1, table 17","Tour 3, table 25","Qui a rencontré qui","La matrice n'est pas dessinée : 260 personnes dépassent le seuil de 44. La fiche de chaque personne porte ses rencontres.","Objectifs des tables","Qui n'est jamais assis à une table qui lui correspond, et à quel tour l'objectif perd-il ?","Aucune personne n'est concernée par les objectifs des tables : la vue n'a rien à montrer.","Animateurs rencontrés","Qui a rencontré le moins d'animateurs ?","Animateurs rencontrés, de la personne la moins servie à la mieux servie","0","1","2","3","4","50","100","150","200","rang","animateurs rencontrés","borne des animateurs","animateurs rencontrés","borne des animateurs","Minimum 3 animateurs rencontrés, 29 personnes au minimum.","Voir le tableau","Diversité par animateur","Quel animateur voit le moins de diversité à sa table ?","Aucune personne présente ne porte d'étiquette : seule la colonne des appartenances se dessine.","Appartenances rencontrées en animant","le moins varié","17","le plus varié","27","au minimum","1","Diversité rencontrée en animant, sur la soirée","0","10","20","François Ivrivet","4 tours animés, 23 personnes rencontrées en animant","Samuel Bralmeux","4 tours animés, 23 personnes rencontrées en animant","Geneviève Dulvumet","4 tours animés, 24 personnes rencontrées en animant","François Obrouze","4 tours animés, 23 personnes rencontrées en animant","Antoine Bralmault","4 tours animés, 25 personnes rencontrées en animant","Catherine Hulveron","4 tours animés, 26 personnes rencontrées en animant","Camille Neskard","4 tours animés, 27 personnes rencontrées en animant","Guillaume Luskichon","4 tours animés, 27 personnes rencontrées en animant","Philippe Ivroubre","4 tours animés, 27 personnes rencontrées en animant","Marie Urvard","4 tours animés, 28 personnes rencontrées en animant","Sophie Pruvotte","4 tours animés, 28 personnes rencontrées en animant","Thomas Selvignac","4 tours animés, 26 personnes rencontrées en animant","Philippe Voskeval","4 tours animés, 27 personnes rencontrées en animant","Manon Urvançon","4 tours animés, 27 personnes rencontrées en animant","Marc Vandergrolcquesmir…","4 tours animés, 27 personnes rencontrées en animant","Fatima Abrevançon","4 tours animés, 26 personnes rencontrées en animant","Claire Jusvivet","4 tours animés, 27 personnes rencontrées en animant","Félix Voskande","4 tours animés, 27 personnes rencontrées en animant","Louise Neskichon","4 tours animés, 28 personnes rencontrées en animant","Gabriel Abrevault","4 tours animés, 28 personnes rencontrées en animant","Émilie Abrevière","4 tours animés, 25 personnes rencontrées en animant","Youssef Jusvard","4 tours animés, 27 personnes rencontrées en animant","Manon Tolveron","4 tours animés, 27 personnes rencontrées en animant","Hugo Bralmouze","4 tours animés, 27 personnes rencontrées en animant","Marie Bralmesson","4 tours animés, 28 personnes rencontrées en animant","Louis Selvivet","4 tours animés, 27 personnes rencontrées en animant","Michel Korveron","4 tours animés, 27 personnes rencontrées en animant","Youssef Neskière","4 tours animés, 28 personnes rencontrées en animant","Jean Fralvumet","4 tours animés, 28 personnes rencontrées en animant","Fatima Korvouze","4 tours animés, 27 personnes rencontrées en animant","Gabriel Ivreval","4 tours animés, 28 personnes rencontrées en animant","Guillaume Dulvivet","4 tours animés, 28 personnes rencontrées en animant","Étienne Tolvançon","4 tours animés, 28 personnes rencontrées en animant","Appartenances rencontrées en animant","Appartenances rencontrées en animant","Le moins varié rencontre 17 appartenances en animant ; 1 animateur au minimum.","Voir le tableau","Performance du calcul","Relancer plus longtemps aurait-il donné mieux ?","2 000 mouvements évalués sur 2 000","durée observée 2,5 s","1 cœur de calcul","cadence 4 000 mouvements par seconde","génération de 5 propositions","Valeurs mesurées aux seuls points de relevé, sous les réglages courants, reliées par un trait : entre deux points, rien n'est mesuré.","minimum brut, aux points de relevé","0","5","10","15","20","1","10","100","2 000","Mouvements évalués, échelle logarithmique","minimum brut","minimum brut","Au premier point de relevé, minimum brut : 21 ; au dernier : 23.","écart au plafond a priori, aux points de relevé","0","2","4","6","1","10","100","2 000","Mouvements évalués, échelle logarithmique","écart au plafond a priori","écart au plafond a priori","Au premier point de relevé, écart au plafond a priori : 6 ; au dernier : 4.","excédent de collisions, aux points de relevé","0","1","2","3","4","1","10","100","2 000","Mouvements évalués, échelle logarithmique","excédent de collisions","excédent de collisions","Au premier point de relevé, excédent de collisions : 3 ; au dernier : 0.","collisions cumulées, aux points de relevé","0","20","40","60","1","10","100","2 000","Mouvements évalués, échelle logarithmique","collisions cumulées","collisions cumulées","Au premier point de relevé, collisions cumulées : 67 ; au dernier : 52.","rencontres répétées, aux points de relevé","0","50","100","1","10","100","2 000","Mouvements évalués, échelle logarithmique","rencontres répétées","rencontres répétées","Au premier point de relevé, rencontres répétées : 145 ; au dernier : 103.","redondance d'appartenance, aux points de relevé","0","500","1 000","1","10","100","2 000","Mouvements évalués, échelle logarithmique","redondance d'appartenance","redondance d'appartenance","Au premier point de relevé, redondance d'appartenance : 1 298 ; au dernier : 1 288.","Une valeur a changé au dernier point de relevé, à 2 000 mouvements.","Voir le tableau"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"17901ca2dd320a78adb4011bc7ebe0c7360d548b65179380a1676bd3023c39ff","etape":"10","etat":"b0cdfdc39ba1cb940f71e0266e14706cc8bb2e7808ad0d42f907e320498349cb","facteurEchelle":1,"fichier":"12a-qualite-sans-placement","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"1dadccb14b0f2dbe84b5e6bc60ebe64647db1daa75068fe2648c96f420b8bed5"},"instant":"2026-10-06T19:03:02-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":13.78,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.57,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":11.07,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Lecture","Liste des événements","Clair","Sombre","À propos","Lecture","Rien ne change ; « Modifier » passe en écriture.","Modifier","Lecture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Petite démonstration, conflit inévitable 1","12 participants","4 tables · 12 sièges","4 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Qualité","Placement mesuré :","aucun, le plan ne montre que les réservations","Aucun placement à mesurer : ni proposition ni placement retenu. Le diagnostic dit ce que la configuration rend inévitable.","Diagnostic","Ateliers Jolvane","5 personnes : au moins 4 collisions cumulées sur les 4 tours, portées par au moins 1 paire distincte","Tout l'événement","au moins 5 appartenances recroisées par personne","plafond a priori 8 rencontres pour 12 mobiles"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"7d2742b5210124adf45c7dea1bbcae3b0ee0d237833e12d1eec36fee9b0d35ea","etape":"10","etat":"5d6ecc1054a87ebe7527e876248fd7169cfac07500c8468e939c6e7e85b28e45","facteurEchelle":1,"fichier":"12b-qualite-une-table","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"d2625f8ca7bcab6b3094501d65d44be8999b84a2534dfecacc2219bb24b0c825"},"instant":"2026-10-06T19:03:02-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":15.94,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":6.09,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":9.05,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":14.16,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":5.81,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-action-texte","ratio":8.45,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte-secondaire","ratio":7.6,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte","ratio":11.23,"role":"texte","seuil":4.5},{"jeton":"--couleur-page-texte-secondaire","ratio":6.72,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Écriture","Chaque geste s’enregistre aussitôt ; « Revenir en lecture » arrête l’écriture.","Revenir en lecture","Écriture","Défaire","État du plan","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Une seule table","6 participants","1 table · 6 sièges","3 tours","Préparer","Import","Participants","Tables et tours","Placer","Plan","Diagnostic","Génération","Propositions","Qualité","Produire","Badges","Qualité","Placement mesuré :","Proposition n° 1 : un geste y pose une réservation","Revenir aux réservations","En-tête chiffré","Profil trié des rencontres","Collisions d'appartenance","Appartenances à chaque table","Paires répétées","Objectifs des tables","Animateurs rencontrés","Diversité par animateur","Performance du calcul","En-tête chiffré","Ce placement est-il bon, et qui s'y trouve mal servi ?","Population mesurée : 6 personnes.","Personnes rencontrées","Personnes distinctes rencontrées, minimum : tous","5","100 %","minimum 5 sur 5","chez Iris Ombrelle","plafond réalisé 5","plafond a priori 5","Personnes distinctes rencontrées, minimum : mobiles","5","Personnes distinctes rencontrées, minimum : ancrés","—","Collisions d'appartenance","Collisions cumulées (paire et tour)","9","plancher 9, le meilleur atteint","Paires distinctes de même appartenance","3","plancher 3, le meilleur atteint","Excédent de collisions (retrouvailles au-delà de la première)","6","plancher 6, le meilleur atteint","Rencontres répétées et retours","Rencontres répétées choisies (paires)","15","0 imposée, à part","Rencontres d'une même paire, au plus","3","Retours choisis à une table déjà occupée","12","0 imposé, à part","Diversité d'appartenance","Appartenances rencontrées A(p), minimum","3","chez Iris Ombrelle","borne de diversité 3","Redondance d'appartenance, somme Σ r(p)","12","Taux de diversité d(p), minimum","0,60","Manque et plafond a priori","Plus grand manque (personnes)","0","chez Iris Ombrelle","0 personne au manque maximal","plafond réalisé 5","Personnes à qui il en manque","0","Écart d'itinéraire maximal (personnes)","0","Écart au plafond a priori maximal (personnes)","0","minimum atteint","Profil trié des rencontres","Qui est le plus mal servi, de combien, et combien sont-ils ?","Une seule table : chacun y rencontre tous les autres, et le profil dirait la même chose de chacun.","Collisions d'appartenance","D'où vient le problème, et relancer peut-il y changer quelque chose ?","Une seule table : chacun y retrouve chaque membre de son appartenance, à chaque tour.","Appartenances à chaque table","Où les mêmes cercles se retrouvent-ils à une même table ?","Une seule table : elle réunit toutes les appartenances à chaque tour.","Paires répétées","Qui revoit trop souvent la même personne ?","Une seule table : chaque paire se revoit à chaque tour.","Objectifs des tables","Qui n'est jamais assis à une table qui lui correspond, et à quel tour l'objectif perd-il ?","Aucune personne n'est concernée par les objectifs des tables : la vue n'a rien à montrer.","Animateurs rencontrés","Qui a rencontré le moins d'animateurs ?","Aucune personne n'est concernée par les animateurs, ou aucun animateur n'est désigné : la vue n'a rien à montrer.","Diversité par animateur","Quel animateur voit le moins de diversité à sa table ?","Aucun animateur n'est désigné : la vue n'a rien à montrer.","Performance du calcul","Relancer plus longtemps aurait-il donné mieux ?","300 mouvements évalués sur 300","durée inconnue","1 cœur de calcul","génération de 5 propositions","Valeurs mesurées aux seuls points de relevé, sous les réglages courants, reliées par un trait : entre deux points, rien n'est mesuré.","minimum brut, aux points de relevé","0","2","4","1","10","100","300","Mouvements évalués, échelle logarithmique","minimum brut","minimum brut","Au premier point de relevé, minimum brut : 5 ; au dernier : 5.","écart au plafond a priori, aux points de relevé","0","1","10","100","300","Mouvements évalués, échelle logarithmique","écart au plafond a priori","écart au plafond a priori","Au premier point de relevé, écart au plafond a priori : 0 ; au dernier : 0.","excédent de collisions, aux points de relevé","0","2","4","6","1","10","100","300","Mouvements évalués, échelle logarithmique","excédent de collisions","excédent de collisions","Au premier point de relevé, excédent de collisions : 6 ; au dernier : 6.","collisions cumulées, aux points de relevé","0","2","4","6","8","1","10","100","300","Mouvements évalués, échelle logarithmique","collisions cumulées","collisions cumulées","Au premier point de relevé, collisions cumulées : 9 ; au dernier : 9.","rencontres répétées, aux points de relevé","0","5","10","15","1","10","100","300","Mouvements évalués, échelle logarithmique","rencontres répétées","rencontres répétées","Au premier point de relevé, rencontres répétées : 15 ; au dernier : 15.","redondance d'appartenance, aux points de relevé","0","5","10","1","10","100","300","Mouvements évalués, échelle logarithmique","redondance d'appartenance","redondance d'appartenance","Au premier point de relevé, redondance d'appartenance : 12 ; au dernier : 12.","Aucune valeur n'a changé entre 1 et 300 mouvements ; une autre graine est l'autre façon de chercher.","Voir le tableau"],"theme":"clair","versionReelle":"2026.10.05.01"}, diff --git a/scripts/documentation.js b/scripts/documentation.js index 0e3db48..12cf69f 100644 --- a/scripts/documentation.js +++ b/scripts/documentation.js @@ -373,7 +373,6 @@ export const EXEMPLES_COMMANDES = Object.freeze({ changerDefaut: { sieges: 10, realignees: 4 }, changerTours: { tours: 3, retirees: 2 }, modifierReglage: { cle: 'separerAppartenances', valeur: false }, - changerCibleRedondance: { cible: 1200 }, changerEtiquettes: { personne: 'Iris Ombrelle', ajoutees: ['finance'], retirees: [] }, poserObjectif: { numero: 3, tour: null, regle: null, etiquettes: ['finance'] }, retirerObjectif: { numero: 3, tour: 2 }, diff --git a/spec.md b/spec.md index d45dcc1..c394326 100644 --- a/spec.md +++ b/spec.md @@ -199,8 +199,8 @@ table, les objectifs des tables tour par tour, les animateurs, et la performance du calcul qui a produit la proposition. Le logiciel sépare ce que le moteur a décidé de ce que les réservations imposent (§ 5.4). Retenir fait passer le plan à l'état *Retenu* (§ 9) ; les propositions non retenues restent disponibles. Le -retenu se résume ensuite dans la liste des événements, par la différence absolue -de sa somme de redondance à la cible de l'événement (§ 8.1). +retenu se résume ensuite dans la liste des événements, par ses collisions +cumulées (§ 5.4, § 8.1). **11. Ajuster à la main.** *Facultative, reprenable.* Le même geste qu'à l'étape 7 déplace maintenant une personne réelle dans le placement retenu. **Le @@ -1905,35 +1905,32 @@ en-têtes de colonnes se groupent en « Événement », « Placement retenu » e « Meilleure proposition du classement », et le nom de chaque ligne en est l'en-tête, la date et le chemin en petite ligne dessous. Sous le tableau, des cartes offrent la création, le dossier de travail et les démonstrations. Une -colonne résume en plus le **placement retenu** : « Diff somme d'appartenance », -|cible − Σ r(p)|, où Σ r(p) est la somme de redondance du retenu — le nombre de -la colonne « Redondance d'appartenance, somme Σ r(p) » du tableau des -propositions — et la cible celle de l'événement (§ 8.9), 1000 par défaut, -réglable dans la section des propositions. Elle se lit dans la même mesure que -le tableau, sans rien écrire ; un événement sans retenu, ou dont le retenu est -fautif ou en dérive (§ 8.9, § 9), y porte « — ». Chaque ligne porte aussi la -**« Meilleure proposition du classement »**, sous les libellés du bloc que la -section de la génération montre en tête : l'identifiant de la proposition au -rang 1 du classement (§ 5.7) — la même proposition que ce bloc et que la -première ligne du tableau des propositions, retenue ou non ; une proposition -fautive, que l'ouverture écarte (§ 8.9, point 3), n'y entre pas. Sans -proposition classée — aucune, ou aucune qui décrive encore la configuration —, -elle vaut « — ». La liste ne montre les collisions cumulées ni du retenu ni de -la meilleure proposition : le tableau des propositions et le bloc de la -génération les portent. Chaque intitulé s'explique dans une infobulle, posée -sur l'en-tête de sa colonne. -**La liste paraît avant ses colonnes calculées** : ses lignes s'affichent dès le -relevé du dossier, et les mesures du retenu et la meilleure proposition, qui -évaluent chaque événement, se calculent ensuite, un événement après l'autre, la -main rendue au navigateur entre deux. Tant que celles d'une ligne se calculent, -chacune de ses valeurs porte un signe de chargement qui tourne ; chaque ligne -montre les siennes dès qu'elles sont obtenues, et une liste relue abandonne les -calculs de la précédente. -**Un répertoire vide n'affiche pas une liste vide** : il présente la commande de -création et les démonstrations — ni tuiles, ni tableau, ni résumé —, car une -liste vide sans issue est le premier écran que voit un opérateur qui lance -l'exécutable. Un filtre qui ne retient aucune ligne le dit, sans laisser un -tableau vide. +colonne résume en plus le **placement retenu** : « Collisions cumulées (paire et +tour) », les collisions cumulées du retenu (§ 5.4) — le nombre que la colonne du +même intitulé du tableau des propositions porte sur la ligne du retenu. Elle se +lit dans la même mesure que le tableau, sans rien écrire ; un événement sans +retenu, ou dont le retenu est fautif ou en dérive (§ 8.9, § 9), y porte « — ». +Chaque ligne porte aussi la **« Meilleure proposition du classement »**, sous +les libellés du bloc que la section de la génération montre en tête : +l'identifiant de la proposition au rang 1 du classement (§ 5.7) — la même +proposition que ce bloc et que la première ligne du tableau des propositions, +retenue ou non ; une proposition fautive, que l'ouverture écarte (§ 8.9, point +3), n'y entre pas. Sans proposition classée — aucune, ou aucune qui décrive +encore la configuration —, elle vaut « — ». La liste ne montre pas les +collisions cumulées de la meilleure proposition : le tableau des propositions et +le bloc de la génération les portent. Chaque intitulé s'explique dans une +infobulle, posée sur l'en-tête de sa colonne. **La liste paraît avant ses +colonnes calculées** : ses lignes s'affichent dès le relevé du dossier, et les +mesures du retenu et la meilleure proposition, qui évaluent chaque événement, se +calculent ensuite, un événement après l'autre, la main rendue au navigateur +entre deux. Tant que celles d'une ligne se calculent, chacune de ses valeurs +porte un signe de chargement qui tourne ; chaque ligne montre les siennes dès +qu'elles sont obtenues, et une liste relue abandonne les calculs de la +précédente. **Un répertoire vide n'affiche pas une liste vide** : il présente la +commande de création et les démonstrations — ni tuiles, ni tableau, ni résumé —, +car une liste vide sans issue est le premier écran que voit un opérateur qui +lance l'exécutable. Un filtre qui ne retient aucune ligne le dit, sans laisser +un tableau vide. Le logiciel n'ouvre **qu'un événement à la fois**, et ferme le précédent sans question : il n'y a rien à enregistrer. @@ -2004,6 +2001,12 @@ retour est **l'entrée qu'elle vise**. *Défaire* vise le prédécesseur sur ce remonter son fil et atterrit sur un fil mort obtient un état légitime, jamais celui qu'il visait. +**Défaire et refaire avancent toujours d'un pas.** Quand l'instant visé a le +même contenu que l'instant courant, leur entrée s'écrit quand même : c'est elle +qui déplace le fil, et un refus laisserait chaque nouveau « Défaire » buter sur +le même instant. Revenir à un tel instant, choisi dans la liste, ne change rien +et ne s'écrit pas. + **Les jalons.** L'opérateur peut **nommer l'instant courant**, et filtrer l'historique sur les instants nommés. Nommer ne modifie pas le contenu : c'est une **ligne de marque**, pas une entrée, sinon l'équivalence du § 8.2 serait @@ -2514,19 +2517,21 @@ retrouve-t-il ?* | participants, tables, tours, réservations, titres de place, filiation | **champs nommés**, un objet par chose | c'est de la saisie d'opérateur : perdue, elle est perdue, et elle doit se lire à l'œil quand un fichier s'abîme. Elle est petite — de l'ordre de 40 ko pour 260 personnes avec courriels | | placements engendrés | **forme positionnelle** | c'est la partie volumineuse, et la seule qui se **recalcule** : une proposition perdue coûte une relance | -#### Un réglage facultatif : la cible de la somme de redondance +#### Un champ ancien, oublié : la cible de la somme de redondance -Les réglages de l'événement portent un champ **facultatif**, `cibleRedondance`, -un entier ≥ 0 : la cible à laquelle la liste des événements compare la somme de -redondance Σ r(p) du retenu (§ 8.1). **Absent, il vaut 1000.** Un fichier qui ne -le porte pas — chaque fichier écrit avant lui, chaque démonstration livrée — se -lit à la cible 1000, et s'écrit sans lui. Une valeur négative, non entière, hors -des entiers exacts ou d'une autre sorte est une faute de forme du fichier -(§ 8.8). La commande « Cible de la somme de redondance », un champ de la section -des propositions, le pose — une entrée d'historique, refusée en lecture et sur -un plan bloqué — et le retire quand la cible revient à 1000 : deux fichiers de -même cible s'écrivent pareil. Un correctif du journal (§ 8.6) le pose quand il -apparaît et le retire quand il disparaît. +Un fichier écrit par une construction antérieure peut porter, dans les +réglages de l'événement, un champ `cibleRedondance` : la cible à laquelle la +liste des événements comparait la somme de redondance Σ r(p) du retenu. La +liste montre les collisions cumulées du retenu (§ 8.1), et ce champ ne sert +plus. **Il est oublié** : un fichier qui le porte se lit sans faute de forme, sans +refus ni avertissement, quelle que soit sa valeur ; la charge lue ne le porte +pas, et le premier geste écrit le fichier sans lui. Aucune commande ne le pose. +Un correctif du journal (§ 8.6) qui le pose ou le retire, qu'un journal plus +ancien porte, s'applique sans rien changer : chaque instant se restitue sans +lui. L'instant qui le posait ou le retirait se restitue donc comme son +voisin : le défaire ou le refaire s'écrit quand même, d'une entrée qui ne +change rien, et le fil avance d'un pas (§ 8.3). Hors des réglages, la même clé +reste une clé inconnue, et une faute de forme. #### Deux champs facultatifs : le placement daté @@ -2552,7 +2557,7 @@ là, et nulle part ailleurs : le pied des pages, la feuille de contrôle, le bad Chacun est **absent tant qu'il vaut son défaut** et s'écrit après les clés du format 1 de son objet, dans l'ordre du tableau qui suit — pour les réglages, -`cibleRedondance`, `regleObjectifs`, `respecterObjectifs`, `modeObjectifs`, +`regleObjectifs`, `respecterObjectifs`, `modeObjectifs`, `rencontrerAnimateurs`, `priorites` : un événement qui n'emploie ni étiquette, ni objectif, ni ordre réglé s'écrit comme un fichier du format 1, au numéro de format près. Chacun se pose par une commande — une entrée d'historique, refusée diff --git a/src/application/commandes.test.js b/src/application/commandes.test.js index 38264c8..9710a67 100644 --- a/src/application/commandes.test.js +++ b/src/application/commandes.test.js @@ -987,7 +987,6 @@ describe('commandes : la charge reçue ne change jamais (§ 13.2)', () => { ['changerDefaut', { sieges: 3, realigner: false }], ['changerTours', { tours: 3 }], ['modifierReglage', { cle: 'nouveauxVoisins', valeur: false }], - ['changerCibleRedondance', { cible: '1200' }], ['changerEtiquettes', { id: 3, ajouter: 'finance' }], ['poserObjectif', { table: 2, tour: 1, etiquettes: 'finance', regle: 'et' }], ['retirerObjectif', { table: 2, tour: null }], diff --git a/src/application/commandes_mobilier.js b/src/application/commandes_mobilier.js index 83e67da..fc71552 100644 --- a/src/application/commandes_mobilier.js +++ b/src/application/commandes_mobilier.js @@ -2,10 +2,10 @@ // License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // Le registre des commandes du mobilier — tables, sièges, tours, réglages, -// cible de la somme de redondance, objectifs des tables et leur règle par -// défaut, ordre des priorités et mode des objectifs (§ 4, § 4.5, § 5.3, -// § 5.7, § 6, § 7.1, § 8.2, § 8.9, § 9), que commandes.js réunit aux autres. Ses imports n'atteignent jamais commandes.js : le registre -// réuni dépend de lui, non l'inverse. +// objectifs des tables et leur règle par défaut, ordre des priorités et +// mode des objectifs (§ 4, § 4.5, § 5.3, § 5.7, § 6, § 7.1, § 8.2, § 8.9, +// § 9), que commandes.js réunit aux autres. Ses imports n'atteignent jamais +// commandes.js : le registre réuni dépend de lui, non l'inverse. // // Chaque commande suit le contrat de commandes.js : une fonction pure // (charge, arguments) → { charge, libelle, avertissements }, la charge reçue @@ -27,11 +27,9 @@ import { caseLibre, geometrieTable, listeSurTable } from '../geometrie/tables.js'; import { CRITERES, MODES_OBJECTIFS, POIDS_MAX, seulsASonPalier, verifierPriorites } from '../moteur/criteres.js'; import { - CIBLE_REDONDANCE_PAR_DEFAUT, SCHEMA, TOURS_MAX, capacite, - cibleRedondance, critereCoche, modeObjectifs, priorites, @@ -377,27 +375,6 @@ function modifierReglage(charge, { cle, valeur } = {}) { return { charge: suivante, libelle: libelle('ENTREE_MODIFIER_REGLAGE', { cle, valeur }), avertissements: [] }; } -/** - * changerCibleRedondance {cible} (§ 5.4, § 8.1, § 8.9) : la cible de la - * somme de redondance de l'événement, que la liste des événements compare à - * Σ r(p) du retenu ; une saisie lue par lireEntier, un entier ≥ 0, NON_ENTIER - * sinon — un signe moins compris —, remède null. La valeur par défaut, - * CIBLE_REDONDANCE_PAR_DEFAUT, retire le champ des réglages : la charge - * s'écrit alors comme une charge qui ne l'a jamais porté. La même valeur que - * la cible courante, lue par cibleRedondance, est SANS_EFFET, champ écrit ou - * non : une cible de 1000 écrite au fichier ne se retire pas par une saisie - * qui ne change rien. - */ -function changerCibleRedondance(charge, { cible: saisie } = {}) { - exigerModifiable(charge); - const cible = entierSaisi(saisie); - if (cible === cibleRedondance(charge)) throw refus('SANS_EFFET', { commande: 'changerCibleRedondance' }); - const suivante = structuredClone(charge); - if (cible === CIBLE_REDONDANCE_PAR_DEFAUT) delete suivante.reglages.cibleRedondance; - else suivante.reglages.cibleRedondance = cible; - return { charge: suivante, libelle: libelle('ENTREE_CHANGER_CIBLE_REDONDANCE', { cible }), avertissements: [] }; -} - // L'enregistrement de la table d'identifiant id qu'un objectif vise, ou // TABLE_DISPARUE {table}, remède null : aucun geste ne rend une table // supprimée. @@ -715,7 +692,6 @@ export const COMMANDES_MOBILIER = new Map([ ['changerDefaut', changerDefaut], ['changerTours', changerTours], ['modifierReglage', modifierReglage], - ['changerCibleRedondance', changerCibleRedondance], ['poserObjectif', poserObjectif], ['retirerObjectif', retirerObjectif], ['changerRegleObjectifs', changerRegleObjectifs], diff --git a/src/application/commandes_mobilier.test.js b/src/application/commandes_mobilier.test.js index 59961f0..ddd6e85 100644 --- a/src/application/commandes_mobilier.test.js +++ b/src/application/commandes_mobilier.test.js @@ -24,7 +24,6 @@ import { TOURS_MAX, analyser, capacite, - cibleRedondance, configurationDepuisCharge, creerCharge, modeObjectifs, @@ -217,7 +216,7 @@ async function seanceSur(charge) { // --- Les épreuves ----------------------------------------------------------- describe('mobilier : le registre (§ 8.2)', () => { - test('COMMANDES_MOBILIER porte les quatorze commandes du mobilier, réunies dans COMMANDES', () => { + test('COMMANDES_MOBILIER porte les treize commandes du mobilier, réunies dans COMMANDES', () => { const noms = [ 'creerTable', 'supprimerTable', @@ -227,7 +226,6 @@ describe('mobilier : le registre (§ 8.2)', () => { 'changerDefaut', 'changerTours', 'modifierReglage', - 'changerCibleRedondance', 'poserObjectif', 'retirerObjectif', 'changerRegleObjectifs', @@ -250,7 +248,6 @@ describe('mobilier : le registre (§ 8.2)', () => { ['changerDefaut', { sieges: 6, realigner: false }], ['changerTours', { tours: 3 }], ['modifierReglage', { cle: 'nouveauxVoisins', valeur: false }], - ['changerCibleRedondance', { cible: '1200' }], ['poserObjectif', { table: 1, tour: null, etiquettes: 'finance', regle: null }], ['retirerObjectif', { table: 1, tour: null }], ['changerRegleObjectifs', { regle: 'et' }], @@ -1151,57 +1148,23 @@ describe('mobilier : les réglages (§ 5.3, § 15.4)', () => { }); }); -describe('mobilier : la cible de la somme de redondance (§ 5.4, § 8.1, § 8.9)', () => { - test('« Cible de la somme de redondance : 1200 », une entrée ; défaire rend la charge sans le champ', async () => { - const { seance, entrees } = await seanceSur(chargeDe({ tables: [{ id: 1 }], n: 2 })); - const avant = entrees(); - assert.equal(Object.hasOwn(seance.charge.reglages, 'cibleRedondance'), false); - const rendu = await seance.executer('changerCibleRedondance', { cible: '1200' }); - assert.equal(rendu.libelle, 'Cible de la somme de redondance : 1200'); - assert.equal(entrees() - avant, 1); - assert.equal(seance.charge.reglages.cibleRedondance, 1200); - await seance.defaire(); - assert.equal(Object.hasOwn(seance.charge.reglages, 'cibleRedondance'), false); - assert.equal(cibleRedondance(seance.charge), 1000); - }); - - test('la saisie se lit par lireEntier, 0 compris ; revenir à 1000 retire le champ, et la charge s’écrit comme sans lui', () => { +describe('mobilier : la cible de la somme de redondance n’est plus un réglage (§ 8.9)', () => { + test('aucune commande ne la pose : ni changerCibleRedondance au registre, ni modifierReglage sous sa clé', () => { + assert.equal(COMMANDES.has('changerCibleRedondance'), false); + assert.equal(COMMANDES_MOBILIER.has('changerCibleRedondance'), false); const charge = chargeDe({ tables: [{ id: 1 }], n: 2 }); - const zero = accepter('changerCibleRedondance', charge, { cible: ' 0 ' }); - assert.equal(zero.charge.reglages.cibleRedondance, 0); - assert.equal(zero.libelle, 'Cible de la somme de redondance : 0'); - const nombre = accepter('changerCibleRedondance', charge, { cible: 750 }); - assert.equal(nombre.charge.reglages.cibleRedondance, 750); - const retour = accepter('changerCibleRedondance', nombre.charge, { cible: '1000' }); - assert.equal(Object.hasOwn(retour.charge.reglages, 'cibleRedondance'), false); - assert.equal(retour.libelle, 'Cible de la somme de redondance : 1000'); - assert.equal(serialiser(retour.charge, { revision: 1, produitVersion: V1 }), serialiser(charge, { revision: 1, produitVersion: V1 })); + assert.throws(() => COMMANDES.get('modifierReglage')(charge, { cle: 'cibleRedondance', valeur: 1200 }), TypeError); }); - test('une saisie négative, décimale ou vide est refusée NON_ENTIER {saisie}, remède null ; la même valeur, SANS_EFFET', () => { + test('une charge qui la porte encore se modifie par les autres commandes, et s’écrit sans elle', () => { const charge = chargeDe({ tables: [{ id: 1 }], n: 2 }); - const saisies = ['-5', '2,5', '', 'mille']; - assert.ok(saisies.length > 0); - for (const saisie of saisies) { - refuse(() => COMMANDES.get('changerCibleRedondance')(charge, { cible: saisie }), 'NON_ENTIER', { saisie, remede: null }); - } - refuse(() => appliquerCommande('changerCibleRedondance', charge, { cible: '1000' }), 'SANS_EFFET', { - commande: 'changerCibleRedondance', - }); - const posee = accepter('changerCibleRedondance', charge, { cible: '640' }).charge; - refuse(() => appliquerCommande('changerCibleRedondance', posee, { cible: '640' }), 'SANS_EFFET'); - assert.throws(() => COMMANDES.get('changerCibleRedondance')(charge, { cible: null }), TypeError); - }); - - test('une cible de 1000 écrite au fichier, saisie de nouveau à 1000 : SANS_EFFET, sans retirer le champ ni faire d’entrée', () => { - const explicite = chargeDe({ tables: [{ id: 1 }], n: 2 }); - explicite.reglages.cibleRedondance = 1000; - analyser(serialiser(explicite, { revision: 1, produitVersion: V1 })); - refuse(() => appliquerCommande('changerCibleRedondance', explicite, { cible: '1000' }), 'SANS_EFFET', { - commande: 'changerCibleRedondance', - }); - refuse(() => COMMANDES.get('changerCibleRedondance')(explicite, { cible: 1000 }), 'SANS_EFFET'); - assert.equal(explicite.reglages.cibleRedondance, 1000); + const ancienne = structuredClone(charge); + ancienne.reglages.cibleRedondance = 1200; + const rendu = accepter('modifierReglage', ancienne, { cle: 'nouveauxVoisins', valeur: false }); + const attendu = accepter('modifierReglage', charge, { cle: 'nouveauxVoisins', valeur: false }); + const texte = serialiser(rendu.charge, { revision: 1, produitVersion: V1 }); + assert.equal(texte.includes('cibleRedondance'), false); + assert.equal(texte, serialiser(attendu.charge, { revision: 1, produitVersion: V1 })); }); }); diff --git a/src/application/evaluation.js b/src/application/evaluation.js index 1609ae4..5a6a2b6 100644 --- a/src/application/evaluation.js +++ b/src/application/evaluation.js @@ -25,8 +25,8 @@ // rencontres répétées imposées dépendent du plan, et chaque ligne les lit // dans ses mesures, à part des choisies. // -// resumeDuRetenu donne à la liste des événements la différence à la cible -// du retenu, lue dans la même mesure que sa ligne du tableau, sans le +// resumeDuRetenu donne à la liste des événements les collisions cumulées +// du retenu, lues dans la même mesure que sa ligne du tableau, sans le // diagnostic ni les plafonds : la liste en résume chaque événement du // dossier. premiereDuClassement est la seule définition de la proposition // au rang 1 : la section de la génération la lit dans sa table, @@ -63,13 +63,10 @@ import { borneDiversite, plafondsRealises } from '../moteur/plafond.js'; import { detailler, planchersParGroupe, profilRencontres } from '../moteur/qualite.js'; import { verifierIndicateurs, verifierInvariants } from '../moteur/verification.js'; import { ErreurConfiguration } from '../moteur/erreurs.js'; -import { CIBLE_REDONDANCE_PAR_DEFAUT, cibleRedondance } from '../stockage/document.js'; import { derive, deriveSansRevalider, fautes, planDepuisFichier, planSansRevalider } from '../stockage/placements.js'; import { configurationDe } from './configuration.js'; import { nomAffiche } from './personnes.js'; -export { CIBLE_REDONDANCE_PAR_DEFAUT, cibleRedondance }; - // Les modes des objectifs, le défaut en tête, et les cases qui font compter // chaque critère, tels que le registre les tient (contrainte 35) : // l'interface, qui n'importe pas le moteur, les lit ici. @@ -220,16 +217,17 @@ export function evaluer(charge, { plafondsAPriori = 'calcul' } = {}) { } /** - * Le résumé du retenu que montre la liste des événements (§ 5.4, § 8.1, - * § 8.9) : diffRedondance, la valeur absolue de la cible de l'événement - * (cibleRedondance) moins la somme de redondance Σ r(p) du retenu, celle de - * mesurer, que la ligne du retenu d'evaluer porte aussi. null sans retenu, - * pour un retenu fautif ou en dérive, et quand la configuration ne se - * normalise pas : rien ne s'y mesure, et la liste écrit « — ». Ne modifie - * pas la charge. + * Le résumé du retenu que montre la liste des événements (§ 5.4, § 8.1) : + * collisionsCumulees, les occurrences (paire, tour) où deux personnes d'une + * même appartenance partagent une table, celles de mesurer — la valeur que + * la ligne du retenu d'evaluer porte, et que la colonne « Collisions + * cumulées (paire et tour) » du tableau des propositions écrit. null sans + * retenu, pour un retenu fautif ou en dérive, et quand la configuration ne + * se normalise pas : rien ne s'y mesure, et la liste écrit « — ». Ne + * modifie pas la charge. * * @param {import('../stockage/types.js').Charge} charge - * @returns {{diffRedondance: number}|null} + * @returns {{collisionsCumulees: number}|null} */ export function resumeDuRetenu(charge) { const { retenu } = charge; @@ -241,8 +239,8 @@ export function resumeDuRetenu(charge) { if (erreur instanceof ErreurConfiguration) return null; throw erreur; } - const { totalRedondance } = mesurer(instance, indexerPlan(instance, planDepuisFichier(retenu))); - return { diffRedondance: Math.abs(cibleRedondance(charge) - totalRedondance) }; + const { collisionsCumulees } = mesurer(instance, indexerPlan(instance, planDepuisFichier(retenu))); + return { collisionsCumulees }; } /** diff --git a/src/application/evaluation.test.js b/src/application/evaluation.test.js index 7eac12b..36394e4 100644 --- a/src/application/evaluation.test.js +++ b/src/application/evaluation.test.js @@ -46,9 +46,8 @@ import { deriveSansRevalider, fautes, planDepuisFichier } from '../stockage/plac import { VERSION } from '../version.genere.js'; import { appliquerCommande } from './commandes.js'; import { configurationDe } from './configuration.js'; +import * as EVALUATION from './evaluation.js'; import { - CIBLE_REDONDANCE_PAR_DEFAUT, - cibleRedondance, detaillerQualite, evaluer, premiereDuClassement, @@ -484,36 +483,32 @@ describe("évaluation : une configuration qui ne se tient pas", () => { }); }); -describe('resumeDuRetenu : la différence à la cible que lit la liste des événements (§ 5.4, § 8.1, § 8.9)', () => { +describe('resumeDuRetenu : les collisions cumulées du retenu que lit la liste des événements (§ 5.4, § 8.1)', () => { // La petite démonstration, trois propositions générées, la deuxième retenue. function retenue() { const generees = generee(chargeDemo('petite-conflit'), { graine: 3, nombre: 3, arret: 300 }); return executer(generees, 'retenirProposition', { id: generees.propositions[1].id }); } - test("la seule différence à la cible, 1000 par défaut, contre le Σ r(p) de la ligne du retenu d'evaluer", () => { + test("les seules collisions cumulées (paire et tour), celles de la ligne du retenu d'evaluer et de mesurer", () => { const charge = figer(retenue()); const resume = resumeDuRetenu(charge); const { mesures } = evaluer(charge).retenu; - assert.ok(mesures.totalRedondance !== 1000, 'la différence se distingue de zéro'); - assert.deepEqual(resume, { diffRedondance: Math.abs(1000 - mesures.totalRedondance) }); + assert.ok(mesures.collisionsCumulees > 0, 'le retenu porte des collisions'); + assert.notEqual(mesures.collisionsCumulees, mesures.totalRedondance); + assert.deepEqual(resume, { collisionsCumulees: mesures.collisionsCumulees }); const instance = normaliser(configurationDe(charge)); const attendues = mesurer(instance, indexerPlan(instance, planDepuisFichier(charge.retenu))); - assert.equal(attendues.totalRedondance, mesures.totalRedondance); - assert.equal(CIBLE_REDONDANCE_PAR_DEFAUT, 1000); - assert.equal(cibleRedondance(charge), 1000); + assert.equal(attendues.collisionsCumulees, mesures.collisionsCumulees); }); - test("une cible propre à l'événement : la différence se lit contre elle, en valeur absolue, des deux côtés", () => { + test('aucune cible : le résumé ne dépend pas d’un champ cibleRedondance resté dans les réglages, et le module n’en exporte plus', () => { const charge = retenue(); - const total = evaluer(charge).retenu.mesures.totalRedondance; - for (const cible of [0, total, total + 7, 5_000]) { - const avecCible = executer(charge, 'changerCibleRedondance', { cible: String(cible) }); - assert.equal(cibleRedondance(avecCible), cible); - assert.deepEqual(resumeDuRetenu(avecCible), { diffRedondance: Math.abs(cible - total) }, String(cible)); - } - const dessous = executer(charge, 'changerCibleRedondance', { cible: '0' }); - assert.equal(resumeDuRetenu(dessous).diffRedondance, total); + const avecChamp = structuredClone(charge); + avecChamp.reglages.cibleRedondance = 25; + assert.deepEqual(resumeDuRetenu(avecChamp), resumeDuRetenu(charge)); + assert.equal(Object.hasOwn(EVALUATION, 'cibleRedondance'), false); + assert.equal(Object.hasOwn(EVALUATION, 'CIBLE_REDONDANCE_PAR_DEFAUT'), false); }); test('sans retenu, null ; un retenu fautif ou en dérive, null, sans lever', () => { diff --git a/src/application/evenements.test.js b/src/application/evenements.test.js index 3ea6056..67871c1 100644 --- a/src/application/evenements.test.js +++ b/src/application/evenements.test.js @@ -639,7 +639,7 @@ describe('lister (§ 8.1, § 19.4)', () => { return serialiser(modifier(charge), { revision: 3, produitVersion: V1 }); } - test('les mesures du retenu : la seule différence à la cible, celle de resumeDuRetenu ; null sans retenu ; rien d’écrit', async () => { + test('les mesures du retenu : ses seules collisions cumulées, celles de resumeDuRetenu ; null sans retenu ; rien d’écrit', async () => { const { fs, facade } = await deuxEvenements(); fs.deposer('portable', `retenue${SUFFIXES.etat}`, texteRetenu()); const avant = ecritures(fs); @@ -648,20 +648,21 @@ describe('lister (§ 8.1, § 19.4)', () => { const par = Object.fromEntries(lignes.map((ligne) => [ligne.base, ligne])); const charge = analyser(texteRetenu()).charge; const { mesures } = evaluer(charge).retenu; - assert.deepEqual(par.retenue.mesuresDuRetenu, { diffRedondance: Math.abs(1000 - mesures.totalRedondance) }); + assert.ok(mesures.collisionsCumulees > 0, 'le retenu porte des collisions'); + assert.deepEqual(par.retenue.mesuresDuRetenu, { collisionsCumulees: mesures.collisionsCumulees }); assert.deepEqual(par.retenue.mesuresDuRetenu, resumeDuRetenu(charge)); assert.equal(par['soiree-a'].mesuresDuRetenu, null); assert.equal(par.petite.mesuresDuRetenu, null); }); - test("une cible propre à l'événement : la différence se lit contre elle", async () => { + test('un état écrit avec la cible de redondance d’un code antérieur se résume sans faute, comme sans elle', async () => { const { fs, facade } = await deuxEvenements(); - const texte = texteRetenu((charge) => appliquerCommande('changerCibleRedondance', charge, { cible: '25' }).charge); - fs.deposer('portable', `retenue${SUFFIXES.etat}`, texte); + const lu = JSON.parse(texteRetenu()); + lu.charge.reglages.cibleRedondance = 25; + fs.deposer('portable', `retenue${SUFFIXES.etat}`, `${JSON.stringify(lu, null, 2)}\n`); const ligne = (await listerResume(facade)).find(({ base }) => base === 'retenue'); - const { totalRedondance } = evaluer(analyser(texte).charge).retenu.mesures; - assert.notEqual(Math.abs(25 - totalRedondance), Math.abs(1000 - totalRedondance)); - assert.deepEqual(ligne.mesuresDuRetenu, { diffRedondance: Math.abs(25 - totalRedondance) }); + assert.equal(ligne.illisible, null); + assert.deepEqual(ligne.mesuresDuRetenu, resumeDuRetenu(analyser(texteRetenu()).charge)); }); test('un retenu fautif, ou en dérive, et un état illisible : null, la liste entière rendue, rien d’écrit', async () => { @@ -894,15 +895,15 @@ describe('lister (§ 8.1, § 19.4)', () => { const lignes = await facade.lister(); const recus = new Map(); const flux = facade.resumer(lignes, { ceder: pasDe(horloge), recevoir: (base, resume) => recus.set(base, resume) }); - // La cible de l'événement passe à 25 après la liste : le résumé du pas - // la lit, et non celle que la liste a vue. - const plusTard = texteRetenu((charge) => appliquerCommande('changerCibleRedondance', charge, { cible: '25' }).charge); + // L'autre proposition devient le retenu après la liste : le résumé du + // pas lit ses collisions, et non celles du retenu que la liste a vu. + const plusTard = texteRetenu((charge) => appliquerCommande('retenirProposition', charge, { id: charge.propositions[0].id }).charge); fs.deposer('portable', `retenue${SUFFIXES.etat}`, plusTard); for (let pas = 0; pas < lignes.length; pas += 1) await unPas(horloge); await flux.fin; - const { totalRedondance } = evaluer(analyser(plusTard).charge).retenu.mesures; - assert.notEqual(Math.abs(25 - totalRedondance), Math.abs(1000 - totalRedondance)); - assert.equal(recus.get('retenue').mesuresDuRetenu.diffRedondance, Math.abs(25 - totalRedondance)); + const { collisionsCumulees } = evaluer(analyser(plusTard).charge).retenu.mesures; + assert.notEqual(collisionsCumulees, evaluer(analyser(texteRetenu()).charge).retenu.mesures.collisionsCumulees); + assert.equal(recus.get('retenue').mesuresDuRetenu.collisionsCumulees, collisionsCumulees); assert.deepEqual(recus.get('retenue'), resumesAttendus(plusTard)); }); diff --git a/src/application/libelles.test.js b/src/application/libelles.test.js index e556811..8044fea 100644 --- a/src/application/libelles.test.js +++ b/src/application/libelles.test.js @@ -578,7 +578,6 @@ const EXEMPLES = new Map([ ['ENTREE_CHANGER_DEFAUT', { sieges: 10, realignees: 4 }], ['ENTREE_CHANGER_TOURS', { tours: 3, retirees: 2 }], ['ENTREE_MODIFIER_REGLAGE', { cle: 'separerAppartenances', valeur: false }], - ['ENTREE_CHANGER_CIBLE_REDONDANCE', { cible: 1200 }], // Objectifs des tables ['OBJECTIF_HORS_SOIREE', { tour: 5, remede: null }], ['OBJECTIF_SANS_ETIQUETTE', { table: 2, tour: 3, remede: { commande: 'retirerObjectif', arguments: { table: 2, tour: 3 } } }], diff --git a/src/application/libelles/mobilier.js b/src/application/libelles/mobilier.js index 155df15..6a1f7c1 100644 --- a/src/application/libelles/mobilier.js +++ b/src/application/libelles/mobilier.js @@ -251,7 +251,6 @@ export const TABLE = new Map([ ({ cle, valeur }) => `${nomDans(NOMS_DES_REGLAGES, cle, 'réglage')} : ${cle === 'modeObjectifs' ? nomMode(valeur) : valeurDeReglage(valeur)}`, ], - ['ENTREE_CHANGER_CIBLE_REDONDANCE', ({ cible }) => `Cible de la somme de redondance : ${cible}`], [ 'ENTREE_POSER_OBJECTIF', ({ numero, tour, regle, etiquettes }) => diff --git a/src/application/registre.test.js b/src/application/registre.test.js index ba3ca05..559c22e 100644 --- a/src/application/registre.test.js +++ b/src/application/registre.test.js @@ -400,14 +400,6 @@ const EXEMPLES = new Map([ sansEffet: { args: { cle: 'attribuerSieges', valeur: false } }, }, ], - [ - 'changerCibleRedondance', - { - accepte: { args: { cible: '1200' } }, - refuse: { args: { cible: '-1' }, code: 'NON_ENTIER', remede: null }, - sansEffet: { args: { cible: '1000' } }, - }, - ], [ 'changerEtiquettes', { diff --git a/src/application/seance.js b/src/application/seance.js index 9081cca..b3402ee 100644 --- a/src/application/seance.js +++ b/src/application/seance.js @@ -1049,6 +1049,12 @@ export function creerSeance({ // Un retour, sans son suivi : défaire vise le prédécesseur de l'instant // courant sur le fil, refaire le sommet de sa pile, revenir l'instant // donné ; l'entrée porte la charge restituée, son sens et sa cible. + // Revenir à un instant de même contenu que la charge courante est + // SANS_EFFET. Défaire et refaire s'écrivent même alors, d'un correctif + // vide : leur entrée seule fait avancer le fil. Un instant dont le geste ne + // changeait qu'un champ oublié à la lecture (§ 8.9) se restitue comme son + // voisin ; le refuser laisserait le fil sur place, et chaque nouveau + // défaire retomberait sur le même refus. async function retour(sens, visee) { const ouvert = exigerEcriture(); const { cibleDefaire, cibleRefaire, courant } = filDe(ouvert); @@ -1062,7 +1068,9 @@ export function creerSeance({ cible = cibleRefaire; } const charge = avecCompteursCourants(restituer(ouvert, cible), ouvert.charge); - if (serialiserCharge(charge) === serialiserCharge(ouvert.charge)) throw new ErreurSeance('SANS_EFFET', { sens }); + if (sens === 'revenir' && serialiserCharge(charge) === serialiserCharge(ouvert.charge)) { + throw new ErreurSeance('SANS_EFFET', { sens }); + } const instant = sens === 'defaire' ? courant : libelleDeRevision(ouvert.journal, cible); const entree = { charge, libelle: libelleDuRetour(sens, instant), avertissements: [] }; const texte = await ecrireGeste(ouvert, entree, { sens, retour: cible }); diff --git a/src/application/seance.test.js b/src/application/seance.test.js index 56d1f5b..ad1c7ca 100644 --- a/src/application/seance.test.js +++ b/src/application/seance.test.js @@ -10,6 +10,7 @@ // se remplace par un autre dossier. Les noms d'épreuve sont inventés ; la // version vient du module engendré. import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { PanneSimulee, creerFichiersSimules } from '../../test/fichiers_simules.js'; import { H_RETENU, personne as personneBadge, soiree, table as tableBadge } from '../../test/fixtures/badges/cas.js'; @@ -18,7 +19,7 @@ import { lirePolices } from '../../test/polices.js'; import { apercevoir } from '../csv/apercu.js'; import { nombrePlacesManquantes, normaliser } from '../moteur/configuration.js'; import { rechercher } from '../moteur/recherche.js'; -import { serialiser } from '../stockage/canonique.js'; +import { serialiser, serialiserCharge } from '../stockage/canonique.js'; import { creer as creerDansLeDepot, ouvrir as ouvrirDansLeDepot } from '../stockage/depot.js'; import { FORMAT, analyser, configurationDepuisCharge, creerCharge, etatDeduit } from '../stockage/document.js'; import { ErreurStockage } from '../stockage/erreurs.js'; @@ -1522,6 +1523,92 @@ describe('séance : revenir, défaire, refaire (§ 8.3)', () => { }); }); +// Un événement écrit par le code du format 1 (test/fixtures/stockage/format1) : +// son journal pose la cible de redondance à la révision 3 et la retire à la +// 5. Ce code oublie ce réglage (§ 8.9) : la révision 3 se restitue comme la +// 2, la 4 comme la 5. ATTENDUES porte, par révision, l'empreinte de la charge +// que ce code restitue ; empreintes.json, celle que restituait le format 1. +const FORMAT1 = new URL('../../test/fixtures/stockage/format1/', import.meta.url); +const FORMAT1_EMPREINTES = JSON.parse(readFileSync(new URL('empreintes.json', FORMAT1), 'utf8')); +const FORMAT1_ATTENDUES = { + ...FORMAT1_EMPREINTES.revisions, + 3: FORMAT1_EMPREINTES.revisions[2], + 4: FORMAT1_EMPREINTES.revisions[5], +}; +const empreinte = (charge) => createHash('sha256').update(serialiserCharge(charge), 'utf8').digest('hex'); + +describe('séance : défaire et refaire sur un journal du format 1 qui porte la cible de redondance (§ 8.3, § 8.9)', () => { + // Une séance en écriture sur une copie de la fixture déposée dans le + // système d'épreuve, et le journal de la fixture, lu tel quel. + async function surLaFixture() { + const fs = creerFichiersSimules(); + fs.deposerDossier('documents'); + const base = FORMAT1_EMPREINTES.base; + for (const suffixe of [SUFFIXES.journal, SUFFIXES.etat, SUFFIXES.precedent]) { + fs.deposer('documents', `${base}${suffixe}`, readFileSync(new URL(`${base}${suffixe}`, FORMAT1), 'utf8')); + } + const montage = monter({ fs }); + await ouvrirSansEcrire(montage, base); + await montage.seance.passerEnEcriture(); + const origine = lireJournal(readFileSync(new URL(`${base}${SUFFIXES.journal}`, FORMAT1), 'utf8')); + return { ...montage, base, origine }; + } + + // La charge courante a le contenu de l'instant revision : son empreinte, les + // compteurs d'identifiants ramenés à ceux de l'instant, est l'attendue — un + // retour garde les compteurs courants (§ 8.9). + function aLeContenuDe(seance, origine, revision) { + const instant = reconstruire(origine, revision); + assert.equal(empreinte({ ...seance.charge, prochainsIds: instant.prochainsIds }), FORMAT1_ATTENDUES[revision], `révision ${revision}`); + } + + test("défaire remonte chaque instant jusqu'à la création, celui qui ne changeait que la cible compris ; refaire redescend de même jusqu'à la dernière", async () => { + const { fs, seance, base, origine } = await surLaFixture(); + const derniereDe = () => journalDe(fs, 'documents', base).entrees.at(-1); + const defaits = []; + while (seance.retours.defaire) { + const { libelle: texte } = await seance.defaire(); + const { sens, retour } = derniereDe(); + assert.equal(sens, 'defaire'); + aLeContenuDe(seance, origine, retour); + defaits.push([retour, texte]); + } + assert.deepEqual(defaits, [ + [5, 'Défait : « Note »'], + [4, 'Défait : « Cible par défaut »'], + [3, 'Défait : « Réservation »'], + [2, 'Défait : « Cible de redondance »'], + [1, 'Défait : « Inscription de Brunelle »'], + ]); + await refus(seance.defaire(), 'RIEN_A_DEFAIRE'); + const refaits = []; + while (seance.retours.refaire) { + const { libelle: texte } = await seance.refaire(); + const { sens, retour } = derniereDe(); + assert.equal(sens, 'refaire'); + aLeContenuDe(seance, origine, retour); + refaits.push([retour, texte]); + } + assert.deepEqual(refaits, [ + [2, 'Refait : « Inscription de Brunelle »'], + [3, 'Refait : « Cible de redondance »'], + [4, 'Refait : « Réservation »'], + [5, 'Refait : « Cible par défaut »'], + [6, 'Refait : « Note »'], + ]); + assert.equal(empreinte(seance.charge), FORMAT1_EMPREINTES.revisions[6]); + assert.equal(fs.contenu('documents', `${base}${SUFFIXES.etat}`).includes('cibleRedondance'), false); + }); + + test("revenir à un instant de même contenu que l'instant courant reste SANS_EFFET, sans écriture", async () => { + const { fs, seance } = await surLaFixture(); + await seance.defaire(); + const avant = fs.compteurs.ecritures; + await refus(seance.revenir(4), 'SANS_EFFET', { sens: 'revenir', remede: null }); + assert.equal(fs.compteurs.ecritures, avant); + }); +}); + describe('séance : les jalons automatiques (§ 8.3)', () => { test("la création, chaque changement d'état, et l'instant qui précède chaque génération sont nommés", async () => { const fs = creerFichiersSimules(); diff --git a/src/interface/App.navigateur.test.js b/src/interface/App.navigateur.test.js index 7de59cc..0ef661a 100644 --- a/src/interface/App.navigateur.test.js +++ b/src/interface/App.navigateur.test.js @@ -29,7 +29,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { cdp } from 'vitest/browser'; import { flushSync } from 'svelte'; import { appliquerCommande } from '../application/commandes.js'; -import { cibleRedondance, evaluer } from '../application/evaluation.js'; +import { evaluer } from '../application/evaluation.js'; import { creerFormat } from '../application/format.js'; import { TABLES_APPLICATION } from '../application/libelles.js'; import { creerTraduction } from '../application/traduction.js'; @@ -394,25 +394,18 @@ describe('la liste relue', () => { describe('les résumés de la liste relue (§ 8.1)', () => { // La petite démonstration au conflit inévitable, deux propositions du - // moteur, la seconde retenue, sous la cible de redondance donnée ; la - // cible par défaut n'a pas de réglage écrit. - function texteRetenu(cible) { + // moteur, celle du rang donné retenue. + function texteRetenu(rang) { let charge = analyser(CONFLIT).charge; const configuration = configurationDepuisCharge(charge); const propositions = rechercher(configuration, { graine: 6, nombre: 2, arret: 300 }); charge = appliquerCommande('enregistrerGeneration', charge, { propositions, configuration, produitVersion: VERSION.affichee }).charge; - charge = appliquerCommande('retenirProposition', charge, { id: charge.propositions[1].id }).charge; - if (cible !== cibleRedondance(charge)) { - charge = appliquerCommande('changerCibleRedondance', charge, { cible: String(cible) }).charge; - } + charge = appliquerCommande('retenirProposition', charge, { id: charge.propositions[rang].id }).charge; return serialiser(charge, { revision: 1, produitVersion: VERSION.affichee }); } - // La différence à la cible que la ligne d'un texte d'état écrit. - const difference = (texte) => { - const charge = analyser(texte).charge; - return format.nombre(Math.abs(cibleRedondance(charge) - evaluer(charge).retenu.mesures.totalRedondance)); - }; - const cellule = (m) => dans(m, '[data-evenement="bal"] [data-colonne="diffRedondance"]'); + // Les collisions cumulées du retenu que la ligne d'un texte d'état écrit. + const collisions = (texte) => format.nombre(evaluer(analyser(texte).charge).retenu.mesures.collisionsCumulees); + const cellule = (m) => dans(m, '[data-evenement="bal"] [data-colonne="collisionsCumulees"]'); const uneImage = () => new Promise((resoudre) => requestAnimationFrame(() => resoudre())); // Retient la prochaine lecture de l'état de « bal » : elle lit le disque à @@ -436,9 +429,9 @@ describe('les résumés de la liste relue (§ 8.1)', () => { } test('une relecture pendant le chargement : la ligne ne montre jamais la valeur de l’ancienne lecture, même finie la dernière', async () => { - const ancien = texteRetenu(1000); - const nouveau = texteRetenu(25); - expect(difference(ancien)).not.toBe(difference(nouveau)); + const ancien = texteRetenu(1); + const nouveau = texteRetenu(0); + expect(collisions(ancien)).not.toBe(collisions(nouveau)); const m = await monter({ fichiers: [{ racine: 'documents', chemin: 'bal.gtt.json', texte: ancien }] }); const retenue = retenirLecture(m); // Le premier pas de la première lecture lit l'état, et reste suspendu. @@ -455,7 +448,7 @@ describe('les résumés de la liste relue (§ 8.1)', () => { await uneImage(); m.horloge.avancer(0); flushSync(); - expect(cellule(m).textContent).toBe(difference(nouveau)); + expect(cellule(m).textContent).toBe(collisions(nouveau)); }); // La lecture d'avant s'achève : son calcul ne se pose pas. retenue.liberer(); @@ -463,13 +456,13 @@ describe('les résumés de la liste relue (§ 8.1)', () => { for (let image = 0; image < 3; image += 1) await uneImage(); m.horloge.avancer(0); flushSync(); - expect(cellule(m).textContent).toBe(difference(nouveau)); + expect(cellule(m).textContent).toBe(collisions(nouveau)); }); test('un événement ouvert pendant le chargement l’abandonne : l’horloge avancée ne relit plus aucun état', async () => { const m = await monter({ fichiers: [ - { racine: 'documents', chemin: 'bal.gtt.json', texte: texteRetenu(1000) }, + { racine: 'documents', chemin: 'bal.gtt.json', texte: texteRetenu(1) }, { racine: 'documents', chemin: 'club.gtt.json', texte: evenement('Club des Merles', { texte: CONFLIT }) }, ], }); @@ -492,7 +485,7 @@ describe('les résumés de la liste relue (§ 8.1)', () => { test('une lecture de la liste qui s’achève sous un événement ouvert ne lance aucun chargement', async () => { const m = await monter({ fichiers: [ - { racine: 'documents', chemin: 'bal.gtt.json', texte: texteRetenu(1000) }, + { racine: 'documents', chemin: 'bal.gtt.json', texte: texteRetenu(1) }, { racine: 'documents', chemin: 'club.gtt.json', texte: evenement('Club des Merles', { texte: CONFLIT }) }, ], }); diff --git a/src/interface/accueil/Accueil.navigateur.test.js b/src/interface/accueil/Accueil.navigateur.test.js index 0403173..33ebfe0 100644 --- a/src/interface/accueil/Accueil.navigateur.test.js +++ b/src/interface/accueil/Accueil.navigateur.test.js @@ -1231,16 +1231,18 @@ async function eprouverTermes(racine, cas) { } } -describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => { +describe('les mesures du retenu dans la liste (§ 5.4, § 8.1)', () => { // Les mesures du retenu, par le moteur sollicité une seconde fois (§ 19.1). function mesuresDe(charge) { const instance = normaliser(configurationDepuisCharge(charge)); return mesurer(instance, indexerPlan(instance, planDepuisFichier(charge.retenu))); } - test('un événement retenu : |1000 \u{2212} Σ r(p)|, sans colonne des collisions cumulées ; sans retenu : « — » ; rien d’écrit', async () => { + test('un événement retenu : ses collisions cumulées (paire et tour), sous l’intitulé du tableau des propositions ; sans retenu : « — » ; rien d’écrit', async () => { const charge = chargeRetenue(); const mesures = mesuresDe(charge); + expect(mesures.collisionsCumulees).toBeGreaterThan(0); + expect(mesures.collisionsCumulees).not.toBe(Math.abs(1000 - mesures.totalRedondance)); const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(charge)), fichier(`club${SUFFIXES.etat}`, texteRenomme('Club des Merles'))], }); @@ -1248,51 +1250,44 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => { await chargerResumes(m); const avant = ecritures(m); const retenue = ligneDe(m, 'retenue'); - expect(colonne(retenue, 'diffRedondance')).toBe(format.nombre(Math.abs(1000 - mesures.totalRedondance))); + expect(colonne(retenue, 'collisionsCumulees')).toBe(format.nombre(mesures.collisionsCumulees)); + expect(colonne(retenue, 'collisionsCumulees')).toBe(format.nombre(evaluer(charge).retenu.mesures.collisionsCumulees)); const club = ligneDe(m, 'club'); - expect(colonne(club, 'diffRedondance')).toBe(TIRET); + expect(colonne(club, 'collisionsCumulees')).toBe(TIRET); // L'intitulé est un en-tête de colonne, une seule fois pour toute la - // liste ; aucune colonne ni aucun intitulé ne nomme les collisions - // cumulées. - expect(tous(m, '[data-terme="diffRedondance"]').length).toBe(1); - expect(terme(accueil(m), 'diffRedondance').closest('thead')).not.toBeNull(); - expect(terme(accueil(m), 'diffRedondance').textContent).toBe(t('accueil.colonne.diffRedondance')); - expect(tous(m, '[data-colonne="collisionsCumulees"], [data-terme="collisionsCumulees"]')).toEqual([]); - expect(tous(m, '[data-ecran="accueil"] thead th').filter((th) => th.textContent.includes(t('propositions.colonne.collisionsCumulees')))).toEqual([]); - expect(visible(retenue.querySelector('[data-colonne="diffRedondance"]'))).toBe(true); + // liste, au texte de la colonne du tableau des propositions ; plus rien + // ne nomme la différence à la cible. + expect(tous(m, '[data-terme="collisionsCumulees"]').length).toBe(1); + expect(terme(accueil(m), 'collisionsCumulees').closest('thead')).not.toBeNull(); + expect(terme(accueil(m), 'collisionsCumulees').textContent).toBe(t('propositions.colonne.collisionsCumulees')); + expect(tous(m, '[data-colonne="diffRedondance"], [data-terme="diffRedondance"]')).toEqual([]); + expect(accueil(m).textContent).not.toContain("Diff somme d'appartenance"); + expect(visible(retenue.querySelector('[data-colonne="collisionsCumulees"]'))).toBe(true); expect(ecritures(m)).toBe(avant); }); test('chaque intitulé s’explique : le survol puis le focus ouvrent son infobulle, Échap et la sortie la ferment', async () => { const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(chargeRetenue()))] }); await vi.waitFor(() => expect(lignes(m).length).toBe(1)); - await eprouverTermes(accueil(m), [['diffRedondance', 'accueil.explication.diffRedondance']]); + await eprouverTermes(accueil(m), [['collisionsCumulees', 'accueil.explication.collisionsCumulees']]); }); - test('la cible changée dans les propositions, puis la liste : la différence se lit contre la nouvelle cible', async () => { + test('un état écrit avec la cible de redondance d’un code antérieur : la ligne se lit sans signalement, ses collisions comme sans elle ; l’ouvrir n’écrit rien', async () => { const charge = chargeRetenue(); - const { totalRedondance } = mesuresDe(charge); - const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(charge))] }); + const lu = JSON.parse(etat(charge)); + lu.charge.reglages.cibleRedondance = 25; + const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, `${JSON.stringify(lu, null, 2)}\n`)] }); await vi.waitFor(() => expect(lignes(m).length).toBe(1)); await chargerResumes(m); - expect(colonne(ligneDe(m, 'retenue'), 'diffRedondance')).toBe(format.nombre(Math.abs(1000 - totalRedondance))); + const ligne = ligneDe(m, 'retenue'); + expect(ligne.querySelector('[data-signalement]')).toBeNull(); + expect(colonne(ligne, 'collisionsCumulees')).toBe(format.nombre(mesuresDe(charge).collisionsCumulees)); + const avant = ecritures(m); await ouvrirLigne(m, 'retenue'); await attendreEvenement(m); - await m.seance.passerEnEcriture(); - m.ui.allerA('propositions'); - await vi.waitFor(() => expect(dans(m, '[data-section="propositions"] [data-champ="cibleRedondance"]')).not.toBeNull()); - const entree = dans(m, '[data-champ="cibleRedondance"]'); - expect(entree.value).toBe('1000'); - entree.focus(); - saisir(entree, '25'); - entree.blur(); - await vi.waitFor(() => expect(m.seance.etat().charge.reglages.cibleRedondance).toBe(25)); - // Le résumé que la liste relue reçoit, non celui de la lecture d'avant, - // que la ligne montre encore tant que la relecture ne s'est pas posée. - const resumeRecu = suivreResumes(m); - await revenirALaListe(m, 1); - await resumeRecu('retenue'); - expect(colonne(ligneDe(m, 'retenue'), 'diffRedondance')).toBe(format.nombre(Math.abs(25 - totalRedondance))); + expect(Object.hasOwn(m.seance.etat().charge.reglages, 'cibleRedondance')).toBe(false); + expect(m.seance.etat().avertissements ?? []).toEqual([]); + expect(ecritures(m)).toBe(avant); }); }); @@ -1452,14 +1447,14 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', ( } // Les deux valeurs de résumé d'une ligne, dans l'ordre du rendu. - const CELLULES = ['[data-colonne="diffRedondance"]', '[data-meilleure-id]']; + const CELLULES = ['[data-colonne="collisionsCumulees"]', '[data-meilleure-id]']; const cellules = (ligne) => CELLULES.map((selecteur) => ligne.querySelector(selecteur)); // Ce que la ligne d'une charge écrit, par la table d'évaluation sollicitée // une seconde fois (§ 19.1). function attendues(charge) { const table = evaluer(charge); const [premiere] = table.propositions; - return [Math.abs(1000 - table.retenu.mesures.totalRedondance), premiere.id].map((n) => format.nombre(n)); + return [table.retenu.mesures.collisionsCumulees, premiere.id].map((n) => format.nombre(n)); } // Chaque valeur de la ligne attend : la cellule se dit occupée, porte un // seul signe, muet pour l'accessibilité (aria-hidden, sans rôle ni nom), @@ -1812,7 +1807,7 @@ describe('la liste en tableau : en-tête, tuiles, groupes et cartes (§ 8.1, § }; const ligne = ligneDe(m, 'gala'); for (const nom of ['nom', 'etat', 'participants', 'modification']) expect(dans2(ligne.querySelector(`[data-colonne="${nom}"]`), entetes[0]), nom).toBe(true); - expect(dans2(ligne.querySelector('[data-colonne="diffRedondance"]'), entetes[1])).toBe(true); + expect(dans2(ligne.querySelector('[data-colonne="collisionsCumulees"]'), entetes[1])).toBe(true); expect(dans2(ligne.querySelector('[data-meilleure-id]'), entetes[2])).toBe(true); // Le nom est l'en-tête de sa ligne ; la date et le chemin le suivent, // dessous, en plus petit. diff --git a/src/interface/accueil/ListeEvenements.svelte b/src/interface/accueil/ListeEvenements.svelte index 399ad56..18dd0e3 100644 --- a/src/interface/accueil/ListeEvenements.svelte +++ b/src/interface/accueil/ListeEvenements.svelte @@ -14,7 +14,7 @@ // // Les en-têtes de colonnes sont groupés : l'événement — nom, état, // participants, dernière modification —, le placement retenu, à la teinte - // de Retenu — sa différence à la cible (COLONNES_DU_RETENU) —, la + // de Retenu — ses collisions cumulées (COLONNES_DU_RETENU) —, la // meilleure proposition du classement, à la teinte de Proposé, sous le // titre et le libellé du bloc de la génération (MEILLEURE) — son // identifiant —, puis les actions. Chaque valeur est dans son propre diff --git a/src/interface/accueil/modele.js b/src/interface/accueil/modele.js index 067d4f9..f3fe357 100644 --- a/src/interface/accueil/modele.js +++ b/src/interface/accueil/modele.js @@ -4,8 +4,8 @@ // Le modèle de l'accueil, pur (§ 8.1, § 8.6, § 8.8) : ce que les composants // de la liste des événements affichent, sans arithmétique ni texte. Une // ligne de la liste (LigneListe de la façade, evenements.js) devient ses six -// informations et ses signalements, mis en forme, puis, son résumé reçu, la -// différence à la cible de son retenu et l'identifiant de la meilleure +// informations et ses signalements, mis en forme, puis, son résumé reçu, les +// collisions cumulées de son retenu et l'identifiant de la meilleure // proposition de son classement ; les lignes de la liste, les tuiles de ses // filtres par état du plan, le résumé de son en-tête et les groupes de son // tableau ; une question du dossier de travail, ses réponses ; un état @@ -25,17 +25,20 @@ const CLES_ETATS = new Map([ export const ETATS = Object.freeze([...CLES_ETATS.keys()]); /** - * La mesure du placement retenu qu'une ligne montre (§ 5.4, § 8.9) : la - * différence à la cible, { colonne, cle, explication } — le nom de sa valeur + * La mesure du placement retenu qu'une ligne montre (§ 5.4, § 8.1) : ses + * collisions cumulées, { colonne, cle, explication } — le nom de sa valeur * dans ligneAffichee et son data-colonne, la clé de son intitulé, celle de - * son explication, que l'infobulle de l'intitulé montre. Les collisions - * cumulées du retenu ne se montrent pas dans la liste. + * la colonne du tableau des propositions qui porte la même mesure, et celle + * de son explication, propre à la liste, que l'infobulle de l'intitulé + * montre. */ -export const COLONNES_DU_RETENU = Object.freeze( - ['diffRedondance'].map((colonne) => - Object.freeze({ colonne, cle: `accueil.colonne.${colonne}`, explication: `accueil.explication.${colonne}` }), - ), -); +export const COLONNES_DU_RETENU = Object.freeze([ + Object.freeze({ + colonne: 'collisionsCumulees', + cle: 'propositions.colonne.collisionsCumulees', + explication: 'accueil.explication.collisionsCumulees', + }), +]); /** * Les clés de la meilleure proposition du classement qu'une ligne montre @@ -127,7 +130,7 @@ export function signalementsDe(ligne) { } /** - * Les six informations d'une ligne (§ 8.1), la différence à la cible de son + * Les six informations d'une ligne (§ 8.1), les collisions cumulées de son * retenu et l'identifiant de la meilleure proposition de son classement, * mis en forme, et ses signalements. nom : celui inscrit dans le fichier, ou la base quand l'état * ne se lit pas ; date, participants et dernière modification par format, @@ -138,21 +141,20 @@ export function signalementsDe(ligne) { * l'affiche, en entier. * * Les deux valeurs de résumé lisent resume, ce que facade.resumer a rendu - * pour la ligne : diffRedondance, celle de mesuresDuRetenu, par format, la - * valeur absente sans retenu mesurable — elle se lit telle quelle, jamais - * refaite ; meilleure.id, l'identifiant de meilleureDuClassement, par - * format, la valeur absente sans proposition classée. Sans resume, la - * ligne est enAttente : son résumé se calcule encore, et les deux valeurs - * valent la valeur absente, que le composant remplace par le signe du - * chargement. + * pour la ligne : collisionsCumulees, celles de mesuresDuRetenu, par format, + * la valeur absente sans retenu mesurable — elles se lisent telles quelles, + * jamais refaites ; meilleure.id, l'identifiant de meilleureDuClassement, par + * format, la valeur absente sans proposition classée. Sans resume, la ligne + * est enAttente : son résumé se calcule encore, et les deux valeurs valent la + * valeur absente, que le composant remplace par le signe du chargement. * * @param {Object} ligne LigneListe (evenements.js) * @param {ReturnType} format - * @param {{mesuresDuRetenu: {diffRedondance: number}|null, + * @param {{mesuresDuRetenu: {collisionsCumulees: number}|null, * meilleureDuClassement: {id: number}|null}} [resume] * @returns {{nom: string, date: string, etat: string, participants: string, * modification: string, chemin: string, enAttente: boolean, - * diffRedondance: string, meilleure: {id: string}, + * collisionsCumulees: string, meilleure: {id: string}, * signalements: Array<{cle: string, details: Object, noms: string[]}>}} */ export function ligneAffichee(ligne, format, resume) { @@ -164,7 +166,7 @@ export function ligneAffichee(ligne, format, resume) { modification: ligne.derniereModification === null ? format.nombre(null) : format.horodatage(ligne.derniereModification), chemin: ligne.chemin, enAttente: resume === undefined, - diffRedondance: format.nombre(resume?.mesuresDuRetenu?.diffRedondance ?? null), + collisionsCumulees: format.nombre(resume?.mesuresDuRetenu?.collisionsCumulees ?? null), meilleure: { id: format.nombre(resume?.meilleureDuClassement?.id ?? null) }, signalements: signalementsDe(ligne), }; diff --git a/src/interface/accueil/modele.test.js b/src/interface/accueil/modele.test.js index 67277f5..2d71f6f 100644 --- a/src/interface/accueil/modele.test.js +++ b/src/interface/accueil/modele.test.js @@ -80,8 +80,8 @@ describe('ligneAffichee : les six informations (§ 8.1)', () => { const affichee = ligneAffichee(ligne(), format); assert.deepEqual(Object.keys(affichee).sort(), [ 'chemin', + 'collisionsCumulees', 'date', - 'diffRedondance', 'enAttente', 'etat', 'meilleure', @@ -334,35 +334,39 @@ describe('saisieDe : la saisie d’un nom pour copier ou renommer (§ 8.7)', () }); }); -describe('ligneAffichee : les mesures du retenu (§ 5.4, § 8.1, § 8.9)', () => { - test('la différence à la cible, celle de la façade, mise en forme, sans calcul ici', () => { - for (const diffRedondance of [500, 7, 0]) { - const affichee = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu: { diffRedondance } }); - assert.equal(affichee.diffRedondance, format.nombre(diffRedondance), String(diffRedondance)); +describe('ligneAffichee : les mesures du retenu (§ 5.4, § 8.1)', () => { + test('les collisions cumulées du retenu, celles de la façade, mises en forme, sans calcul ici', () => { + for (const collisionsCumulees of [500, 7, 0]) { + const affichee = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu: { collisionsCumulees } }); + assert.equal(affichee.collisionsCumulees, format.nombre(collisionsCumulees), String(collisionsCumulees)); } }); - test('sans retenu mesurable, et pour une ligne illisible, la différence vaut « — »', () => { + test('sans retenu mesurable, et pour une ligne illisible, les collisions valent « — »', () => { for (const champs of [{}, { nom: null, etat: null, participants: null, illisible: { raison: 'VIDE', chemin: null } }]) { const affichee = ligneAffichee(ligne(champs), format, AUCUN); - assert.equal(affichee.diffRedondance, VIDE); + assert.equal(affichee.collisionsCumulees, VIDE); } }); - test('COLONNES_DU_RETENU : la seule différence à la cible, avec son intitulé et son explication, qui existent ; aucune clé des collisions cumulées dans la liste', () => { - assert.deepEqual( - COLONNES_DU_RETENU.map(({ colonne }) => colonne), - ['diffRedondance'], - ); + test('COLONNES_DU_RETENU : les seules collisions cumulées, sous l’intitulé de la colonne du tableau des propositions, avec l’explication propre à la liste ; plus rien de la différence à la cible', () => { + assert.deepEqual(COLONNES_DU_RETENU, [ + { + colonne: 'collisionsCumulees', + cle: 'propositions.colonne.collisionsCumulees', + explication: 'accueil.explication.collisionsCumulees', + }, + ]); assert.ok(Object.isFrozen(COLONNES_DU_RETENU)); - for (const { colonne, cle, explication } of COLONNES_DU_RETENU) { - assert.equal(cle, `accueil.colonne.${colonne}`); - assert.equal(explication, `accueil.explication.${colonne}`); - assert.ok(t(cle).length > 0); - assert.ok(t(explication).length > 0); + assert.ok(Object.isFrozen(COLONNES_DU_RETENU[0])); + assert.equal(t('propositions.colonne.collisionsCumulees'), 'Collisions cumulées (paire et tour)'); + const explication = t('accueil.explication.collisionsCumulees'); + for (const fragment of ['paire', 'tour', 'Collisions cumulées (paire et tour)', '\u{2014}']) { + assert.ok(explication.includes(fragment), fragment); + } + for (const cle of ['accueil.colonne.diffRedondance', 'accueil.explication.diffRedondance', 'accueil.colonne.collisionsCumulees']) { + assert.throws(() => t(cle), RangeError, cle); } - assert.equal(t('accueil.colonne.diffRedondance'), "Diff somme d'appartenance"); - for (const cle of ['accueil.colonne.collisionsCumulees', 'accueil.explication.collisionsCumulees']) assert.throws(() => t(cle), RangeError, cle); }); }); @@ -370,20 +374,20 @@ describe('ligneAffichee : les résumés en attente (§ 8.1)', () => { test('sans résumé reçu, la ligne est en attente et ses deux valeurs de résumé ne disent rien ; reçu, elle ne l’est plus', () => { const attente = ligneAffichee(ligne(), format); assert.equal(attente.enAttente, true); - assert.equal(attente.diffRedondance, VIDE); + assert.equal(attente.collisionsCumulees, VIDE); assert.deepEqual(attente.meilleure, { id: VIDE }); assert.equal(ligneAffichee(ligne(), format, AUCUN).enAttente, false); const recu = ligneAffichee(ligne(), format, { - mesuresDuRetenu: { diffRedondance: 10 }, + mesuresDuRetenu: { collisionsCumulees: 10 }, meilleureDuClassement: { id: 3 }, }); assert.equal(recu.enAttente, false); - assert.equal(recu.diffRedondance, format.nombre(10)); + assert.equal(recu.collisionsCumulees, format.nombre(10)); assert.deepEqual(recu.meilleure, { id: format.nombre(3) }); }); test('les six informations ne dépendent pas des résumés : en attente ou non, elles sont les mêmes', () => { - const sans = ({ enAttente, diffRedondance, meilleure, ...reste }) => reste; + const sans = ({ enAttente, collisionsCumulees, meilleure, ...reste }) => reste; assert.deepEqual(sans(ligneAffichee(ligne(), format)), sans(ligneAffichee(ligne(), format, AUCUN))); }); }); diff --git a/src/interface/generation/Reglages.navigateur.test.js b/src/interface/generation/Reglages.navigateur.test.js index c793faa..7307785 100644 --- a/src/interface/generation/Reglages.navigateur.test.js +++ b/src/interface/generation/Reglages.navigateur.test.js @@ -130,10 +130,10 @@ const entree = (cible, cle) => cible.querySelector(`input[data-champ="${cle}"]`) const libelleDe = (cible, cle) => cible.querySelector(`label[for="${entree(cible, cle).id}"]`).textContent.trim(); describe('Reglages : l’écran montre la définition (§ 19.8)', () => { - test('la définition livrée : la graine, chaque entrée de la génération dans son ordre, sous son libellé, à la valeur de la charge, puis les cœurs ; la cible des propositions n’y est pas', async () => { + test('la définition livrée : la graine, chaque entrée de la génération dans son ordre, sous son libellé, à la valeur de la charge, puis les cœurs ; aucune cible de redondance', async () => { const cible = await monter(); expect(DE_LA_GENERATION.length).toBeGreaterThan(0); - expect(DE_LA_GENERATION.length).toBeLessThan(REGLAGES_GENERATION.length); + expect(DE_LA_GENERATION.length).toBe(REGLAGES_GENERATION.length); expect(designations(cible)).toEqual(['graine', ...designationsAttendues(REGLAGES_GENERATION), 'coeursAutomatiques']); expect(cible.querySelector('[data-champ="cibleRedondance"]')).toBeNull(); for (const { cle, etiquette, genre, defaut, valeurs } of DE_LA_GENERATION) { diff --git a/src/interface/generation/reglages.js b/src/interface/generation/reglages.js index 446abc3..2ded266 100644 --- a/src/interface/generation/reglages.js +++ b/src/interface/generation/reglages.js @@ -16,8 +16,7 @@ // generation. pour un champ de la génération, sinon ; // etiquette la clé du libellé, qui porte l'unité pour un champ ; // unite ce qu'un champ compte, 'propositions' ou 'mouvements' ; null -// pour une bascule, un choix, l'ordre, et pour la cible, une -// somme sans unité ; +// pour une bascule, un choix, l'ordre ; // defaut la valeur d'une charge neuve, ou celle d'un réglage facultatif // que la charge ne porte pas ; // genre 'champ', une saisie entière ; 'bascule', une case à cocher ; @@ -25,9 +24,7 @@ // l'ordre des priorités, une liste de paliers que son éditeur // règle (Priorites.svelte) ; // section la section de l'écran qui le montre : 'generation', écrit par -// modifierReglage — l'ordre par changerPriorites —, ou -// 'propositions', la cible de la somme de redondance, écrite par -// changerCibleRedondance ; +// modifierReglage — l'ordre par changerPriorites — ; // valeurs les valeurs d'un choix, dans l'ordre de ses boutons ; le // libellé de chacune est ., son explication // ..explication ; @@ -40,23 +37,22 @@ // // Les défauts se lisent sur la charge d'un événement neuf, telle que la // commande de création la pose (chargeNeuve) : creerCharge y copie -// GENERATION_PAR_DEFAUT et y écrit les bascules, et l'interface n'importe -// pas le stockage. La charge lue ici n'est écrite nulle part. Les réglages -// facultatifs n'y sont pas : la cible a pour défaut -// CIBLE_REDONDANCE_PAR_DEFAUT, l'ordre des priorités LISTE_PAR_DEFAUT, lue -// dans le registre par l'application (priorites.js). Le mode prend ses -// valeurs et son défaut, le premier, dans MODES_OBJECTIFS, et son critère -// dans CASES, celui que sa case fait compter : le registre, que +// GENERATION_PAR_DEFAUT et y écrit les bascules, et l'interface n'importe pas +// le stockage. La charge lue ici n'est écrite nulle part. Les réglages +// facultatifs n'y sont pas : l'ordre des priorités a pour défaut +// LISTE_PAR_DEFAUT, lue dans le registre par l'application (priorites.js). Le +// mode prend ses valeurs et son défaut, le premier, dans MODES_OBJECTIFS, et +// son critère dans CASES, celui que sa case fait compter : le registre, que // l'application réexporte (contrainte 35). Les deux cases portent ici le // défaut que le stockage leur lit (§ 8.9), et les épreuves de // reglages.test.js les comparent à ses lecteurs. import { chargeNeuve } from '../../application/commandes.js'; -import { CASES, CIBLE_REDONDANCE_PAR_DEFAUT, MODES_OBJECTIFS } from '../../application/evaluation.js'; +import { CASES, MODES_OBJECTIFS } from '../../application/evaluation.js'; import { SIEGES_PAR_DEFAUT, TOURS_PAR_DEFAUT } from '../../application/evenements.js'; import { LISTE_PAR_DEFAUT } from './priorites.js'; /** @typedef {'champ'|'bascule'|'choix'|'ordre'} GenreReglage */ -/** @typedef {'generation'|'propositions'} SectionReglage */ +/** @typedef {'generation'} SectionReglage */ /** * @typedef {Readonly<{nom: string, cle: string, etiquette: string, unite: 'propositions'|'mouvements'|null, * defaut: number|boolean|string|ReadonlyArray>, @@ -126,22 +122,11 @@ const PRIORITES = Object.freeze({ section: 'generation', }); -const CIBLE = Object.freeze({ - nom: 'cibleRedondance', - cle: 'cibleRedondance', - etiquette: 'propositions.cible.etiquette', - unite: null, - defaut: CIBLE_REDONDANCE_PAR_DEFAUT, - genre: 'champ', - section: 'propositions', -}); - /** * Les réglages de la charge, dans l'ordre des écrans : les trois champs de * la recherche (§ 5.7, § 5.10), puis les six contraintes et l'attribution * des sièges, dans l'ordre du § 5.3 ; le mode des objectifs, que l'écran - * montre sous sa case ; l'ordre des priorités ; enfin la cible de la somme - * de redondance (§ 8.9), de la section des propositions. + * montre sous sa case ; enfin l'ordre des priorités. * * @type {ReadonlyArray} */ @@ -158,7 +143,6 @@ export const REGLAGES_GENERATION = Object.freeze([ bascule('attribuerSieges'), MODE_OBJECTIFS, PRIORITES, - CIBLE, ]); /** diff --git a/src/interface/generation/reglages.test.js b/src/interface/generation/reglages.test.js index dc50d2c..6527ab5 100644 --- a/src/interface/generation/reglages.test.js +++ b/src/interface/generation/reglages.test.js @@ -3,8 +3,8 @@ // Épreuves de la définition des réglages de la charge (§ 5.3, § 5.7, // § 5.10, § 8.9, § 19.8) : sa forme, figée ; l'ordre des écrans ; les -// défauts, ceux d'une charge neuve, GENERATION_PAR_DEFAUT et -// CIBLE_REDONDANCE_PAR_DEFAUT compris, et ceux que les lecteurs du stockage +// défauts, ceux d'une charge neuve, GENERATION_PAR_DEFAUT compris, et ceux +// que les lecteurs du stockage // donnent aux réglages facultatifs — les deux cases, le mode des objectifs, // l'ordre des priorités ; chaque réglage du schéma du fichier, facultatif // compris, et lui seul ; les clés que les commandes acceptent et les @@ -16,7 +16,6 @@ import { appliquerCommande } from '../../application/commandes.js'; import { creerTraduction } from '../../application/traduction.js'; import { MODES_OBJECTIFS } from '../../moteur/criteres.js'; import { - CIBLE_REDONDANCE_PAR_DEFAUT, GENERATION_PAR_DEFAUT, SCHEMA, creerCharge, @@ -66,7 +65,6 @@ const ECRITURES = new Map([ (entree, valeur) => entree.genre === 'ordre' ? ['changerPriorites', { paliers: valeur }] : ['modifierReglage', { cle: entree.cle, valeur }], ], - ['propositions', (entree, valeur) => ['changerCibleRedondance', { cible: String(valeur) }]], ]); // Une valeur autre que le défaut de l'entrée : la case contraire, un mode @@ -96,7 +94,7 @@ describe('REGLAGES_GENERATION : la définition que lisent les écrans et la docu assert.throws(() => REGLAGES_GENERATION.push({}), TypeError); }); - test('l’ordre des écrans : les trois champs de la recherche, les bascules du § 5.3, le mode des objectifs, l’ordre des priorités, puis la cible de la section des propositions', () => { + test('l’ordre des écrans : les trois champs de la recherche, les bascules du § 5.3, le mode des objectifs, l’ordre des priorités ; aucun réglage hors de la génération', () => { assert.deepEqual( REGLAGES_GENERATION.map(({ nom, genre, section }) => [section, genre, nom]), [ @@ -112,7 +110,6 @@ describe('REGLAGES_GENERATION : la définition que lisent les écrans et la docu ['generation', 'bascule', 'attribuerSieges'], ['generation', 'choix', 'modeObjectifs'], ['generation', 'ordre', 'priorites'], - ['propositions', 'champ', 'cibleRedondance'], ], ); }); @@ -178,23 +175,13 @@ describe('REGLAGES_GENERATION : la définition que lisent les écrans et la docu assert.ok(Object.isFrozen(ordre.defaut)); }); - test('la cible de la somme de redondance : un champ de la section des propositions, à la racine des réglages, sous le libellé de son champ, sans unité', () => { - assert.deepEqual(reglagesDeSection(REGLAGES_GENERATION, 'propositions'), [ - { - nom: 'cibleRedondance', - cle: 'cibleRedondance', - etiquette: 'propositions.cible.etiquette', - unite: null, - defaut: CIBLE_REDONDANCE_PAR_DEFAUT, - genre: 'champ', - section: 'propositions', - }, - ]); + test('la cible de la somme de redondance n’est plus un réglage : ni entrée, ni section des propositions', () => { + assert.equal(REGLAGES_GENERATION.some(({ nom, cle }) => nom === 'cibleRedondance' || cle === 'cibleRedondance'), false); + assert.deepEqual(reglagesDeSection(REGLAGES_GENERATION, 'propositions'), []); }); - test('chaque défaut est celui d’une charge neuve : GENERATION_PAR_DEFAUT pour les champs, les bascules de creerCharge, CIBLE_REDONDANCE_PAR_DEFAUT pour la cible absente', () => { + test('chaque défaut est celui d’une charge neuve : GENERATION_PAR_DEFAUT pour les champs, les bascules de creerCharge', () => { const { reglages } = chargeNeuveEpreuve(); - assert.equal(Object.hasOwn(reglages, 'cibleRedondance'), false); for (const entree of REGLAGES_GENERATION) { assert.equal(entree.defaut, valeurDuReglage(reglages, entree), entree.nom); } @@ -218,7 +205,7 @@ describe('REGLAGES_GENERATION : la définition que lisent les écrans et la docu test('chaque réglage du schéma du fichier, facultatif compris, a son entrée, et chaque entrée en désigne un : aucun n’échappe aux écrans ni au tableau, hors des réglages nommés sans entrée', () => { const chemins = cheminsDuSchema(); - assert.ok(chemins.includes('cibleRedondance') && chemins.includes('generation.nombre'), chemins.join(', ')); + assert.ok(chemins.includes('generation.nombre') && !chemins.includes('cibleRedondance'), chemins.join(', ')); const cles = REGLAGES_GENERATION.map(({ cle }) => cle); for (const cle of SANS_ENTREE.keys()) { assert.ok(chemins.includes(cle), `${cle} : absent du schéma, à retirer de SANS_ENTREE`); @@ -242,19 +229,18 @@ describe('REGLAGES_GENERATION : la définition que lisent les écrans et la docu describe('valeurDuReglage, reglagesDuGenre et reglagesDeSection', () => { test('la valeur d’un réglage se lit au chemin de sa clé : sous generation pour un champ de la génération, à la racine sinon', () => { - const reglages = { nouvelleTable: false, cibleRedondance: 7, generation: { nombre: 9 } }; + const reglages = { nouvelleTable: false, modeObjectifs: 'minimum', generation: { nombre: 9 } }; const [nombre] = REGLAGES_GENERATION; const nouvelleTable = REGLAGES_GENERATION.find(({ nom }) => nom === 'nouvelleTable'); - const cible = REGLAGES_GENERATION.find(({ nom }) => nom === 'cibleRedondance'); + const mode = REGLAGES_GENERATION.find(({ nom }) => nom === 'modeObjectifs'); assert.equal(valeurDuReglage(reglages, nombre), 9); assert.equal(valeurDuReglage(reglages, nouvelleTable), false); - assert.equal(valeurDuReglage(reglages, cible), 7); + assert.equal(valeurDuReglage(reglages, mode), 'minimum'); }); test('un réglage facultatif absent vaut son défaut', () => { - const cible = REGLAGES_GENERATION.find(({ nom }) => nom === 'cibleRedondance'); - assert.equal(valeurDuReglage({ generation: {} }, cible), CIBLE_REDONDANCE_PAR_DEFAUT); - assert.equal(valeurDuReglage({ generation: {} }, { ...cible, defaut: 3 }), 3); + const mode = REGLAGES_GENERATION.find(({ nom }) => nom === 'modeObjectifs'); + assert.equal(valeurDuReglage({ generation: {} }, { ...mode, defaut: 'chacun' }), 'chacun'); for (const entree of REGLAGES_GENERATION.filter(({ genre }) => genre !== 'champ')) { assert.equal(valeurDuReglage({ generation: {} }, entree), entree.defaut, entree.cle); } @@ -277,7 +263,7 @@ describe('valeurDuReglage, reglagesDuGenre et reglagesDeSection', () => { const definition = [...REGLAGES_GENERATION].reverse(); assert.deepEqual( reglagesDuGenre(definition, 'champ').map(({ nom }) => nom), - ['cibleRedondance', 'historique', 'arret', 'nombre'], + ['historique', 'arret', 'nombre'], ); assert.deepEqual( reglagesDeSection(definition, 'generation').slice(0, 2).map(({ nom }) => nom), diff --git a/src/interface/libelles/accueil.js b/src/interface/libelles/accueil.js index da50db9..952e8d0 100644 --- a/src/interface/libelles/accueil.js +++ b/src/interface/libelles/accueil.js @@ -10,7 +10,6 @@ // Une donnée — un chemin, une base — n'entre dans aucun libellé d'écran : // le composant la rend dans son propre nœud, à côté du texte traduit // (§ 14.6). Un compte s'accorde dans la phrase qui le porte. -import { CIBLE_REDONDANCE_PAR_DEFAUT } from '../../application/evaluation.js'; import { creerFormat } from '../../application/format.js'; const compte = (n, singulier, pluriel) => `${n} ${n <= 1 ? singulier : pluriel}`; @@ -74,17 +73,17 @@ export const TABLE = new Map([ ['accueil.colonne.participants', 'Participants'], ['accueil.colonne.modification', 'Dernière modification'], ['accueil.colonne.chemin', 'Fichier'], - // La mesure du placement retenu (§ 5.4, § 8.9), et l'explication que - // l'infobulle de son intitulé donne : ce que compte le nombre, d'où il - // vient, et quand il vaut « — ». - ['accueil.colonne.diffRedondance', "Diff somme d'appartenance"], + // La mesure du placement retenu (§ 5.4, § 8.1) : son intitulé est celui de + // la colonne du tableau des propositions qui porte la même mesure ; + // l'explication que l'infobulle de l'intitulé donne dit ce que compte le + // nombre, d'où il vient, et quand il vaut « — ». [ - 'accueil.explication.diffRedondance', - '|cible \u{2212} Σ r(p)| : la différence, sans signe, entre la cible de la soirée et la somme Σ r(p) du placement ' + - "retenu, celle de la colonne « Redondance d'appartenance, somme Σ r(p) » du tableau des propositions. r(p) compte " + - "les personnes que p rencontre et qui ont une appartenance, moins le nombre d'appartenances différentes qu'elles " + - `portent. La cible vaut ${CIBLE_REDONDANCE_PAR_DEFAUT} tant qu'on ne la change pas, dans la section des ` + - "propositions. « \u{2014} » quand aucun placement n'est retenu, ou que le retenu ne décrit plus la configuration.", + 'accueil.explication.collisionsCumulees', + 'Les collisions du placement retenu. Chaque fois que deux personnes de la même appartenance partagent une ' + + 'table, compté une fois par paire et par tour : deux collègues ensemble aux quatre tours font 4. Le même nombre ' + + 'que la colonne « Collisions cumulées (paire et tour) » du tableau des propositions, sur la ligne du retenu. ' + + "Plus il est bas, mieux c'est. « \u{2014} » quand aucun placement n'est retenu, ou que le retenu ne décrit " + + "plus la configuration, ou se contredit.", ], // La meilleure proposition du classement de chaque événement (§ 5.4, diff --git a/src/interface/libelles/communs.js b/src/interface/libelles/communs.js index 1e94595..fb3a571 100644 --- a/src/interface/libelles/communs.js +++ b/src/interface/libelles/communs.js @@ -85,7 +85,6 @@ export const TABLE = new Map([ ['communs.remede.changerDefaut', 'Changer le nombre de places par défaut'], ['communs.remede.changerTours', 'Changer le nombre de tours'], ['communs.remede.modifierReglage', 'Modifier le réglage'], - ['communs.remede.changerCibleRedondance', 'Changer la cible de la somme de redondance'], ['communs.remede.changerEtiquettes', 'Changer les étiquettes'], ['communs.remede.poserObjectif', "Poser l'objectif"], ['communs.remede.retirerObjectif', "Retirer l'objectif"], diff --git a/src/interface/libelles/propositions.js b/src/interface/libelles/propositions.js index 8f53ad2..fa0ff48 100644 --- a/src/interface/libelles/propositions.js +++ b/src/interface/libelles/propositions.js @@ -340,14 +340,6 @@ export const TABLE = new Map([ ['propositions.statut.derive', 'Non comparable : la proposition ne décrit plus la configuration courante.'], ['propositions.statut.fautif', 'Non comparable : le placement se contredit.'], - // --- La cible de la somme de redondance (§ 5.4, § 8.1, § 8.9) ------------------- - ['propositions.cible.etiquette', 'Cible de la somme de redondance'], - [ - 'propositions.cible.aide', - "La liste des événements montre, pour le placement retenu, la différence sans signe entre cette cible et la " + - "somme Σ r(p) de la colonne « Redondance d'appartenance ». Un nombre entier, 0 ou plus.", - ], - // --- Les commandes (§ 2.1 étape 10, § 5.7) ------------------------------------ ['propositions.commande.voir', 'Voir sur le plan'], ['propositions.commande.retenir', 'Retenir'], diff --git a/src/interface/propositions/Propositions.navigateur.test.js b/src/interface/propositions/Propositions.navigateur.test.js index f7816a6..c8aff30 100644 --- a/src/interface/propositions/Propositions.navigateur.test.js +++ b/src/interface/propositions/Propositions.navigateur.test.js @@ -1042,51 +1042,15 @@ describe('les infobulles d’explication : chaque nom de critère, chaque en-tê }); }); -// --- La cible de la somme de redondance (§ 5.4, § 8.1, § 8.9) --------------------- +// --- La cible de la somme de redondance n'est plus un réglage (§ 8.9) ----------- -describe('la cible de la somme de redondance : un champ de la section', () => { - const champCible = (m) => section(m).querySelector('[data-champ="cibleRedondance"]'); - - // Saisit un texte dans le champ, comme une frappe, puis quitte le champ : - // la sortie valide la saisie (Champ). - async function saisirCible(m, texte) { - const entree = champCible(m); - entree.focus(); - entree.value = texte; - entree.dispatchEvent(new Event('input', { bubbles: true })); - flushSync(); - entree.blur(); - flushSync(); - } - - test('le champ montre 1000 sans réglage ; 25 saisi écrit une entrée, la cible 25 ; défaire rend 1000', async () => { - const m = await ouvrir(chargeDemo('petite')); - const entree = champCible(m); - expect(entree).not.toBeNull(); - expect(entree.value).toBe('1000'); - expect(texteDe(entree.closest('label') ?? section(m))).toContain(t('propositions.cible.etiquette')); - const avant = revision(m); - await saisirCible(m, '25'); - await attendre(() => expect(charge(m).reglages.cibleRedondance).toBe(25)); - expect(revision(m)).toBe(avant + 1); - await m.seance.defaire(); - await attendre(() => expect(champCible(m).value).toBe('1000')); - expect(Object.hasOwn(charge(m).reglages, 'cibleRedondance')).toBe(false); - }); - - test('en lecture, la saisie est refusée MODE_LECTURE contre le champ, rien n’écrit ; une saisie négative, NON_ENTIER', async () => { - const lecture = await ouvrir(chargeDemo('petite'), { mode: 'lecture' }); - const avant = revision(lecture); - await saisirCible(lecture, '25'); - await attendre(() => expect(section(lecture).querySelector('[data-refus="MODE_LECTURE"]')).not.toBeNull()); - expect(revision(lecture)).toBe(avant); - expect(Object.hasOwn(charge(lecture).reglages, 'cibleRedondance')).toBe(false); - - const ecriture = await ouvrir(chargeDemo('petite')); - const revisionEcriture = revision(ecriture); - await saisirCible(ecriture, '-3'); - await attendre(() => expect(section(ecriture).querySelector('[data-refus="NON_ENTIER"]')).not.toBeNull()); - expect(revision(ecriture)).toBe(revisionEcriture); +describe('la cible de la somme de redondance : aucun champ dans la section', () => { + test('la section ne porte ni le champ de la cible ni son libellé, en écriture comme en lecture', async () => { + for (const options of [{}, { mode: 'lecture' }]) { + const m = await ouvrir(chargeDemo('petite'), options); + expect(section(m).querySelector('[data-champ="cibleRedondance"]')).toBeNull(); + expect(texteDe(section(m))).not.toContain('Cible de la somme de redondance'); + } }); }); diff --git a/src/interface/propositions/SectionPropositions.svelte b/src/interface/propositions/SectionPropositions.svelte index 8902122..17d75fc 100644 --- a/src/interface/propositions/SectionPropositions.svelte +++ b/src/interface/propositions/SectionPropositions.svelte @@ -63,18 +63,9 @@ // // Habillage : le tableau et la phrase de l'ordre vivent dans une carte // titrée, « Effacer les propositions » à droite de son en-tête ; le message - // d'une liste vide et le champ de la cible, chacun dans sa carte. - // - // Le champ de la cible de la somme de redondance montre la cible de - // l'événement (cibleRedondance), et exécute changerCibleRedondance à la - // sortie du champ — une entrée ; la saisie se lit par lireEntier, et un - // refus se pose contre le champ (Champ). + // d'une liste vide dans sa carte. import { getContext, tick, untrack } from 'svelte'; - import { lireEntier } from '../../application/capacite.js'; - import { cibleRedondance } from '../../application/evaluation.js'; - import { REGLAGES_GENERATION, reglagesDeSection } from '../generation/reglages.js'; import Carte from '../primitives/Carte.svelte'; - import Champ from '../primitives/Champ.svelte'; import Commande from '../primitives/Commande.svelte'; import Confirmation from '../primitives/Confirmation.svelte'; import Infobulle from '../primitives/Infobulle.svelte'; @@ -96,9 +87,6 @@ const t = getContext('t'); const format = getContext('format'); - // Le champ de la cible lit l'entrée que lit aussi le tableau des réglages - // de la documentation (§ 19.8). - const [entreeCible] = reglagesDeSection(REGLAGES_GENERATION, 'propositions'); const seance = getContext('seance'); const ui = getContext('ui'); @@ -196,7 +184,6 @@ } const effacer = () => seance.executer('effacerPropositions', {}); - const changerCible = (cible) => seance.executer('changerCibleRedondance', { cible }); const nommer = (id) => nomDe(charge, id); @@ -278,21 +265,6 @@ - {#if charge !== null} - -
- -

{t('propositions.cible.aide')}

-
-
- {/if} - {#if aConfirmer !== null} diff --git a/src/stockage/canonique.test.js b/src/stockage/canonique.test.js index 80c4a81..43c07e0 100644 --- a/src/stockage/canonique.test.js +++ b/src/stockage/canonique.test.js @@ -737,7 +737,6 @@ describe('les champs facultatifs du format 2 (§ 8.9, A16)', () => { charge.reglages.modeObjectifs = 'chacun'; charge.reglages.respecterObjectifs = false; charge.reglages.regleObjectifs = 'et'; - charge.reglages.cibleRedondance = 800; charge.participants[0].etiquettes = ['finance', 'bénévole', 'Montréal']; charge.tables[1].objectifs = [ { portee: 'tour', tour: 2, regle: 'ou', etiquettes: ['ressources humaines', 'finance'] }, @@ -751,7 +750,7 @@ describe('les champs facultatifs du format 2 (§ 8.9, A16)', () => { remplacer( TEXTE_CONTRAT, '"generation":{"nombre":5,"arret":200000,"historique":1000}}', - '"generation":{"nombre":5,"arret":200000,"historique":1000},"cibleRedondance":800,"regleObjectifs":"et","respecterObjectifs":false,"modeObjectifs":"chacun","rencontrerAnimateurs":true}', + '"generation":{"nombre":5,"arret":200000,"historique":1000},"regleObjectifs":"et","respecterObjectifs":false,"modeObjectifs":"chacun","rencontrerAnimateurs":true}', ), '"notes":null,"exclu":false},\n {"id":2', '"notes":null,"exclu":false,"etiquettes":["Montréal","bénévole","finance"]},\n {"id":2', @@ -769,7 +768,7 @@ describe('les champs facultatifs du format 2 (§ 8.9, A16)', () => { test('absents, ils ne s’écrivent pas : la charge du format 1 rend le texte du contrat', () => { const charge = chargeFormat2(); - for (const cle of ['cibleRedondance', 'regleObjectifs', 'respecterObjectifs', 'modeObjectifs', 'rencontrerAnimateurs']) { + for (const cle of ['regleObjectifs', 'respecterObjectifs', 'modeObjectifs', 'rencontrerAnimateurs']) { delete charge.reglages[cle]; } delete charge.participants[0].etiquettes; @@ -777,6 +776,14 @@ describe('les champs facultatifs du format 2 (§ 8.9, A16)', () => { assert.equal(serialiser(charge, ENTETE), TEXTE_CONTRAT); }); + test('la cible de redondance, un ancien réglage qu’une charge porte encore, ne se copie ni ne s’écrit', () => { + const charge = chargeFormat2(); + charge.reglages = { cibleRedondance: 800, ...charge.reglages }; + assert.equal(Object.hasOwn(canoniser(charge).reglages, 'cibleRedondance'), false); + assert.equal(serialiser(charge, ENTETE), TEXTE_FORMAT_2); + assert.equal(serialiserCharge(charge), serialiserCharge(chargeFormat2())); + }); + test('l’ordre des étiquettes est celui des unités de code : une lettre hors du plan de base passe avant U+FF21, une majuscule avant une minuscule', () => { const charge = chargeContrat(); charge.participants[1].etiquettes = ['\u{FF21}', 'été', 'Été', '\u{1D538}', 'zèbre']; diff --git a/src/stockage/correctifs.js b/src/stockage/correctifs.js index 668805a..7f1eae1 100644 --- a/src/stockage/correctifs.js +++ b/src/stockage/correctifs.js @@ -8,7 +8,9 @@ // Une opération pose une valeur au bout d'un chemin, ou retire // l'enregistrement qu'il désigne : { op: 'poser', chemin, valeur } ou // { op: 'retirer', chemin }. Un champ facultatif du schéma (optionnel) se -// pose aussi quand son objet ne le porte pas, et se retire par sa clé. Un +// pose aussi quand son objet ne le porte pas, et se retire par sa clé. Une +// clé oubliée (oubliees de sa règle) ne se pose ni ne se retire : son +// opération, qu'un journal antérieur porte, s'applique sans rien changer. Un // chemin est une liste d'étapes depuis la charge : une clé d'objet, chaîne ; // un rang de liste, entier ≥ 0 ; ou // { id }, qui désigne dans une liste d'enregistrements à identifiant — @@ -371,7 +373,10 @@ function retirer(conteneur, etape, regle) { // charge, ou que la valeur posée n'y suivrait pas sa règle (valeurAdmise). // Un chemin vide n'a pas de dernière étape, et undefined n'en désigne // aucune. Une opération poser porte une valeur : JSON n'en écrit pas -// d'undefined. +// d'undefined. Une pose ou un retrait à une clé oubliée de l'objet qui la +// porterait (oubliees de sa règle), qu'écrivait un journal antérieur, rend +// vrai sans rien changer, quelle que soit la valeur et que la clé soit là +// ou non : la charge ne la porte jamais. function executer(charge, operation) { if (!estObjet(operation) || !Array.isArray(operation.chemin)) return false; const { op, chemin, valeur } = operation; @@ -383,6 +388,9 @@ function executer(charge, operation) { if (conteneur === undefined) return false; } const derniere = chemin[chemin.length - 1]; + if (lecture.regle === undefined && lecture.parent?.oubliees?.has(derniere) === true && estObjet(conteneur)) { + return op === 'poser' ? valeur !== undefined : op === 'retirer'; + } if (op === 'poser') { if (valeur === undefined || !valeurAdmise(charge, chemin, lecture.regle, valeur)) return false; return poser(conteneur, derniere, valeur, lecture.regle, lecture.parent); @@ -401,12 +409,13 @@ function executer(charge, operation) { * garde l'ordre des identifiants. * * Lève ErreurStockage('CORRECTIF', { rang }) à la première opération qui ne - * s'applique pas — rang est sa place dans le correctif, à partir de 0 — : - * une opération mal formée ; une étape { id } hors des participants, des - * tables et des propositions, même dans une liste vide ; une étape qui ne - * trouve pas sa clé, que l'objet porte en propre et n'hérite pas, son rang - * ou exactement un enregistrement ; poser à une clé que l'objet ne porte - * pas, hors d'un champ facultatif, ou au-delà de la fin d'une liste, ou sous + * s'applique pas — rang est sa place dans le correctif, à partir de 0 — : une + * opération mal formée ; une étape { id } hors des participants, des tables + * et des propositions, même dans une liste vide ; une étape qui ne trouve pas + * sa clé, que l'objet porte en propre et n'hérite pas, son rang ou exactement + * un enregistrement ; poser à une clé que l'objet ne porte pas, hors d'un + * champ facultatif et d'une clé oubliée — dont la pose et le retrait + * s'appliquent sans rien changer —, ou au-delà de la fin d'une liste, ou sous * une liste d'étiquettes, qui ne se pose qu'entière ; poser par { id } un * identifiant déjà présent, ou une valeur qui n'est pas l'enregistrement de * cet identifiant ; poser une valeur qui sort de la règle de sa place, en diff --git a/src/stockage/correctifs.test.js b/src/stockage/correctifs.test.js index 50e97a6..66affea 100644 --- a/src/stockage/correctifs.test.js +++ b/src/stockage/correctifs.test.js @@ -1836,8 +1836,9 @@ describe('le marqueur de chaise vide : correctifs et forme canonique (§ 8.9)', }); }); -describe('la cible de la somme de redondance : un champ facultatif dans un correctif (§ 8.6, § 8.9)', () => { - // Une charge neuve, sans la cible, et la même portant la cible donnée. +describe('la cible de la somme de redondance : un ancien réglage, que le correctif oublie (§ 8.6, § 8.9)', () => { + // Une charge neuve, sans la cible, et la même portant encore la cible + // donnée en mémoire. const sans = () => creerCharge({ id: 'evt-cible', nom: 'Veillée des Grives', siegesParDefaut: 6, tours: 2 }); const avec = (cible) => { const charge = sans(); @@ -1846,34 +1847,51 @@ describe('la cible de la somme de redondance : un champ facultatif dans un corre }; const CHEMIN = ['reglages', 'cibleRedondance']; - test('absente puis posée : une pose ; posée puis retirée : un retrait ; changée : une pose ; absente des deux côtés : rien', () => { - const cas = [ - [sans(), avec(1200), [{ op: 'poser', chemin: CHEMIN, valeur: 1200 }]], - [avec(1200), sans(), [{ op: 'retirer', chemin: CHEMIN }]], - [avec(1200), avec(0), [{ op: 'poser', chemin: CHEMIN, valeur: 0 }]], - [sans(), sans(), []], - ]; - assert.ok(cas.length > 0); - for (const [a, b, attendu] of cas) { - const correctif = difference(a, b); - assert.deepEqual(correctif, attendu); - const rendue = appliquer(a, JSON.parse(JSON.stringify(correctif))); - assert.deepEqual(rendue, canoniser(b)); - assert.equal(serialiser(rendue, ENTETE), serialiser(b, ENTETE)); + test('difference ne la pose ni ne la retire jamais : deux charges qui ne diffèrent que par elle ont un correctif vide', () => { + for (const [a, b] of [ + [sans(), avec(1200)], + [avec(1200), sans()], + [avec(1200), avec(0)], + ]) { + assert.deepEqual(difference(a, b), []); + assert.equal(serialiser(a, ENTETE), serialiser(b, ENTETE)); } }); - test('une cible hors de sa règle, un retrait de cible absente ou de tout autre champ, une pose sous une clé inconnue lèvent CORRECTIF', () => { + test('la pose et le retrait qu’écrivait un journal antérieur s’appliquent sans rien changer, de toute valeur, la clé présente ou non', () => { + const operations = [ + { op: 'poser', chemin: CHEMIN, valeur: 800 }, + { op: 'poser', chemin: CHEMIN, valeur: 0 }, + { op: 'poser', chemin: CHEMIN, valeur: -1 }, + { op: 'poser', chemin: CHEMIN, valeur: null }, + { op: 'poser', chemin: CHEMIN, valeur: '1000' }, + { op: 'retirer', chemin: CHEMIN }, + ]; + for (const depart of [sans(), avec(800)]) { + for (const operation of operations) { + const rendue = appliquer(depart, [JSON.parse(JSON.stringify(operation))]); + assert.equal(Object.hasOwn(rendue.reglages, 'cibleRedondance'), false, JSON.stringify(operation)); + assert.deepEqual(rendue, canoniser(sans()), JSON.stringify(operation)); + } + } + const suite = appliquer(sans(), [ + { op: 'poser', chemin: CHEMIN, valeur: 800 }, + { op: 'poser', chemin: ['reglages', 'regleObjectifs'], valeur: 'et' }, + { op: 'retirer', chemin: CHEMIN }, + ]); + assert.equal(suite.reglages.regleObjectifs, 'et'); + assert.equal(Object.hasOwn(suite.reglages, 'cibleRedondance'), false); + }); + + test('l’oubli ne vaut que pour elle, à sa place : un réglage obligatoire retiré, une autre clé inconnue, la cible hors des réglages ou sous elle lèvent CORRECTIF', () => { const cas = [ - ['cible négative', sans(), { op: 'poser', chemin: CHEMIN, valeur: -1 }], - ['cible non entière', avec(10), { op: 'poser', chemin: CHEMIN, valeur: 2.5 }], - ['cible nulle', sans(), { op: 'poser', chemin: CHEMIN, valeur: null }], - ['retrait de la cible absente', sans(), { op: 'retirer', chemin: CHEMIN }], ["retrait d'un réglage obligatoire", avec(10), { op: 'retirer', chemin: ['reglages', 'attribuerSieges'] }], ['pose sous une clé inconnue', sans(), { op: 'poser', chemin: ['reglages', 'cibleInconnue'], valeur: 3 }], + ['retrait d’une clé inconnue', sans(), { op: 'retirer', chemin: ['reglages', 'cibleInconnue'] }], ['pose de la cible hors des réglages', sans(), { op: 'poser', chemin: ['evenement', 'cibleRedondance'], valeur: 3 }], + ['pose sous la cible', avec(10), { op: 'poser', chemin: [...CHEMIN, 'valeur'], valeur: 3 }], + ['opération inconnue sur la cible', sans(), { op: 'changer', chemin: CHEMIN, valeur: 3 }], ]; - assert.ok(cas.length > 0); for (const [libelle, charge, operation] of cas) { assert.deepEqual(refusDe(charge, [operation]), { rang: 0 }, libelle); } @@ -2055,13 +2073,13 @@ describe('les étiquettes, les objectifs et les réglages du format 2 dans un co c.participants[0].exclu = true; }); const b = base((c) => { - c.reglages.cibleRedondance = 900; + c.reglages.regleObjectifs = 'et'; c.reglages.respecterObjectifs = false; c.reglages.rencontrerAnimateurs = true; c.participants[0].etiquettes = ['finance']; }); const rendue = appliquer(a, difference(a, b)); - assert.deepEqual(Object.keys(rendue.reglages).slice(-3), ['cibleRedondance', 'respecterObjectifs', 'rencontrerAnimateurs']); + assert.deepEqual(Object.keys(rendue.reglages).slice(-3), ['regleObjectifs', 'respecterObjectifs', 'rencontrerAnimateurs']); assert.equal(JSON.stringify(rendue), JSON.stringify(canoniser(b))); }); diff --git a/src/stockage/document.js b/src/stockage/document.js index 234b948..8d08ccb 100644 --- a/src/stockage/document.js +++ b/src/stockage/document.js @@ -45,6 +45,13 @@ // fichier du format 1 ne les porte pas, et se lit sans conversion. Il ne se // réécrit qu'au premier geste, au format courant. // +// Une clé que les réglages ont portée et ne portent plus — la cible de la +// somme de redondance, cibleRedondance — est oubliée (oubliees de la règle +// des réglages) : un fichier qui la porte se lit sans faute, quelle que soit +// sa valeur, la charge rendue ne la porte pas, et le premier geste écrit le +// fichier sans elle. Un correctif du journal qui la pose ou la retire +// s'applique sans rien changer (correctifs.js). +// // Les autres fonctions créent une charge ou dérivent d'elle la configuration // du moteur, l'état qu'impose son contenu, la capacité d'une table et la // valeur d'un réglage facultatif. Aucune ne modifie ce qu'elle reçoit. @@ -86,13 +93,6 @@ export const TOURS_MAX = 100; */ export const GENERATION_PAR_DEFAUT = Object.freeze({ nombre: 5, arret: 200_000, historique: HISTORIQUE_PAR_DEFAUT }); -/** - * La cible de la somme de redondance d'un événement quand ses réglages n'en - * portent pas (§ 8.9) : la liste des événements la compare à Σ r(p) du - * placement retenu. Un fichier sans le champ se lit à cette valeur. - */ -export const CIBLE_REDONDANCE_PAR_DEFAUT = 1000; - // AAAA-MM-JJTHH:MM:SS±HH:MM, la forme que rend l'horloge de l'application. // Le calendrier ne se contrôle pas : un horodatage ne décide d'aucun ordre. const HORODATAGE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/; @@ -113,10 +113,12 @@ export const estHorodatage = (valeur) => typeof valeur === 'string' && HORODATAG // Une règle du schéma ; types.js en décrit les propriétés (Regle). Un objet // porte ses champs, paires [clé, règle] dans l'ordre des clés, et l'ensemble -// de leurs clés ; une liste porte la règle de ses éléments, et peut borner -// sa longueur (min, max) et exiger des éléments distincts (distincts). Un -// champ facultatif (optionnel) peut manquer à son objet : absent, il ne -// s'écrit pas, et sa valeur par défaut se lit par la fonction qui le nomme. +// de leurs clés, et peut nommer des clés oubliées (oubliees), admises de +// toute valeur et jamais copiées ; une liste porte la règle de ses éléments, +// et peut borner sa longueur (min, max) et exiger des éléments distincts +// (distincts). Un champ facultatif (optionnel) peut manquer à son objet : +// absent, il ne s'écrit pas, et sa valeur par défaut se lit par la fonction +// qui le nomme. function regle(genre, proprietes) { return Object.freeze({ genre, nul: false, optionnel: false, ...proprietes }); } @@ -275,28 +277,32 @@ const PRIORITES = liste( { min: 1, optionnel: true }, ); -const REGLAGES = objet([ - ['separerAppartenances', BOOLEEN], - ['nouveauxVoisins', BOOLEEN], - ['nouvelleTable', BOOLEEN], - ['varierAppartenances', BOOLEEN], - ['attribuerSieges', BOOLEEN], - ['generation', objet([['nombre', entier(1)], ['arret', entier(1)], ['historique', entier(1)]])], - // Facultatifs, en dernier, dans l'ordre de A16 : un fichier qui n'en porte - // aucun s'écrit sans eux, au numéro de format près. La cible de la somme - // de redondance (cibleRedondance) ; la règle par défaut des objectifs, - // « et » seulement (regleObjectifs) ; la case des objectifs, décochée - // seulement (respecterObjectifs) ; le mode des objectifs hors du plus de - // places servies (modeObjectifs) ; la case des animateurs, cochée - // seulement (rencontrerAnimateurs) ; l'ordre des priorités hors de l'ordre - // par défaut (priorites). - ['cibleRedondance', entier(0, Infinity, { optionnel: true })], - ['regleObjectifs', horsDuDefaut('et')], - ['respecterObjectifs', horsDuDefaut(false)], - ['modeObjectifs', horsDuDefaut('chacun', 'minimum')], - ['rencontrerAnimateurs', horsDuDefaut(true)], - ['priorites', PRIORITES], -]); +// La cible de la somme de redondance (cibleRedondance), un réglage que les +// fichiers ont porté : oubliée, elle se lit de toute valeur et ne s'écrit +// plus. +const REGLAGES = objet( + [ + ['separerAppartenances', BOOLEEN], + ['nouveauxVoisins', BOOLEEN], + ['nouvelleTable', BOOLEEN], + ['varierAppartenances', BOOLEEN], + ['attribuerSieges', BOOLEEN], + ['generation', objet([['nombre', entier(1)], ['arret', entier(1)], ['historique', entier(1)]])], + // Facultatifs, en dernier, dans l'ordre de A16 : un fichier qui n'en + // porte aucun s'écrit sans eux, au numéro de format près. La règle par + // défaut des objectifs, « et » seulement (regleObjectifs) ; la case des + // objectifs, décochée seulement (respecterObjectifs) ; le mode des + // objectifs hors du plus de places servies (modeObjectifs) ; la case des + // animateurs, cochée seulement (rencontrerAnimateurs) ; l'ordre des + // priorités hors de l'ordre par défaut (priorites). + ['regleObjectifs', horsDuDefaut('et')], + ['respecterObjectifs', horsDuDefaut(false)], + ['modeObjectifs', horsDuDefaut('chacun', 'minimum')], + ['rencontrerAnimateurs', horsDuDefaut(true)], + ['priorites', PRIORITES], + ], + { oubliees: new Set(['cibleRedondance']) }, +); const PARTICIPANT = objet([ ['id', IDENTIFIANT], @@ -577,9 +583,11 @@ export function clesRangees(valeur) { return Object.keys(valeur).sort(comparerTextes); } -// Plus petite des clés de valeur absentes de cles, en comparant les unités -// UTF-16, ou undefined. -const cleInconnue = (valeur, cles) => clesRangees(valeur).find((cle) => !cles.has(cle)); +// Plus petite des clés de valeur que la règle objet ne connaît pas — ni +// l'une de ses clés, ni l'une de ses clés oubliées —, en comparant les +// unités UTF-16, ou undefined. +const cleInconnue = (valeur, { cles, oubliees }) => + clesRangees(valeur).find((cle) => !cles.has(cle) && oubliees?.has(cle) !== true); // Format du fichier lu, contrôlé avant toute autre règle : il dit comment // lire le reste. Une racine qui n'est pas un objet, un en-tête ou un format @@ -627,7 +635,7 @@ function fauteDeForme(valeur, regleDeValeur, chemin, racine) { const faute = fauteDeForme(valeur[cle], regleDuChamp, joindre(chemin, cle), false); if (faute !== null) return faute; } - const inconnue = cleInconnue(valeur, regleDeValeur.cles); + const inconnue = cleInconnue(valeur, regleDeValeur); return inconnue === undefined ? null : joindre(chemin, inconnue); } if (regleDeValeur.genre === 'liste') { @@ -642,7 +650,8 @@ function fauteDeForme(valeur, regleDeValeur, chemin, racine) { /** * Chemin de la première valeur qui sort de sa règle, en profondeur, ou null * quand valeur est conforme ; ne lève jamais. D'un objet, chaque champ dans - * l'ordre du schéma, puis la plus petite de ses clés inconnues ; d'une + * l'ordre du schéma, puis la plus petite de ses clés inconnues — une clé + * oubliée de sa règle n'en est pas une, quelle que soit sa valeur ; d'une * liste, chaque élément dans l'ordre des rangs. null est conforme à une * règle qui l'admet. Un champ absent se lit undefined, qu'admet seule une * règle facultative ; ailleurs, il se nomme par son chemin comme une valeur @@ -810,14 +819,21 @@ function examinerReferences(charge, plus) { charge.titres.forEach((titre, rang) => examinerPlace(titre, `charge.titres[${rang}]`)); } +// Retire des réglages lus, en place, chaque clé oubliée de leur règle +// (oubliees) : la charge que rend la lecture ne la porte pas. +function oublierDesReglages(reglages) { + for (const cle of REGLAGES.oubliees) delete reglages[cle]; +} + /** * Lit le texte d'un fichier d'état, sans rien écrire (§ 8.4, § 8.8). Une * marque d'ordre d'octets en tête est ignorée. Rend l'en-tête, la charge, * formatPlusRecent, vrai quand le format dépasse FORMAT, et manquants, ce que * les comptes des propositions et du retenu annoncent sans que la charge le * porte, qui ne refuse jamais le fichier (§ 8.9, point 3). Au format courant, - * l'en-tête et la charge sont ceux que le texte porte, ni copiés ni triés : - * canoniser les met dans l'ordre canonique. Un fichier d'un format plus + * l'en-tête et la charge sont ceux que le texte porte, ni copiés ni triés, + * les clés oubliées des réglages retirées (oublierDesReglages) : canoniser + * les met dans l'ordre canonique. Un fichier d'un format plus * récent s'ouvre en lecture seule et ne se réécrit jamais : l'en-tête et la * charge rendus sont des copies réduites aux clés que le schéma connaît, et * une faute de forme y porte la raison FORMAT_PLUS_RECENT ; les comptes et @@ -856,6 +872,7 @@ export function analyser(texte) { const cheminFautif = premiereFaute(fichier, SCHEMA); if (cheminFautif !== null) faute(cheminFautif); const { entete, charge } = fichier; + oublierDesReglages(charge.reglages); examinerIdentifiants(charge, faute); examinerTours(charge, faute); examinerObjectifs(charge, faute); @@ -947,18 +964,6 @@ export function etatDeduit({ retenu, propositions }) { return propositions.length > 0 ? 'propose' : 'brouillon'; } -/** - * La cible de la somme de redondance de l'événement (§ 8.9) : le réglage - * cibleRedondance quand la charge le porte, CIBLE_REDONDANCE_PAR_DEFAUT - * sinon. C'est la seule lecture de ce réglage. - * - * @param {import('./types.js').Charge} charge - * @returns {number} - */ -export function cibleRedondance(charge) { - return charge.reglages.cibleRedondance ?? CIBLE_REDONDANCE_PAR_DEFAUT; -} - // La règle par défaut des objectifs et le mode des objectifs quand les // réglages ne les portent pas (§ 8.9). const REGLE_OBJECTIFS_PAR_DEFAUT = 'ou'; diff --git a/src/stockage/document.test.js b/src/stockage/document.test.js index 7b9cac1..627c0ea 100644 --- a/src/stockage/document.test.js +++ b/src/stockage/document.test.js @@ -20,8 +20,8 @@ import { HISTORIQUE_PAR_DEFAUT } from '../moteur/recherche.js'; import { texteLivre } from '../demo/livrees.js'; import { VERSION } from '../version.genere.js'; import { serialiser, serialiserCharge } from './canonique.js'; +import * as DOCUMENT from './document.js'; import { - CIBLE_REDONDANCE_PAR_DEFAUT, ENTREE_DE_COURSE, FORMAT, GENERATION_PAR_DEFAUT, @@ -30,7 +30,6 @@ import { TOURS_MAX, analyser, capacite, - cibleRedondance, configurationDepuisCharge, creerCharge, critereCoche, @@ -1139,65 +1138,62 @@ describe('le marqueur de chaise vide : la règle d\'une liste de table (§ 8.9)' }); }); -describe('la cible de la somme de redondance : un réglage facultatif (§ 8.9)', () => { +describe('la cible de la somme de redondance : un ancien réglage, oublié à la lecture (§ 8.9)', () => { const REGLAGES = (() => { const CHARGE = SCHEMA.champs.find(([nom]) => nom === 'charge')[1]; return CHARGE.champs.find(([nom]) => nom === 'reglages')[1]; })(); - const CHEMIN = 'charge.reglages.cibleRedondance'; + // Toute valeur qu'un fichier peut porter sous la clé : celles que le code + // antérieur écrivait, et celles qu'il refusait. + const VALEURS = [0, 1, 1000, 1234, Number.MAX_SAFE_INTEGER, -1, 2.5, 2 ** 53, '1000', true, null, [1000], { cible: 3 }]; - test('sans le champ, le fichier se lit, la charge ne le porte pas, et la cible vaut 1000', () => { - assert.equal(CIBLE_REDONDANCE_PAR_DEFAUT, 1000); - const { charge } = analyser(JSON.stringify(documentValide())); - assert.equal(Object.hasOwn(charge.reglages, 'cibleRedondance'), false); - assert.equal(cibleRedondance(charge), 1000); - assert.equal(premiereFaute(charge.reglages, REGLAGES), null); + test('le schéma ne la décrit plus, et le module n’en exporte ni la valeur par défaut ni la lecture', () => { + assert.equal(REGLAGES.cles.has('cibleRedondance'), false); + assert.equal(REGLAGES.champs.some(([cle]) => cle === 'cibleRedondance'), false); + assert.equal(Object.hasOwn(DOCUMENT, 'cibleRedondance'), false); + assert.equal(Object.hasOwn(DOCUMENT, 'CIBLE_REDONDANCE_PAR_DEFAUT'), false); const neuve = creerCharge({ id: 'evt-neuf', nom: 'Atelier', siegesParDefaut: 8, tours: 3 }); assert.equal(Object.hasOwn(neuve.reglages, 'cibleRedondance'), false); - assert.equal(cibleRedondance(neuve), 1000); }); - test('un entier ≥ 0, sûr, se lit tel quel et fait la cible, 1000 écrit compris', () => { - const valeurs = [0, 1, 1000, 1234, Number.MAX_SAFE_INTEGER]; - assert.ok(valeurs.length > 0); - for (const valeur of valeurs) { - const { charge } = analyser(abime((d) => { d.charge.reglages.cibleRedondance = valeur; })); - assert.equal(charge.reglages.cibleRedondance, valeur); - assert.equal(cibleRedondance(charge), valeur); + test('un fichier qui la porte, de toute valeur, se lit sans faute, et la charge lue ne la porte plus : la même que sans elle', () => { + const sans = analyser(JSON.stringify(documentValide())); + for (const valeur of VALEURS) { + const texte = abime((d) => { d.charge.reglages.cibleRedondance = valeur; }); + assert.equal(premiereFaute(JSON.parse(texte), SCHEMA), null, JSON.stringify(valeur)); + const lu = analyser(texte); + assert.equal(Object.hasOwn(lu.charge.reglages, 'cibleRedondance'), false, JSON.stringify(valeur)); + assert.deepEqual(lu, sans, JSON.stringify(valeur)); } }); - test('une valeur négative, non entière, hors des entiers sûrs ou d\'une autre sorte est refusée FORME, par son chemin', () => { - const cas = [ - ['négative', -1], - ['non entière', 2.5], - ['au-delà des entiers sûrs', 2 ** 53], - ['en texte', '1000'], - ['booléenne', true], - ['nulle', null], - ['une liste', [1000]], - ]; - assert.deepEqual(ecartsDeRefus(cas.map(([libelle, valeur]) => [ - libelle, - abime((d) => { d.charge.reglages.cibleRedondance = valeur; }), - 'FORME', - CHEMIN, - ])), []); + test('l’oubli ne vaut que pour elle, et que dans les réglages : une autre clé inconnue reste une faute de forme', () => { + assert.deepEqual(ecartsDeRefus([ + ['une autre clé des réglages', abime((d) => { d.charge.reglages.cibleRedondances = 1000; }), 'FORME', 'charge.reglages.cibleRedondances'], + ['la clé hors des réglages', abime((d) => { d.charge.evenement.cibleRedondance = 1000; }), 'FORME', 'charge.evenement.cibleRedondance'], + [ + 'une clé inconnue à côté d’elle', + abime((d) => { + d.charge.reglages.cibleRedondance = 1000; + d.charge.reglages.autreReglage = true; + }), + 'FORME', + 'charge.reglages.autreReglage', + ], + ]), []); }); - test("absente, la charge s'écrit sans le champ ; posée, il s'écrit après la génération, et la relecture rend la même charge", () => { + test('une charge qui la porte encore en mémoire s’écrit sans elle, comme la charge qui ne l’a jamais portée', () => { const { charge } = analyser(JSON.stringify(documentValide())); const sans = serialiser(charge, { revision: 12, produitVersion: VERSION.affichee }); assert.equal(sans.includes('cibleRedondance'), false); const posee = structuredClone(charge); posee.reglages = { cibleRedondance: 1200, ...posee.reglages }; - const avec = serialiser(posee, { revision: 12, produitVersion: VERSION.affichee }); - assert.ok(avec.includes('"generation":{"nombre":3,"arret":50000,"historique":500},"cibleRedondance":1200}'), avec); - assert.deepEqual(analyser(avec).charge, analyser(serialiser(analyser(avec).charge, { revision: 12, produitVersion: VERSION.affichee })).charge); - assert.equal(cibleRedondance(analyser(avec).charge), 1200); + assert.equal(serialiser(posee, { revision: 12, produitVersion: VERSION.affichee }), sans); + assert.equal(serialiserCharge(posee), serialiserCharge(charge)); }); - test("un fichier d'un format plus récent garde la cible qu'il porte, réduite aux clés connues", () => { + test("un fichier d'un format plus récent qui la porte se lit réduit aux clés connues, sans elle", () => { const texte = abime((d) => { d.entete.format = FORMAT + 1; d.charge.reglages.cibleRedondance = 750; @@ -1205,7 +1201,7 @@ describe('la cible de la somme de redondance : un réglage facultatif (§ 8.9)', }); const lecture = analyser(texte); assert.equal(lecture.formatPlusRecent, true); - assert.equal(cibleRedondance(lecture.charge), 750); + assert.equal(Object.hasOwn(lecture.charge.reglages, 'cibleRedondance'), false); assert.equal(Object.hasOwn(lecture.charge.reglages, 'autreReglage'), false); }); }); @@ -1418,10 +1414,9 @@ describe('les champs facultatifs du format 2 : forme et place (§ 8.9, A16)', () const PARTICIPANT = regleDuChamp(CHARGE_SCHEMA, 'participants').element; const TABLE = regleDuChamp(CHARGE_SCHEMA, 'tables').element; assert.deepEqual( - REGLAGES.champs.slice(-7).map(([cle, regle]) => [cle, regle.optionnel]), + REGLAGES.champs.slice(-6).map(([cle, regle]) => [cle, regle.optionnel]), [ ['generation', false], - ['cibleRedondance', true], ['regleObjectifs', true], ['respecterObjectifs', true], ['modeObjectifs', true], @@ -1452,12 +1447,6 @@ describe('les champs facultatifs du format 2 : forme et place (§ 8.9, A16)', () const relue = analyser(texte); assert.deepEqual(relue.charge, charge); assert.equal(serialiser(relue.charge, relue.entete), texte); - const avecCible = structuredClone(charge); - avecCible.reglages.cibleRedondance = 900; - assert.ok( - serialiser(avecCible, relue.entete).includes('"historique":500},"cibleRedondance":900,"regleObjectifs":"et",'), - 'la cible avant la règle', - ); }); test('des étiquettes hors de l’ordre des unités de code et des objectifs hors de l’ordre soirée puis tours se lisent, et s’écrivent rangés', () => { diff --git a/src/stockage/format1.long.test.js b/src/stockage/format1.long.test.js index d042e8a..0e0d872 100644 --- a/src/stockage/format1.long.test.js +++ b/src/stockage/format1.long.test.js @@ -7,14 +7,19 @@ // jalon, une inscription, une cible de redondance posée puis retirée, une // réservation, un second jalon, une note. empreintes.json y porte, par // révision, l'empreinte SHA-256 du texte de serialiserCharge que restituait -// le journal sous ce même code. Chaque épreuve travaille sur une copie de ces +// le journal sous ce même code. Les révisions 3 et 4 y portent la cible de +// redondance, un réglage que ce code oublie à la lecture (§ 8.9) : la +// révision 3 s'y restitue comme la 2, la 4 comme la 5 (ATTENDUES). Chaque +// épreuve travaille sur une copie de ces // fichiers dans un dossier temporaire neuf, retiré à la fin, par le système // de fichiers de la plateforme electron (fichiers_electron.js devant les // primitives de la coquille, electron/fichiers.js) : l'ouverture n'écrit // rien ; chaque révision se restitue comme le format 1 la restituait ; le // premier geste réécrit le journal au format courant, chaque ligne // d'historique gardée ; défaire, d'un geste à l'autre au-delà de la -// conversion, rend chaque charge d'avant. L'épreuve est de la série longue : +// conversion, rend chaque charge d'avant ; un état qui porte encore la cible +// s'ouvre sans avertissement et s'écrit sans elle au premier geste. +// L'épreuve est de la série longue : // elle charge les primitives de la coquille, et le motif de surveillance // d'electron/ (vitest.config.js) ne relance, dans la série node, que les // épreuves qu'il nomme. @@ -39,6 +44,10 @@ const { base: BASE, revisions: EMPREINTES } = JSON.parse(readFileSync(join(FIXTU const NOMS = [SUFFIXES.journal, SUFFIXES.etat, SUFFIXES.precedent].map((suffixe) => `${BASE}${suffixe}`); const [JOURNAL, ETAT, PRECEDENT] = NOMS; const DERNIERE = Object.keys(EMPREINTES).length; +// Les empreintes que restitue ce code : celles du format 1, la cible de +// redondance oubliée — la révision 3, qui la posait, égale la 2 ; la 4, qui +// la portait encore, égale la 5, qui la retirait. +const ATTENDUES = { ...EMPREINTES, 3: EMPREINTES[2], 4: EMPREINTES[5] }; const empreinte = (charge) => createHash('sha256').update(serialiserCharge(charge), 'utf8').digest('hex'); const lignesDe = (texte) => texte.replace(/\n$/, '').split('\n'); @@ -126,11 +135,15 @@ describe('stockage : un événement écrit par le code du format 1 (§ 8.8, A15) assert.equal(ouvert.revision, DERNIERE); })); - test('chaque révision se restitue telle que le code du format 1 l’enregistrait ; l’état et le .precedent portent les deux dernières', () => { + test('chaque révision se restitue telle que le code du format 1 l’enregistrait, la cible de redondance oubliée ; l’état et le .precedent portent les deux dernières', () => { const journal = lireJournal(fixture(JOURNAL)); assert.equal(journal.ecartees, 0); + assert.ok(fixture(JOURNAL).includes('"chemin":["reglages","cibleRedondance"],"valeur":800')); const lues = Object.fromEntries(journal.entrees.map(({ revision }) => [revision, empreinte(reconstruire(journal, revision))])); - assert.deepEqual(lues, EMPREINTES); + assert.deepEqual(lues, ATTENDUES); + for (const { revision } of journal.entrees) { + assert.equal(Object.hasOwn(reconstruire(journal, revision).reglages, 'cibleRedondance'), false, `révision ${revision}`); + } assert.equal(empreinte(analyser(fixture(ETAT)).charge), EMPREINTES[DERNIERE]); assert.equal(empreinte(analyser(fixture(PRECEDENT)).charge), EMPREINTES[DERNIERE - 1]); assert.deepEqual( @@ -155,7 +168,7 @@ describe('stockage : un événement écrit par le code du format 1 (§ 8.8, A15) const nouvelles = lignesDe(texte); assert.deepEqual(nouvelles.slice(1, anciennes.length), anciennes.slice(1)); assert.equal(nouvelles.length, anciennes.length + 1); - for (const [revision, attendue] of Object.entries(EMPREINTES)) { + for (const [revision, attendue] of Object.entries(ATTENDUES)) { assert.equal(empreinte(reconstruire(journal, Number(revision))), attendue, `révision ${revision}`); } assert.equal(empreinte(reconstruire(journal, DERNIERE + 1)), empreinte(apres)); @@ -175,7 +188,7 @@ describe('stockage : un événement écrit par le code du format 1 (§ 8.8, A15) for (let n = 2; fil(ouvert.journal).cibleDefaire !== null; n += 1) { const cible = fil(ouvert.journal).cibleDefaire; ouvert = await enregistrer(ouvert, reconstruire(ouvert.journal, cible), geste(n, { sens: 'defaire', retour: cible })); - assert.equal(empreinte(ouvert.charge), EMPREINTES[cible], `retour à ${cible}`); + assert.equal(empreinte(ouvert.charge), ATTENDUES[cible], `retour à ${cible}`); atteintes.push(cible); } assert.deepEqual(atteintes, [6, 5, 4, 3, 2, 1]); @@ -184,4 +197,21 @@ describe('stockage : un événement écrit par le code du format 1 (§ 8.8, A15) assert.equal(rouvert.journal.format, FORMAT); assert.equal(fil(rouvert.journal).cibleRefaire, 2); })); + + test('un état qui porte encore la cible de redondance s’ouvre sans avertissement, sans elle, et s’écrit sans elle au premier geste', () => + surUneCopie(async ({ fs, racine, chemin }) => { + const lu = JSON.parse(fixture(ETAT)); + lu.charge.reglages.cibleRedondance = 800; + writeFileSync(chemin(ETAT), `${JSON.stringify(lu, null, 2)}\n`); + const ouvert = await ouvrir(fs, racine, BASE); + assert.deepEqual(ouvert.avertissements, []); + assert.equal(Object.hasOwn(ouvert.charge.reglages, 'cibleRedondance'), false); + assert.equal(empreinte(ouvert.charge), EMPREINTES[DERNIERE]); + const apres = structuredClone(ouvert.charge); + apres.participants[1].notes = 'converti'; + await enregistrer(ouvert, apres, geste(1)); + const ecrit = readFileSync(chemin(ETAT), 'utf8'); + assert.equal(ecrit.includes('cibleRedondance'), false); + assert.equal(empreinte(analyser(ecrit).charge), empreinte(apres)); + })); }); diff --git a/src/stockage/journal.test.js b/src/stockage/journal.test.js index 89d72e1..279c604 100644 --- a/src/stockage/journal.test.js +++ b/src/stockage/journal.test.js @@ -1156,6 +1156,23 @@ describe('le journal d’un format plus ancien se lit (§ 8.8, A15)', () => { assert.deepEqual([lu.format, lu.entrees.length, lu.ecartees], [1, 36, 64]); }); + test('la cible de redondance qu’écrivait un code antérieur — dans un instantané, posée puis retirée par un correctif — se lit, et chaque instant se restitue sans elle', () => { + const courant = lireJournal(cent()); + const lignes = lignesDe(auFormat(cent(), 1)).map((ligne) => JSON.parse(ligne)); + const entree = (revision) => lignes.find((ligne) => ligne.type === 'entree' && ligne.revision === revision); + entree(1).instantane.reglages.cibleRedondance = 800; + entree(2).correctif.unshift({ op: 'poser', chemin: ['reglages', 'cibleRedondance'], valeur: 1200 }); + entree(3).correctif.push({ op: 'retirer', chemin: ['reglages', 'cibleRedondance'] }); + entree(4).correctif.push({ op: 'retirer', chemin: ['reglages', 'cibleRedondance'] }); + const lu = lireJournal(texteDe(lignes.map((ligne) => JSON.stringify(ligne)))); + assert.deepEqual([lu.format, lu.ecartees, lu.entrees.length], [1, 0, courant.entrees.length]); + for (const revision of [1, 2, 3, 4, 5, 51, 100]) { + const charge = reconstruire(lu, revision); + assert.equal(Object.hasOwn(charge.reglages, 'cibleRedondance'), false, String(revision)); + assert.equal(texteCanonique(charge), texteCanonique(reconstruire(courant, revision)), String(revision)); + } + }); + test('élagué, un journal du format 1 se réécrit au format courant : seule son ouverture change', () => { const etapes = suite(650); const texte = journalDe(etapes); diff --git a/src/stockage/types.js b/src/stockage/types.js index f9f4aeb..20458a5 100644 --- a/src/stockage/types.js +++ b/src/stockage/types.js @@ -103,10 +103,6 @@ * propre siegesAttribues * @property {{nombre: number, arret: number, historique: number}} generation * entiers ≥ 1 - * @property {number} [cibleRedondance] entier ≥ 0, facultatif : la cible - * de la somme de redondance ; absent, - * elle vaut CIBLE_REDONDANCE_PAR_DEFAUT, - * que rend la fonction cibleRedondance * @property {'et'} [regleObjectifs] facultatif, présent seulement à * « et » : la règle que suit un * objectif de règle nulle ; absent, @@ -288,6 +284,10 @@ * @property {Array<[string, Regle]>} [champs] objet : ses champs, dans * l'ordre des clés du texte canonique * @property {Set} [cles] objet : les clés de ses champs + * @property {Set} [oubliees] objet : des clés qu'il a portées + * et ne porte plus. La forme les admet, de toute valeur ; la + * lecture les retire, canoniser ne les copie pas, et un correctif + * qui les pose ou les retire s'applique sans rien changer * @property {Regle} [element] liste : la règle de ses éléments * @property {number} [min] entier : bornes incluses ; * liste : bornes incluses de sa longueur diff --git a/test/navigateur/designations.js b/test/navigateur/designations.js index 4d0b164..27dcb53 100644 --- a/test/navigateur/designations.js +++ b/test/navigateur/designations.js @@ -40,7 +40,6 @@ const ENTREES = [ ['data-chemin-planche', 'badges : le chemin de la planche produite, ou son nom téléchargé', ['']], ['data-choix-siege', 'plan : un siège offert par le panneau d’une table, par son rang', null], ['data-cible', 'la cible d’une commande : section, groupe, colonne, vue imprimée ou papier', null], - ['data-cible-redondance', 'propositions : le champ de la cible de redondance', ['']], ['data-classement', 'propositions : les lignes classées', ['']], ['data-coeurs', 'génération : le réglage des cœurs de calcul', ['']], ['data-coeurs-disponibles', 'génération : la valeur automatique des cœurs', ['']], diff --git a/test/navigateur/ecrans.js b/test/navigateur/ecrans.js index 1280ee3..e5812dc 100644 --- a/test/navigateur/ecrans.js +++ b/test/navigateur/ecrans.js @@ -658,21 +658,22 @@ export const ECRANS = Object.freeze([ nom: 'accueil.mesuresRetenu', mode: null, async atteindre(contexte) { - // La principale, sa proposition retenue : la ligne montre la - // différence du retenu et la meilleure proposition du classement, et le - // pointeur réel sur l'intitulé de la différence — un en-tête de - // colonne du tableau, une fois pour la liste — ouvre son infobulle. + // La principale, sa proposition retenue : la ligne montre les + // collisions cumulées du retenu et la meilleure proposition du + // classement, et le pointeur réel sur l'intitulé des collisions — un + // en-tête de colonne du tableau, une fois pour la liste — ouvre son + // infobulle. const principale = chargePrincipale(); const charge = executer(principale, 'retenirProposition', { id: principale.propositions[0].id }); const m = await contexte.monter({ fichiers: [fichierEtat(charge)] }); const ligne = `[data-evenement="${BASE}"]`; await chargerResumes(m); - await attendre('la différence du retenu et la meilleure proposition', () => - ['[data-colonne="diffRedondance"]', '[data-meilleure-id]'].every((valeur) => /\d/.test(m.racine.querySelector(`${ligne} ${valeur}`)?.textContent ?? '')), + await attendre('les collisions du retenu et la meilleure proposition', () => + ['[data-colonne="collisionsCumulees"]', '[data-meilleure-id]'].every((valeur) => /\d/.test(m.racine.querySelector(`${ligne} ${valeur}`)?.textContent ?? '')), ); - const { x, y } = await centre(await element(m, '[data-ecran="accueil"] thead [data-terme="diffRedondance"]')); + const { x, y } = await centre(await element(m, '[data-ecran="accueil"] thead [data-terme="collisionsCumulees"]')); await pointer('bouger', x, y); - await attendre("l'infobulle de la différence", () => { + await attendre("l'infobulle des collisions", () => { const bulle = m.racine.querySelector('[data-ecran="accueil"] [role="tooltip"]'); return bulle !== null && !bulle.hidden && getComputedStyle(bulle).visibility === 'visible'; });