From c7cd5f91420060f2b85de707b740c7c44227a29d Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 5 Oct 2026 22:52:30 -0400 Subject: [PATCH] [ADD] engine: end-to-end runs on the demonstrations, measured thresholds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The whole chain — search, both verifiers, measure, ceilings, ranking — runs end to end on the demonstrations. At 500 000 moves the large one reaches its proven floor: largest gap 1, no collision, no repeat. Its quality thresholds were set after measurement, with the measurement in the comment, never guessed. Checked: small at 8 for all and certified; conflict variant spreads its 4 collisions over 4 pairs; large at gap 1 with 99 imposed returns; the ranking holds when its order differs from id order. --- FR --- [ADD] moteur : la chaîne de bout en bout, seuils posés après mesure Toute la chaîne — recherche, les deux vérificateurs, mesure, plafonds, classement — tourne de bout en bout sur les démonstrations. À 500 000 mouvements, la grande atteint son plancher prouvé : écart maximal 1, aucune collision, aucune répétition. Ses seuils de qualité sont posés après mesure, la mesure en commentaire, jamais de tête. Vérifié : petite à 8 pour tous et certifiée ; variante en conflit à 4 collisions sur 4 paires ; grande à l'écart 1 avec 99 retours imposés ; le classement tient quand son ordre diffère de celui des identifiants. Assisted-by: Claude Opus 5.5 --- src/moteur/integration.long.test.js | 202 ++++++++++++++++++++++++++ src/moteur/integration.test.js | 217 ++++++++++++++++++++++++++++ 2 files changed, 419 insertions(+) create mode 100644 src/moteur/integration.long.test.js create mode 100644 src/moteur/integration.test.js diff --git a/src/moteur/integration.long.test.js b/src/moteur/integration.long.test.js new file mode 100644 index 0000000..977c614 --- /dev/null +++ b/src/moteur/integration.long.test.js @@ -0,0 +1,202 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuve d'intégration lourde du moteur, sur la grande démonstration +// (§ 5.5, § 12.10.4, § 15.1). Trois propositions traversent la chaîne +// d'integration.test.js, de rechercher au certificat. La configuration est +// exactement tendue et impose ses planchers, que le diagnostic chiffre avant +// la recherche : 99 retours imposés par les 33 ancrages ; un écart +// d'itinéraire maximal d'au moins 1, porté par au moins 24 mobiles ; un +// certificat « minimum atteint » hors d'atteinte. Les seuils de qualité sont +// posés après mesure, la mesure en commentaire (§ 14.1). +import assert from 'node:assert/strict'; +import { describe, test } from '../../test/lanceur.js'; +import { CATALOGUE } from '../demo/catalogue.js'; +import { indexerPlan, normaliser } from './configuration.js'; +import { diagnostiquer } from './diagnostic.js'; +import { mesurer } from './indicateurs.js'; +import { + ecartsAuPlafondAPriori, + ecartsItineraire, + minimumAtteint, + troisChiffres, +} from './manque.js'; +import { plafondsAPriori, plafondsRealises } from './plafond.js'; +import { rechercher } from './recherche.js'; +import { verifierIndicateurs, verifierInvariants } from './verification.js'; + +// Statut d'un ancré dans instance.statut (§ 4.2) ; les autres sont mobiles. +const ANCRE = 2; + +// Compte d'arrêt choisi par la mesure. À 1 000 000 mouvements, chacune des +// trois propositions atteint, sur chaque seuil ci-dessous, la meilleure +// valeur que la configuration permette, sans rencontre répétée ni retour +// choisi. Avec cette graine, chaque seuil tient à partir de son propre +// compte : +// - rencontres minimales 24, sur tous comme sur les ancrés : déjà à +// 100 000 ; +// - écart au plafond a priori maximal 1, et rencontres minimales 27 des +// mobiles : dès 250 000 ; à 200 000, ils valent 2 et 26 ; +// - manque maximal 0, aucun retour choisi : dès 350 000 ; à 300 000, une +// proposition garde un manque de 1 et un retour choisi ; +// - aucune collision : dès 450 000 ; à 400 000, les trois propositions en +// portent 8, 1 et 2. +// 1 000 000 vaut ainsi 4 fois le compte où tiennent l'écart au plafond a +// priori et les mobiles, 2,9 fois celui du manque et des retours choisis, +// 2,2 fois celui des collisions. Sur les graines 1 à 8, les vingt-quatre +// propositions tiennent l'écart au plafond a priori et les mobiles dès +// 250 000, le manque et les retours choisis dès 350 000, les collisions dès +// 500 000 ; à 450 000, deux en portent encore 1 et 5. +const REGLAGES = Object.freeze({ graine: 314_159, arret: 1_000_000, nombre: 3 }); + +// Seuils de qualité, posés après mesure ; chacun est la meilleure valeur +// que la configuration permette. +// +// Rencontres minimales : 24 mesuré à chacune des trois propositions. C'est +// le plafond a priori des quatre animateurs des tables de 7, qu'aucune +// proposition ne dépasse (§ 15.1) : le minimum global ne peut pas le +// dépasser, et l'épreuve exige qu'il l'atteigne. Les deux lignes +// secondaires du § 5.4 se lisent à part : 24 chez les ancrés, que ces +// quatre animateurs gouvernent ; 27 chez les mobiles. Un mobile assis une +// fois à une table de 7 rencontre 27 personnes au plus, une de moins que +// son plafond a priori de 28, et au moins 24 mobiles y passent (§ 15.1) ; +// l'écart au plafond a priori maximal de 1 interdit à chacun d'en +// rencontrer moins. Un agrégat « tous » qui écarterait les ancrés vaudrait +// 27 (§ 5.4). +const RENCONTRES_MIN = 24; +const RENCONTRES_MIN_ANCRES = 24; +const RENCONTRES_MIN_MOBILES = 27; +// Écart au plafond a priori maximal : 1 mesuré à chacune des trois +// propositions. Il ne descend pas sous l'écart d'itinéraire maximal, dont le +// plancher vaut 1 (§ 12.10.4) : ce seuil aussi est la meilleure valeur +// possible. +const ECART_AU_PLAFOND_A_PRIORI_MAX = 1; +// Collisions cumulées : 0 mesuré à chacune des trois propositions, la +// meilleure valeur possible ; le diagnostic n'impose à cette configuration +// aucun plancher de collisions. Une recherche qui ne sépare pas les +// appartenances en laisse de 14 à 20 à chaque proposition. +const COLLISIONS_CUMULEES_MAX = 0; + +// Plus grand élément d'une liste non vide. +const plusGrand = (valeurs) => valeurs.reduce((plus, valeur) => Math.max(plus, valeur)); + +// La génération et ses mesures, calculées au premier appel puis partagées +// par les épreuves du fichier : la recherche en est le coût. Chaque +// proposition porte la liste de ses violations ; ses mesures, ses plafonds +// réalisés, ses trois chiffres et ses écarts d'itinéraire ne sont calculés +// que pour un plan sans violation. indexerPlan n'examine ni capacités ni +// réservations et lève à la première faute de forme, là où le vérificateur +// les nomme toutes. +let generation = null; +function genererGrande() { + if (generation !== null) return generation; + const configuration = CATALOGUE.find(({ cle }) => cle === 'grande').construire(); + const instance = normaliser(configuration); + const aPriori = plafondsAPriori(instance); + const propositions = rechercher(configuration, REGLAGES).map(({ id, plan }) => { + const violations = verifierInvariants(instance, plan); + if (violations.length > 0) return { id, violations }; + const tableDe = indexerPlan(instance, plan); + const mesures = mesurer(instance, tableDe); + const realises = plafondsRealises(instance, tableDe); + return { + id, + violations, + mesures, + realises, + chiffres: troisChiffres(instance, mesures, aPriori, realises), + ecartsItineraire: ecartsItineraire(aPriori, realises), + }; + }); + generation = { instance, aPriori, diagnostic: diagnostiquer(configuration), propositions }; + return generation; +} + +// Les propositions de la génération, chacune jugée par verifierInvariants +// avant toute lecture de ses mesures. Un parcours vide ne prouverait rien +// (§ 14.2). +function propositionsValides() { + const { propositions } = genererGrande(); + assert.equal(propositions.length, REGLAGES.nombre); + for (const { id, violations } of propositions) { + assert.deepEqual(violations, [], `proposition ${id}`); + } + return propositions; +} + +describe('intégration : la grande démonstration de bout en bout (§ 5.5, § 12.10.4, § 15.1)', () => { + test('chaque proposition tient invariants et indicateurs, porte les 99 retours imposés du diagnostic et aucun retour choisi', () => { + const { aPriori, diagnostic } = genererGrande(); + // 33 ancrages de 4 tours, 3 retours chacun : attribués aux réservations, + // les mêmes pour toute proposition (§ 5.4). + assert.equal(diagnostic.retoursImposes, 99); + for (const { id, mesures, realises } of propositionsValides()) { + assert.deepEqual(verifierIndicateurs(mesures, aPriori, realises), [], `proposition ${id}`); + assert.equal(mesures.totalRetoursImposes, diagnostic.retoursImposes, `proposition ${id}`); + // Retours choisis : 0 mesuré à chacune des trois propositions, la + // meilleure valeur possible ; avec cette graine, dès 350 000 + // mouvements (REGLAGES). Les deux comptes restent séparés, chacun à sa + // valeur : leur somme afficherait 99 retours choisis à chaque + // proposition, une constante où la différence entre propositions ne se + // lit plus (§ 5.4, § 14.10). + assert.equal(mesures.totalRetoursChoisis, 0, `proposition ${id}`); + } + }); + + test("le plancher du diagnostic tient : écart d'itinéraire maximal ≥ 1, au moins 24 mobiles sous leur plafond a priori", () => { + const { instance, diagnostic } = genererGrande(); + // Les tables de 7 offrent 96 sièges-tours mobiles, qu'un mobile n'occupe + // que 4 fois au plus : au moins 24 mobiles y passent, et chacun y perd au + // moins 1 sur son plafond a priori de 28 avant toute rencontre, son écart + // d'itinéraire (§ 12.10.4). Sans ce plancher écrit, les comparaisons + // ci-dessous n'éprouveraient rien. + assert.deepEqual(diagnostic.ecartItineraire, { plancher: 1, mobilesAuMoins: 24 }); + const { plancher, mobilesAuMoins } = diagnostic.ecartItineraire; + for (const { id, chiffres, ecartsItineraire: ecarts } of propositionsValides()) { + assert.ok( + chiffres.ecartItineraireMax.tous >= plancher, + `proposition ${id} : écart d'itinéraire maximal ${chiffres.ecartItineraireMax.tous}, plancher ${plancher}`, + ); + let mobilesSous = 0; + for (let p = 0; p < instance.N; p += 1) { + if (instance.statut[p] !== ANCRE && ecarts[p] >= plancher) mobilesSous += 1; + } + assert.ok( + mobilesSous >= mobilesAuMoins, + `proposition ${id} : ${mobilesSous} mobiles sous leur plafond a priori, au moins ${mobilesAuMoins} attendus`, + ); + } + }); + + test('jamais « minimum atteint » (§ 15.1), même quand chacun atteint son plafond réalisé', () => { + const { aPriori } = genererGrande(); + for (const { id, mesures, chiffres } of propositionsValides()) { + // Manque maximal, seuil posé après mesure : 0 à chacune des trois + // propositions, sur les 260 personnes. Chacun rencontre tout ce que son + // itinéraire permet ; un certificat lu sur le seul manque serait + // décerné, et il ne l'est pas : il se lit contre le plafond a priori + // (§ 5.5). + assert.equal(chiffres.manqueMax.tous, 0, `proposition ${id}`); + assert.equal(minimumAtteint(mesures, aPriori), false, `proposition ${id}`); + } + }); + + test('seuils de qualité posés après mesure : rencontres minimales et leurs lignes secondaires, écart au plafond a priori maximal, collisions cumulées', () => { + const { aPriori } = genererGrande(); + for (const { id, mesures } of propositionsValides()) { + const { tous, ancres, mobiles } = mesures.aggRencontres; + assert.equal(tous.min, RENCONTRES_MIN, `proposition ${id} : rencontres minimales`); + assert.equal(ancres.min, RENCONTRES_MIN_ANCRES, `proposition ${id} : rencontres minimales des ancrés`); + assert.equal(mobiles.min, RENCONTRES_MIN_MOBILES, `proposition ${id} : rencontres minimales des mobiles`); + const ecartMax = plusGrand(ecartsAuPlafondAPriori(mesures, aPriori)); + assert.ok( + ecartMax <= ECART_AU_PLAFOND_A_PRIORI_MAX, + `proposition ${id} : écart au plafond a priori maximal ${ecartMax}, seuil ${ECART_AU_PLAFOND_A_PRIORI_MAX}`, + ); + assert.ok( + mesures.collisionsCumulees <= COLLISIONS_CUMULEES_MAX, + `proposition ${id} : ${mesures.collisionsCumulees} collisions cumulées, seuil ${COLLISIONS_CUMULEES_MAX}`, + ); + } + }); +}); diff --git a/src/moteur/integration.test.js b/src/moteur/integration.test.js new file mode 100644 index 0000000..ea1860d --- /dev/null +++ b/src/moteur/integration.test.js @@ -0,0 +1,217 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuve d'intégration du moteur, de la configuration au classement, sur la +// petite démonstration et sa variante conflit (§ 5.5, § 5.6, § 5.7, +// § 15.3). Chaque proposition de rechercher traverse la chaîne entière : +// verifierInvariants juge son plan ; mesurer et plafondsRealises le +// mesurent ; verifierIndicateurs éprouve la ligne de chacun contre le +// plafond a priori ; classer ordonne les propositions, et minimumAtteint lit +// le certificat de la première ; diagnostiquer fournit les planchers que la +// meilleure respecte. Chaque quantité vient de son module (§ 13.2) : +// l'épreuve n'en recalcule aucune. +import assert from 'node:assert/strict'; +import { describe, test } from '../../test/lanceur.js'; +import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js'; +import { CRITERES, classer } from './classement.js'; +import { indexerPlan, normaliser } from './configuration.js'; +import { diagnostiquer } from './diagnostic.js'; +import { mesurer } from './indicateurs.js'; +import { minimumAtteint } from './manque.js'; +import { plafondsAPriori, plafondsRealises } from './plafond.js'; +import { rechercher } from './recherche.js'; +import { verifierIndicateurs, verifierInvariants } from './verification.js'; + +// Réglages de la génération : une graine fixée, écrite ici (§ 15.5, point 2). +// À 200 000 mouvements, avec cette graine, les trois propositions de chaque +// démonstration rencontrent 8 pour les douze, sans rencontre répétée ni +// retour choisi, et ne portent que les collisions que le diagnostic impose : +// aucune dans la petite démonstration, 4 dans sa variante. Les assertions +// des épreuves tiennent avec cette graine dès 80 000 mouvements : 200 000 +// leur laisse une marge de 2,5. Sur les graines 1 à 64, elles tiennent +// toutes à 200 000 ; à 150 000, deux graines en font échouer. +const REGLAGES = Object.freeze({ graine: 314_159, arret: 200_000, nombre: 3 }); + +// La même génération, arrêtée au premier mouvement. Aucune de ses +// propositions n'atteint son plafond a priori : avec cette graine, leurs +// écarts au plafond a priori maximaux valent 2, 3 et 3, et sur les graines +// 1 à 64, aucune proposition brève ne l'atteint non plus. +const REGLAGES_BREVES = Object.freeze({ ...REGLAGES, arret: 1 }); + +// Configuration neuve d'une démonstration du catalogue. +const demo = (cle) => CATALOGUE.find((entree) => entree.cle === cle).construire(); + +// Génère les propositions d'une configuration et les fait traverser la +// chaîne. Chaque plan est jugé par verifierInvariants avant d'être indexé : +// indexerPlan n'examine ni capacités ni réservations et lève à la première +// faute de forme, là où le vérificateur les nomme toutes. Vient ensuite la +// ligne N − 1 ≥ a priori ≥ réalisé ≥ rencontres de chacun. decalage +// s'ajoute aux identifiants que rend rechercher, comme l'appelant qui +// accumule des générations donne à chaque proposition un identifiant unique +// (§ 5.7). Rend le plafond a priori de chacun, et les propositions dans +// l'ordre de leurs identifiants, avec leurs mesures. +function generer(configuration, etiquette, reglages, decalage = 0) { + const instance = normaliser(configuration); + const aPriori = plafondsAPriori(instance); + const propositions = rechercher(configuration, reglages).map(({ id, plan }) => { + const nom = `${etiquette}, proposition ${id + decalage}`; + assert.deepEqual(verifierInvariants(instance, plan), [], nom); + const tableDe = indexerPlan(instance, plan); + const mesures = mesurer(instance, tableDe); + const realises = plafondsRealises(instance, tableDe); + assert.deepEqual(verifierIndicateurs(mesures, aPriori, realises), [], nom); + return { id: id + decalage, mesures }; + }); + // Un parcours vide ne prouverait rien (§ 14.2). + assert.equal(propositions.length, reglages.nombre, etiquette); + return { aPriori, propositions }; +} + +// Le classement de propositions mesurées sur une même configuration, et +// celle qu'il met en tête. +function classerPropositions({ aPriori, propositions }) { + const classement = classer( + propositions.map(({ id, mesures }) => ({ id, mesures, plafondsAPriori: aPriori })), + ); + const premiere = propositions.find(({ id }) => id === classement.ordre[0]); + return { classement, premiere }; +} + +// La génération de la variante conflit aux réglages de l'épreuve, calculée +// au premier appel puis partagée par les deux épreuves qui la lisent : la +// recherche en est le coût. +let variante = null; +function genererVariante() { + variante ??= generer(demo('petite-conflit'), 'petite-conflit', REGLAGES); + return variante; +} + +// Le plan écrit du § 15.3 fait visiter les quatre tables à chacun : aucun +// retour choisi. Le classement ne lit pas les retours (§ 5.7) : entre +// propositions égales sur ses cinq critères, l'identifiant désigne la +// première, qu'elle garde des retours ou non. L'épreuve exige donc le zéro +// d'une proposition au moins, la preuve que la recherche tient compte de +// nouvelleTable. +function exigerUneSansRetourChoisi(etiquette, propositions) { + const retours = propositions.map(({ mesures }) => mesures.totalRetoursChoisis); + assert.ok(retours.includes(0), `${etiquette} : retours choisis par proposition ${retours.join(', ')}`); +} + +describe('intégration : de la configuration au classement (§ 5.5, § 5.7, § 15.3)', () => { + test('petite : chaque proposition tient invariants et indicateurs et ne porte aucune collision, une au moins aucun retour choisi ; la première du classement porte « minimum atteint »', () => { + const generation = generer(demo('petite'), 'petite', REGLAGES); + const { classement, premiere } = classerPropositions(generation); + // Le plafond a priori est connu : aucun critère n'est sauté, et le + // certificat se lit contre lui (§ 5.5). + assert.deepEqual(classement.criteresAppliques, [...CRITERES]); + assert.equal(classement.critereSaute, null); + assert.equal(minimumAtteint(premiere.mesures, generation.aPriori), true); + // Le certificat ne lit que les rencontres : il se décerne aussi à un plan + // qui réunit des collègues. Chaque appartenance de quatre membres peut + // se répartir sur les quatre tables à chaque tour, comme dans le plan + // écrit du § 15.3 : aucune proposition ne réunit de collègues, et le + // compteur de collisions se mesure à zéro (§ 15.3). + for (const { id, mesures } of generation.propositions) { + assert.equal(mesures.collisionsCumulees, 0, `proposition ${id}`); + } + exigerUneSansRetourChoisi('petite', generation.propositions); + }); + + test('petite-conflit : chaque proposition porte les 4 collisions du plancher du diagnostic, une au moins aucun retour choisi ; la meilleure respecte les planchers, rencontre 8 pour les douze, sur 4 paires, excédent 0', () => { + const configuration = demo('petite-conflit'); + // Le groupe de cinq ne se répartit pas sur quatre tables : au moins une + // collision par tour, 4 cumulées sur les 4 tours, portées par une paire + // distincte au moins (§ 15.3). Le diagnostic les chiffre ; une liste vide + // laisserait la boucle ci-dessous sans rien éprouver. + const { collisions } = diagnostiquer(configuration); + assert.deepEqual( + collisions.map(({ plancherCumulees, plancherPairesDistinctes }) => [ + plancherCumulees, + plancherPairesDistinctes, + ]), + [[4, 1]], + ); + const generation = genererVariante(); + const { premiere } = classerPropositions(generation); + // Tout plan valide respecte ces planchers : ils éprouvent l'accord du + // diagnostic et de la mesure, non la recherche. + for (const plancher of collisions) { + const mesure = premiere.mesures.parGroupe.find(({ groupe }) => groupe === plancher.groupe); + assert.ok( + mesure.collisionsCumulees >= plancher.plancherCumulees, + `${plancher.groupe} : ${mesure.collisionsCumulees} collisions cumulées, plancher ${plancher.plancherCumulees}`, + ); + assert.ok( + mesure.pairesDistinctes >= plancher.plancherPairesDistinctes, + `${plancher.groupe} : ${mesure.pairesDistinctes} paires distinctes, plancher ${plancher.plancherPairesDistinctes}`, + ); + } + assert.deepEqual(premiere.mesures.rencontres, Array(12).fill(8)); + // La recherche se juge à ses collisions : chaque proposition atteint le + // plancher des collisions cumulées sans le dépasser. La meilleure les + // porte sur 4 paires distinctes, comme le plan écrit du § 15.3 (épreuve + // suivante). + for (const { id, mesures } of generation.propositions) { + assert.equal(mesures.collisionsCumulees, collisions[0].plancherCumulees, `proposition ${id}`); + } + assert.equal(premiere.mesures.pairesDistinctes, 4); + // Deux voisins par tour, quatre tours : qui rencontre 8 personnes n'en + // revoit aucune. Chaque paire se réunit donc une fois au plus, et + // l'excédent 0 découle de 8 pour les douze. Il éprouve mesurer, non la + // recherche. + assert.equal(premiere.mesures.excedentCollisions, 0); + exigerUneSansRetourChoisi('petite-conflit', generation.propositions); + }); + + test('le plan écrit du § 15.3, mesuré sur la variante conflit : 8 pour les douze, 4 collisions sur 4 paires, excédent 0', () => { + // Le témoin de l'épreuve précédente : la personne passée de C à A + // retrouve un membre de A à chacun des quatre tours, chaque fois un + // autre. Ce que l'épreuve exige de la recherche est donc atteignable. + const instance = normaliser(demo('petite-conflit')); + const mesures = mesurer(instance, indexerPlan(instance, PLAN_PARFAIT_PETITE)); + assert.deepEqual(mesures.rencontres, Array(12).fill(8)); + assert.equal(mesures.collisionsCumulees, 4); + assert.equal(mesures.pairesDistinctes, 4); + assert.equal(mesures.excedentCollisions, 0); + }); + + test('petite-conflit, deux générations accumulées : classer met la plus longue en tête, numérotée après la brève, et départage ses propositions égales par identifiant croissant (§ 5.7, § 15.5)', () => { + // La génération brève prend les identifiants 1 à 3, la longue 4 à 6 : + // l'ordre attendu contredit l'ordre des identifiants, et seul un + // classement qui applique ses critères peut le rendre. + const breve = generer(demo('petite-conflit'), 'petite-conflit, génération brève', REGLAGES_BREVES); + const variante = genererVariante(); + const longue = { + aPriori: variante.aPriori, + propositions: variante.propositions.map(({ id, mesures }) => ({ id: id + REGLAGES.nombre, mesures })), + }; + // Ce que l'ordre départage, mesuré avant d'être lu. Chaque proposition + // longue atteint son plafond a priori, et les trois sont égales sur les + // quatre autres critères du classement ; aucune brève n'atteint le sien. + // Sur une même population, la redondance moyenne ordonne comme la somme + // que lit le classement. + const autresCriteres = ({ mesures }) => [ + mesures.excedentCollisions, + mesures.collisionsCumulees, + mesures.rencontresRepetees.choisies, + mesures.aggRedondance.tous.moyenne, + ]; + for (const proposition of longue.propositions) { + const nom = `proposition ${proposition.id}`; + assert.equal(minimumAtteint(proposition.mesures, longue.aPriori), true, nom); + assert.deepEqual(autresCriteres(proposition), autresCriteres(longue.propositions[0]), nom); + } + for (const { id, mesures } of breve.propositions) { + assert.equal(minimumAtteint(mesures, breve.aPriori), false, `proposition ${id}`); + } + // Le premier critère renvoie les brèves derrière les longues ; les + // longues, égales sur les cinq, se départagent par identifiant croissant + // (§ 15.5, point 4). + const { classement } = classerPropositions({ + aPriori: longue.aPriori, + propositions: [...breve.propositions, ...longue.propositions], + }); + assert.deepEqual(classement.ordre.slice(0, REGLAGES.nombre), [4, 5, 6]); + assert.deepEqual([...classement.ordre.slice(REGLAGES.nombre)].sort((a, b) => a - b), [1, 2, 3]); + }); +});