diff --git a/src/application/empreintes.test.js b/src/application/empreintes.test.js new file mode 100644 index 0000000..34a6de0 --- /dev/null +++ b/src/application/empreintes.test.js @@ -0,0 +1,137 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Classement de référence (§ 5.7, § 14.10) : une charge de propositions +// engendrées par rechercher, enregistrées par la commande, s'évalue par +// evaluer sous les quatre cases cochées, plafond a priori calculé puis +// inconnu. L'ordre, les critères appliqués, le critère sauté et les grandeurs +// nommées de chaque proposition se comparent au fichier versionné +// test/fixtures/moteur/empreintes_defaut.json, que seule +// src/moteur/empreintes.test.js récrit, sous GTT_EMPREINTES=ecrire : cette +// épreuve-ci ne fait que relire, et refuse de se jouer sous ce mode. Les +// grandeurs de chaque proposition sont celles qu'une épreuve des seize +// combinaisons relit pour en déduire l'ordre attendu sous chacune. +// +// Un ordre figé ne garde un critère que s'il y décide quelque chose : la +// référence est tenue de faire de chaque critère appliqué, dans un de ses +// classements au moins, le premier qui départage deux propositions voisines, +// et un critère dont le retrait change l'ordre. Sans quoi un comparateur qui +// inverse ou oublie ce critère rend le même ordre que la référence. +import assert from 'node:assert/strict'; +import { describe, test } from '../../test/lanceur.js'; +import { + CASES, + CLASSEMENT, + chargeDeClassement, + classementLu, + empreintesDuClassement, + exigerVerification, + lireEmpreintes, +} from '../../test/empreintes_moteur.js'; +import { evaluer } from './evaluation.js'; + +exigerVerification(process.env); +const REFERENCE = lireEmpreintes(); + +// Les deux classements de chaque charge, sous le nom de leur plafond a priori. +const PLAFONDS = Object.freeze(['connu', 'inconnu']); + +// Pour chaque critère du classement, le nom de la grandeur nommée qu'il lit +// dans le fichier. +const GRANDEUR_DU_CRITERE = new Map([ + ['ecartAuPlafondAPrioriMax', 'ecartAuPlafondAPrioriMax'], + ['excedentCollisions', 'excedentCollisions'], + ['collisionsCumulees', 'collisionsCumulees'], + ['rencontresRepetees', 'rencontresRepeteesChoisies'], + ['redondance', 'redondance'], +]); + +// La valeur d'un critère pour la proposition id, lue dans les grandeurs de +// la référence. Lève pour un critère sans grandeur, en le nommant. +function valeur(grandeurs, id, critere) { + const nom = GRANDEUR_DU_CRITERE.get(critere); + if (nom === undefined) throw new Error(`critère sans grandeur nommée : ${critere}`); + return grandeurs[String(id)][nom]; +} + +// Le premier critère de criteres sur lequel deux propositions diffèrent ; +// null quand elles sont égales sur tous. +function premierQuiDepartage(grandeurs, criteres, a, b) { + return criteres.find((critere) => valeur(grandeurs, a, critere) !== valeur(grandeurs, b, critere)) ?? null; +} + +// Les identifiants ids rangés critère par critère dans l'ordre de criteres, +// chacun croissant, puis par identifiant croissant : le classement du § 5.7, +// refait sur les grandeurs du fichier. +function ordonner(grandeurs, criteres, ids) { + return [...ids].sort((a, b) => { + const critere = premierQuiDepartage(grandeurs, criteres, a, b); + return critere === null ? a - b : valeur(grandeurs, a, critere) - valeur(grandeurs, b, critere); + }); +} + +// Chaque classement de la référence, nommé par sa charge et son plafond a +// priori, avec les grandeurs de ses propositions. +function classementsDeReference() { + return CLASSEMENT.cles.flatMap((cle) => + PLAFONDS.map((plafond) => ({ + nom: `${cle}, plafond a priori ${plafond}`, + ...REFERENCE.classements[cle][plafond], + grandeurs: REFERENCE.classements[cle].propositions, + })), + ); +} + +describe('classement de référence : evaluer sous les quatre cases cochées (§ 5.7, § 14.10)', () => { + test(`la référence porte ${CLASSEMENT.cles.join(' et ')}, ${CLASSEMENT.nombre} propositions chacune`, () => { + assert.deepEqual(Object.keys(REFERENCE.classements), CLASSEMENT.cles); + for (const cle of CLASSEMENT.cles) { + const { propositions } = REFERENCE.classements[cle]; + assert.equal(Object.keys(propositions).length, CLASSEMENT.nombre, cle); + } + }); + + for (const cle of CLASSEMENT.cles) { + test(`${cle} : l’ordre et les critères appliqués, plafond a priori calculé puis inconnu, sont ceux de la référence`, () => { + const charge = chargeDeClassement(cle); + assert.deepEqual(CASES.map((nom) => charge.reglages[nom]), [true, true, true, true], `${cle} : les quatre cases cochées`); + const attendu = REFERENCE.classements[cle]; + assert.deepEqual(classementLu(evaluer(charge).classement), attendu.connu, `${cle}, plafond a priori calculé`); + assert.deepEqual( + classementLu(evaluer(charge, { plafondsAPriori: null }).classement), + attendu.inconnu, + `${cle}, plafond a priori inconnu`, + ); + }); + + test(`${cle} : les grandeurs nommées de chaque proposition sont celles de la référence`, () => { + assert.deepEqual(empreintesDuClassement(cle).propositions, REFERENCE.classements[cle].propositions, cle); + }); + } +}); + +describe('classement de référence : chaque critère appliqué y décide (§ 5.7, § 14.10)', () => { + test('les grandeurs de la référence, rangées sous ses critères appliqués, redonnent son ordre', () => { + for (const { nom, ordre, criteresAppliques, grandeurs } of classementsDeReference()) { + assert.deepEqual(ordonner(grandeurs, criteresAppliques, ordre), ordre, nom); + } + }); + + test('chaque critère appliqué départage le premier deux propositions voisines, et son retrait change l’ordre', () => { + const classements = classementsDeReference(); + const criteres = [...new Set(classements.flatMap(({ criteresAppliques }) => criteresAppliques))]; + const sansEffet = []; + for (const critere of criteres) { + const departage = classements.some(({ ordre, criteresAppliques, grandeurs }) => + ordre.slice(1).some((id, rang) => premierQuiDepartage(grandeurs, criteresAppliques, ordre[rang], id) === critere), + ); + const compte = classements.some(({ ordre, criteresAppliques, grandeurs }) => { + const sans = criteresAppliques.filter((autre) => autre !== critere); + return criteresAppliques.includes(critere) && ordonner(grandeurs, sans, ordre).join() !== ordre.join(); + }); + if (!departage) sansEffet.push(`${critere} : ne départage le premier aucune paire voisine`); + if (!compte) sansEffet.push(`${critere} : son retrait ne change aucun ordre`); + } + assert.deepEqual(sansEffet, []); + }); +}); diff --git a/src/moteur/empreintes.long.test.js b/src/moteur/empreintes.long.test.js new file mode 100644 index 0000000..b50645a --- /dev/null +++ b/src/moteur/empreintes.long.test.js @@ -0,0 +1,39 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Empreintes de référence du moteur, série node-long (§ 5.7, § 14.10, +// § 15.5) : la grande démonstration et sa variante sans exception, sous les +// trois graines et le compte réduit de test/empreintes_moteur.js. Chaque plan +// rendu par rechercher se compare, empreinte et grandeurs nommées, au fichier +// versionné test/fixtures/moteur/empreintes_defaut.json, que seule +// src/moteur/empreintes.test.js récrit, sous GTT_EMPREINTES=ecrire : cette +// épreuve-ci ne fait que relire, et refuse de se jouer sous ce mode, où elle +// pourrait lire un fichier en cours d'écriture. +import assert from 'node:assert/strict'; +import { describe, test } from '../../test/lanceur.js'; +import { + ARRET, + GRAINES, + differences, + empreintesDesPlans, + exigerVerification, + lireEmpreintes, +} from '../../test/empreintes_moteur.js'; +import { HISTORIQUE_PAR_DEFAUT } from './recherche.js'; + +exigerVerification(process.env); +const REFERENCE = lireEmpreintes(); + +describe('empreintes de référence : la grande démonstration et sa variante sans exception (§ 14.10)', () => { + test('la référence a été relevée à l’historique par défaut de la recherche', () => { + assert.equal(REFERENCE.reglages.historique, HISTORIQUE_PAR_DEFAUT); + }); + + for (const cle of ['grande', 'grande-sans-exception']) { + test(`${cle} : les plans des graines ${GRAINES.join(', ')}, à ${ARRET} mouvements, sont ceux de la référence`, () => { + const calcule = empreintesDesPlans(cle); + assert.deepEqual(differences(cle, calcule, REFERENCE.plans[cle]), []); + assert.deepEqual(calcule, REFERENCE.plans[cle], cle); + }); + } +}); diff --git a/src/moteur/empreintes.test.js b/src/moteur/empreintes.test.js new file mode 100644 index 0000000..08199c4 --- /dev/null +++ b/src/moteur/empreintes.test.js @@ -0,0 +1,180 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Empreintes de référence du moteur, série node (§ 5.7, § 14.10, § 15.5) : la +// petite démonstration et sa variante au conflit inévitable, sous les trois +// graines et le compte réduit de test/empreintes_moteur.js, et la petite sous +// chacune des seize combinaisons des quatre cases ; les deux encore sous le +// compte plus long de la section bascule, où l'ordre d'acceptation qui suit +// l'annulation de l'écart laisse sa trace dans le plan. Chaque plan rendu par +// rechercher se compare, empreinte et grandeurs nommées, au fichier versionné +// test/fixtures/moteur/empreintes_defaut.json : une empreinte qui change est +// un défaut, jamais une régénération. Le fichier ne se récrit que sous +// GTT_EMPREINTES=ecrire, par cette seule épreuve, qui recalcule alors toutes +// les sections — la grande et le classement compris — avant de les relire. +// La grande et sa variante sans exception se vérifient dans la série +// node-long (empreintes.long.test.js), le classement par evaluer dans +// src/application/empreintes.test.js. +import assert from 'node:assert/strict'; +import { describe, test } from '../../test/lanceur.js'; +import { + ARRET, + BASCULE, + CASES, + CLASSEMENT, + GRAINES, + GRANDEURS, + combinaisons, + differences, + ecrireEmpreintes, + empreinteDe, + empreinteDuPlan, + empreintesDesCombinaisons, + empreintesDesPlans, + lireEmpreintes, + modeEmpreintes, +} from '../../test/empreintes_moteur.js'; +import { CATALOGUE } from '../demo/catalogue.js'; +import { indexerPlan, normaliser } from './configuration.js'; +import { HISTORIQUE_PAR_DEFAUT, rechercher } from './recherche.js'; + +if (modeEmpreintes(process.env) === 'ecrire') ecrireEmpreintes(); +const REFERENCE = lireEmpreintes(); + +// Les configurations de cette série, dans l'ordre du catalogue. +const PETITES = ['petite', 'petite-conflit']; + +describe('empreintes de référence : la petite démonstration et sa variante (§ 14.10)', () => { + for (const cle of PETITES) { + test(`${cle} : les plans des graines ${GRAINES.join(', ')}, à ${ARRET} mouvements, sont ceux de la référence`, () => { + const calcule = empreintesDesPlans(cle); + assert.deepEqual(differences(cle, calcule, REFERENCE.plans[cle]), []); + assert.deepEqual(calcule, REFERENCE.plans[cle], cle); + }); + } + + test('petite : sous chacune des seize combinaisons des quatre cases, graine 1, le plan est celui de la référence', () => { + const calcule = empreintesDesCombinaisons(); + assert.equal(Object.keys(REFERENCE.combinaisons).length, 16); + assert.deepEqual(differences('petite', calcule, REFERENCE.combinaisons), []); + assert.deepEqual(calcule, REFERENCE.combinaisons); + }); + + for (const cle of BASCULE.cles) { + test(`${cle} : à ${BASCULE.arret} mouvements, où la meilleure affectation s’améliore après la bascule, les plans sont ceux de la référence`, () => { + const calcule = empreintesDesPlans(cle, BASCULE.arret); + assert.deepEqual(differences(`${cle} (bascule)`, calcule, REFERENCE.bascule[cle]), []); + assert.deepEqual(calcule, REFERENCE.bascule[cle], cle); + }); + } + + test('la référence a été relevée sous les réglages de test/empreintes_moteur.js', () => { + assert.deepEqual(REFERENCE.reglages, { + graines: [...GRAINES], + arret: ARRET, + historique: HISTORIQUE_PAR_DEFAUT, + cases: [...CASES], + classement: { ...CLASSEMENT, cles: [...CLASSEMENT.cles] }, + bascule: { ...BASCULE, cles: [...BASCULE.cles] }, + }); + }); + + test('la référence couvre les quatre configurations du catalogue, chacune sous chaque graine', () => { + assert.deepEqual( + Object.keys(REFERENCE.plans), + CATALOGUE.map(({ cle }) => cle), + ); + for (const { cle } of CATALOGUE) { + assert.deepEqual(Object.keys(REFERENCE.plans[cle]), GRAINES.map(String), cle); + } + }); +}); + +describe('empreintes de référence : ce que porte une empreinte', () => { + // Un plan de la petite variante et son instance, la configuration du + // catalogue sous la graine 1. + function unPlan() { + const configuration = CATALOGUE.find(({ cle }) => cle === 'petite-conflit').construire(); + const [{ plan }] = rechercher(configuration, { graine: 1, arret: 2_000, nombre: 1 }); + return { instance: normaliser(configuration), plan }; + } + + test('les grandeurs nommées sont les six du § 14.10, dans cet ordre, et aucune composante du n-uplet', () => { + assert.deepEqual(GRANDEURS, [ + 'ecartAuPlafondAPrioriMax', + 'excedentCollisions', + 'collisionsCumulees', + 'rencontresRepeteesChoisies', + 'retoursChoisis', + 'redondance', + ]); + const { instance, plan } = unPlan(); + assert.deepEqual(Object.keys(empreinteDuPlan(instance, plan).grandeurs), GRANDEURS); + }); + + test('deux plans qui ne diffèrent que par la table d’une personne à un tour ont deux empreintes', () => { + const { instance, plan } = unPlan(); + const avant = empreinteDuPlan(instance, plan); + // La première personne des deux premières tables du tour 1 échange sa + // place : les grandeurs peuvent rester égales, le plan ne l'est pas. + const tours = plan.tours.map((tour) => tour.map((liste) => [...liste])); + const [a] = tours[0][0]; + const [b] = tours[0][1]; + tours[0][0][0] = b; + tours[0][1][0] = a; + const apres = empreinteDuPlan(instance, { ...plan, tours }); + assert.notEqual(apres.empreinte, avant.empreinte); + assert.notDeepEqual(indexerPlan(instance, { ...plan, tours }), indexerPlan(instance, plan)); + }); + + test('l’empreinte lit chaque grandeur sous son nom : une grandeur changée change l’empreinte', () => { + const { instance, plan } = unPlan(); + const { empreinte, grandeurs } = empreinteDuPlan(instance, plan); + const tableDe = indexerPlan(instance, plan); + for (const nom of GRANDEURS) { + const autre = { ...grandeurs, [nom]: grandeurs[nom] + 1 }; + assert.notEqual(empreinteDe(tableDe, autre), empreinte, nom); + } + assert.equal(empreinteDe(tableDe, grandeurs), empreinte); + }); + + test('les seize combinaisons sont distinctes, chacune nommée par ses cases cochées', () => { + const toutes = combinaisons(); + assert.equal(toutes.length, 16); + assert.equal(new Set(toutes.map(({ cle }) => cle)).size, 16); + assert.deepEqual(toutes[0], { cle: CASES.join('+'), contraintes: Object.fromEntries(CASES.map((c) => [c, true])) }); + assert.deepEqual(toutes.at(-1), { cle: 'aucune', contraintes: Object.fromEntries(CASES.map((c) => [c, false])) }); + for (const { cle, contraintes } of toutes) { + assert.deepEqual(Object.keys(contraintes), CASES, cle); + const cochees = CASES.filter((c) => contraintes[c]); + assert.equal(cle, cochees.length === 0 ? 'aucune' : cochees.join('+')); + } + }); + + test('differences nomme la configuration, la graine absente de la référence et la grandeur changée', () => { + const attendu = { 1: { empreinte: 'a', grandeurs: { excedentCollisions: 2, redondance: 5 } } }; + assert.deepEqual(differences('petite', attendu, attendu), []); + assert.deepEqual( + differences('petite', { 4: attendu[1] }, attendu), + ['petite, 4 : absente de la référence', 'petite, 1 : référence non recalculée'], + ); + assert.deepEqual( + differences('petite', { 1: { empreinte: 'b', grandeurs: { excedentCollisions: 3, redondance: 5 } } }, attendu), + ['petite, 1 : excedentCollisions 2 → 3'], + ); + assert.deepEqual( + differences('petite', { 1: { empreinte: 'b', grandeurs: attendu[1].grandeurs } }, attendu), + ['petite, 1 : plan changé, grandeurs égales'], + ); + }); +}); + +describe('GTT_EMPREINTES : verifier par défaut, ecrire sur demande', () => { + test('absente ou vide : verifier ; verifier et ecrire se lisent tels quels ; une autre valeur lève en la nommant', () => { + assert.equal(modeEmpreintes({}), 'verifier'); + assert.equal(modeEmpreintes({ GTT_EMPREINTES: '' }), 'verifier'); + assert.equal(modeEmpreintes({ GTT_EMPREINTES: 'verifier' }), 'verifier'); + assert.equal(modeEmpreintes({ GTT_EMPREINTES: 'ecrire' }), 'ecrire'); + assert.throws(() => modeEmpreintes({ GTT_EMPREINTES: 'ecrir' }), /GTT_EMPREINTES.*« ecrir »/); + }); +}); diff --git a/test/empreintes_moteur.js b/test/empreintes_moteur.js new file mode 100644 index 0000000..3b85a86 --- /dev/null +++ b/test/empreintes_moteur.js @@ -0,0 +1,392 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Empreintes de référence du moteur (§ 5.7, § 14.10, § 15.5) : ce qui se +// calcule, et le fichier versionné qui en garde la valeur, +// test/fixtures/moteur/empreintes_defaut.json. +// +// Empreinte d'un plan. Le plan indexé tableDe (indexerPlan : une case par +// personne et par tour, dans l'ordre canonique de l'instance, l'index de la +// table ou RESERVE) s'écrit en entiers de 32 bits petit-boutistes, suivis du +// texte JSON des grandeurs nommées, chacune sous son nom, dans l'ordre de +// GRANDEURS ; l'empreinte est le SHA-256 de ces octets, en hexadécimal. Les +// grandeurs sont celles que le classement lit — le plus grand écart au +// plafond a priori, l'excédent de collisions, les collisions cumulées, les +// rencontres répétées choisies, les retours choisis, la redondance —, lues +// dans mesurer et dans manque.js, jamais dans le n-uplet de la recherche : +// une composante toujours nulle qu'on retire ou qu'on ajoute ne change aucune +// empreinte. Le fichier garde aussi les grandeurs en clair, pour que l'échec +// nomme celle qui a changé. +// +// Ce que le fichier porte, chaque section dans cet ordre : +// - reglages : les constantes de ce module sous lesquelles il a été relevé ; +// - plans : pour chaque configuration du catalogue, dans son ordre, et chaque +// graine de GRAINES, l'empreinte du plan que rend rechercher à ARRET +// mouvements, une proposition, historique par défaut, quatre cases cochées ; +// - combinaisons : la petite démonstration sous chacune des seize +// combinaisons des quatre cases, graine 1, mêmes réglages ; +// - bascule : les configurations de BASCULE sous chaque graine de GRAINES, à +// BASCULE.arret mouvements, assez pour que la meilleure affectation +// s'améliore encore après la bascule vers l'ordre des contraintes : à +// ARRET, sur ces configurations, elle ne change plus une fois l'écart +// annulé, et l'ordre des composantes après la bascule ne laisse aucune +// trace dans les plans ; +// - classements : pour chaque charge de CLASSEMENT.cles, le classement +// qu'evaluer rend sous les quatre cases cochées, plafond a priori calculé +// (connu) puis inconnu, et les grandeurs nommées de chaque proposition. +// +// GTT_EMPREINTES choisit le mode, sur le modèle de GTT_CAPTURES : verifier +// par défaut ; ecrire récrit le fichier, par la seule épreuve +// src/moteur/empreintes.test.js, et seulement sur l'état du moteur que le +// fichier doit garder. Une empreinte qui change est un défaut, jamais une +// régénération : écrire est un geste conscient (§ 15.5, point 5). +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { appliquerCommande } from '../src/application/commandes.js'; +import { evaluer } from '../src/application/evaluation.js'; +import { CATALOGUE } from '../src/demo/catalogue.js'; +import { texteLivre } from '../src/demo/livrees.js'; +import { indexerPlan, normaliser } from '../src/moteur/configuration.js'; +import { mesurer } from '../src/moteur/indicateurs.js'; +import { ecartsAuPlafondAPriori } from '../src/moteur/manque.js'; +import { plafondsAPriori } from '../src/moteur/plafond.js'; +import { HISTORIQUE_PAR_DEFAUT, rechercher } from '../src/moteur/recherche.js'; +import { analyser, configurationDepuisCharge } from '../src/stockage/document.js'; +import { VERSION } from '../src/version.genere.js'; + +/** Le fichier des empreintes de référence. */ +export const CHEMIN_EMPREINTES = fileURLToPath(new URL('./fixtures/moteur/empreintes_defaut.json', import.meta.url)); + +/** Les graines des plans de référence, une proposition chacune. */ +export const GRAINES = Object.freeze([1, 2, 3]); + +/** Le compte d'arrêt réduit des plans de référence. */ +export const ARRET = 20_000; + +/** + * Les plans de la section bascule (en-tête du module) : les configurations + * et le compte d'arrêt, les graines étant celles de GRAINES. + */ +export const BASCULE = Object.freeze({ + cles: Object.freeze(['petite', 'petite-conflit']), + arret: 50_000, +}); + +/** Les quatre cases du § 5.3, dans l'ordre où une combinaison les nomme. */ +export const CASES = Object.freeze([ + 'separerAppartenances', + 'nouveauxVoisins', + 'nouvelleTable', + 'varierAppartenances', +]); + +/** + * Les charges du classement de référence, des démonstrations livrées, et la + * génération qui leur donne leurs propositions : nombre propositions de + * graine et arret. Le compte court laisse les propositions inégales sur + * plusieurs critères, ce dont les seize combinaisons ont besoin pour + * départager ; le nombre donne à des propositions voisines des égalités + * jusqu'aux derniers critères. Sous ces grandeurs, chaque critère appliqué + * est, dans un classement au moins, le premier qui départage deux + * propositions voisines, et son retrait change l'ordre : + * src/application/empreintes.test.js le vérifie sur le fichier. + */ +export const CLASSEMENT = Object.freeze({ + cles: Object.freeze(['petite-conflit', 'grande']), + graine: 7, + arret: 1_000, + nombre: 12, +}); + +const MODES = Object.freeze(['verifier', 'ecrire']); + +// Les grandeurs nommées, chacune avec sa lecture dans les mesures d'un plan +// et le plus grand de ses écarts au plafond a priori, null quand ce plafond +// est inconnu. +const LECTURES = Object.freeze([ + ['ecartAuPlafondAPrioriMax', (_mesures, ecartMax) => ecartMax], + ['excedentCollisions', (mesures) => mesures.excedentCollisions], + ['collisionsCumulees', (mesures) => mesures.collisionsCumulees], + ['rencontresRepeteesChoisies', (mesures) => mesures.rencontresRepetees.choisies], + ['retoursChoisis', (mesures) => mesures.totalRetoursChoisis], + ['redondance', (mesures) => mesures.totalRedondance], +]); + +/** Les noms des grandeurs qu'une empreinte porte, dans leur ordre. */ +export const GRANDEURS = Object.freeze(LECTURES.map(([nom]) => nom)); + +/** + * Le mode que GTT_EMPREINTES demande : 'verifier' quand la variable est + * absente ou vide. Seule lecture de cette variable. + * + * @param {Record} env + * @returns {'verifier'|'ecrire'} + * @throws {Error} une autre valeur, nommée + */ +export function modeEmpreintes(env) { + const valeur = env.GTT_EMPREINTES ?? ''; + if (valeur === '') return 'verifier'; + if (MODES.includes(valeur)) return valeur; + throw new Error(`GTT_EMPREINTES : « ${valeur} » n’est ni ${MODES.map((m) => `« ${m} »`).join(' ni ')}`); +} + +/** + * Refuse le mode ecrire : une épreuve qui ne fait que relire le fichier + * pourrait le lire pendant que src/moteur/empreintes.test.js le récrit. + * + * @param {Record} env + * @throws {Error} sous GTT_EMPREINTES=ecrire, ou une valeur inconnue + */ +export function exigerVerification(env) { + if (modeEmpreintes(env) === 'ecrire') { + throw new Error('GTT_EMPREINTES=ecrire se joue sur src/moteur/empreintes.test.js seule ; cette épreuve ne fait que relire'); + } +} + +// Le plus grand élément d'une liste ; null pour une liste vide ou absente. +function plusGrand(valeurs) { + if (valeurs === null || valeurs.length === 0) return null; + let plus = valeurs[0]; + for (const valeur of valeurs) if (valeur > plus) plus = valeur; + return plus; +} + +/** + * Les grandeurs nommées d'un plan mesuré, dans l'ordre de GRANDEURS. + * + * @param {import('../src/moteur/indicateurs.js').Mesures} mesures + * @param {number|null} ecartMax le plus grand écart au plafond a priori + * @returns {Record} + */ +export function grandeursDe(mesures, ecartMax) { + return Object.fromEntries(LECTURES.map(([nom, lire]) => [nom, lire(mesures, ecartMax)])); +} + +/** + * L'empreinte d'un plan indexé et de ses grandeurs nommées (en-tête du + * module). Ne lit des grandeurs que celles de GRANDEURS, chacune sous son + * nom. + * + * @param {ArrayLike} tableDe + * @param {Record} grandeurs + * @returns {string} 64 chiffres hexadécimaux + */ +export function empreinteDe(tableDe, grandeurs) { + const octets = Buffer.alloc(tableDe.length * 4); + for (let i = 0; i < tableDe.length; i += 1) octets.writeInt32LE(tableDe[i], i * 4); + const texte = JSON.stringify(GRANDEURS.map((nom) => [nom, grandeurs[nom]])); + return createHash('sha256').update(octets).update(texte, 'utf8').digest('hex'); +} + +/** + * L'empreinte d'un plan et ses grandeurs nommées. Le plafond a priori se + * calcule quand l'appelant ne le donne pas. + * + * @param {import('../src/moteur/types.js').Instance} instance + * @param {import('../src/moteur/types.js').Plan} plan + * @param {ArrayLike} [aPriori] + * @returns {{empreinte: string, grandeurs: Record}} + */ +export function empreinteDuPlan(instance, plan, aPriori = plafondsAPriori(instance)) { + const tableDe = indexerPlan(instance, plan); + const mesures = mesurer(instance, tableDe); + const grandeurs = grandeursDe(mesures, plusGrand(ecartsAuPlafondAPriori(mesures, aPriori))); + return { empreinte: empreinteDe(tableDe, grandeurs), grandeurs }; +} + +// La configuration de la démonstration du catalogue de clé cle. +function configurationDuCatalogue(cle) { + const entree = CATALOGUE.find((candidate) => candidate.cle === cle); + if (entree === undefined) throw new RangeError(`configuration inconnue du catalogue : ${cle}`); + return entree.construire(); +} + +// L'empreinte du plan que rechercher rend pour une configuration sous une +// graine, une proposition, à arret mouvements, historique par défaut. +function empreinteRecherchee(configuration, graine, arret = ARRET) { + const instance = normaliser(configuration); + const [{ plan }] = rechercher(configuration, { graine, arret, nombre: 1 }); + return empreinteDuPlan(instance, plan); +} + +/** + * Les empreintes des plans d'une configuration du catalogue, une par graine + * de GRAINES, sous la clé de la graine, à arret mouvements. + * + * @param {string} cle + * @param {number} [arret=ARRET] + * @returns {Record} + */ +export function empreintesDesPlans(cle, arret = ARRET) { + const configuration = configurationDuCatalogue(cle); + return Object.fromEntries( + GRAINES.map((graine) => [String(graine), empreinteRecherchee(configuration, graine, arret)]), + ); +} + +/** + * Les seize combinaisons des quatre cases, la première toutes cochées, la + * dernière aucune, chacune nommée par ses cases cochées jointes par « + », + * ou « aucune ». + * + * @returns {{cle: string, contraintes: Record}[]} + */ +export function combinaisons() { + const toutes = []; + for (let masque = 2 ** CASES.length - 1; masque >= 0; masque -= 1) { + const contraintes = Object.fromEntries( + CASES.map((nom, rang) => [nom, ((masque >> (CASES.length - 1 - rang)) & 1) === 1]), + ); + const cochees = CASES.filter((nom) => contraintes[nom]); + toutes.push({ cle: cochees.length === 0 ? 'aucune' : cochees.join('+'), contraintes }); + } + return toutes; +} + +/** + * Les empreintes des plans de la petite démonstration sous chacune des seize + * combinaisons, graine 1, sous la clé de la combinaison. + * + * @returns {Record} + */ +export function empreintesDesCombinaisons() { + return Object.fromEntries( + combinaisons().map(({ cle, contraintes }) => [ + cle, + empreinteRecherchee({ ...configurationDuCatalogue('petite'), contraintes }, GRAINES[0]), + ]), + ); +} + +/** + * La charge du classement de référence : la démonstration livrée de clé + * cle, et la génération de CLASSEMENT enregistrée par la commande. + * + * @param {string} cle + * @returns {import('../src/stockage/types.js').Charge} + */ +export function chargeDeClassement(cle) { + const charge = analyser(texteLivre(cle)).charge; + const configuration = configurationDepuisCharge(charge); + const { graine, arret, nombre } = CLASSEMENT; + const propositions = rechercher(configuration, { graine, arret, nombre }); + return appliquerCommande('enregistrerGeneration', charge, { + propositions, + configuration, + produitVersion: VERSION.affichee, + }).charge; +} + +/** + * Ce que l'empreinte garde d'un classement : l'ordre, les noms des critères + * appliqués dans leur ordre, le critère sauté. + * + * @param {{ordre: number[], criteresAppliques: string[], critereSaute: string|null}} classement + * @returns {{ordre: number[], criteresAppliques: string[], critereSaute: string|null}} + */ +export function classementLu({ ordre, criteresAppliques, critereSaute }) { + return { ordre: [...ordre], criteresAppliques: [...criteresAppliques], critereSaute }; +} + +/** + * Le classement de référence d'une charge de CLASSEMENT.cles : connu et + * inconnu, ses classements par evaluer, plafond a priori calculé puis + * inconnu ; propositions, les grandeurs nommées de chacune, plafond a priori + * calculé, par identifiant croissant. + * + * @param {string} cle + * @returns {{connu: object, inconnu: object, propositions: Record>}} + * @throws {Error} une proposition qui ne se compare pas : elle n'a pas de + * grandeurs + */ +export function empreintesDuClassement(cle) { + const charge = chargeDeClassement(cle); + const connu = evaluer(charge); + const inconnu = evaluer(charge, { plafondsAPriori: null }); + const ligneDe = new Map(connu.propositions.map((ligne) => [ligne.id, ligne])); + const propositions = [...charge.propositions] + .sort((a, b) => a.id - b.id) + .map(({ id }) => { + const ligne = ligneDe.get(id); + if (ligne.statut !== 'comparable') throw new Error(`${cle}, proposition ${id} : ${ligne.statut}`); + return [String(id), grandeursDe(ligne.mesures, ligne.ecartAuPlafondAPrioriMax)]; + }); + return { + connu: classementLu(connu.classement), + inconnu: classementLu(inconnu.classement), + propositions: Object.fromEntries(propositions), + }; +} + +/** + * Tout le contenu du fichier des empreintes, recalculé (en-tête du module). + * + * @returns {object} + */ +export function calculerEmpreintes() { + return { + reglages: { + graines: [...GRAINES], + arret: ARRET, + historique: HISTORIQUE_PAR_DEFAUT, + cases: [...CASES], + classement: { ...CLASSEMENT, cles: [...CLASSEMENT.cles] }, + bascule: { ...BASCULE, cles: [...BASCULE.cles] }, + }, + plans: Object.fromEntries(CATALOGUE.map(({ cle }) => [cle, empreintesDesPlans(cle)])), + combinaisons: empreintesDesCombinaisons(), + bascule: Object.fromEntries(BASCULE.cles.map((cle) => [cle, empreintesDesPlans(cle, BASCULE.arret)])), + classements: Object.fromEntries(CLASSEMENT.cles.map((cle) => [cle, empreintesDuClassement(cle)])), + }; +} + +/** Le fichier des empreintes, tel qu'il est versionné. */ +export function lireEmpreintes() { + return JSON.parse(readFileSync(CHEMIN_EMPREINTES, 'utf8')); +} + +/** + * Récrit le fichier des empreintes à partir du moteur courant : un fichier + * voisin, puis un renommage, pour qu'aucune lecture ne trouve un fichier à + * demi écrit. + */ +export function ecrireEmpreintes() { + const texte = `${JSON.stringify(calculerEmpreintes(), null, 2)}\n`; + mkdirSync(dirname(CHEMIN_EMPREINTES), { recursive: true }); + const provisoire = `${CHEMIN_EMPREINTES}.ecriture`; + writeFileSync(provisoire, texte); + renameSync(provisoire, CHEMIN_EMPREINTES); +} + +/** + * Ce qui sépare des empreintes recalculées de celles de la référence, une + * ligne par écart, chacune ouverte par la configuration et la clé : une clé + * absente de la référence, une clé de la référence non recalculée, les + * grandeurs changées, ou un plan changé sous des grandeurs égales. [] quand + * rien ne les sépare. + * + * @param {string} cle + * @param {Record} calcule + * @param {Record} [attendu] + * @returns {string[]} + */ +export function differences(cle, calcule, attendu = {}) { + const nomsCalcules = Object.keys(calcule); + const nomsAttendus = Object.keys(attendu); + const lignes = [ + ...nomsCalcules.filter((nom) => !nomsAttendus.includes(nom)).map((nom) => `${cle}, ${nom} : absente de la référence`), + ...nomsAttendus.filter((nom) => !nomsCalcules.includes(nom)).map((nom) => `${cle}, ${nom} : référence non recalculée`), + ]; + for (const nom of nomsCalcules.filter((n) => nomsAttendus.includes(n))) { + const avant = attendu[nom]; + const apres = calcule[nom]; + const changees = GRANDEURS.filter((g) => apres.grandeurs[g] !== avant.grandeurs[g]).map( + (g) => `${g} ${avant.grandeurs[g]} → ${apres.grandeurs[g]}`, + ); + if (changees.length > 0) lignes.push(`${cle}, ${nom} : ${changees.join(', ')}`); + else if (apres.empreinte !== avant.empreinte) lignes.push(`${cle}, ${nom} : plan changé, grandeurs égales`); + } + return lignes; +} diff --git a/test/fixtures/moteur/empreintes_defaut.json b/test/fixtures/moteur/empreintes_defaut.json new file mode 100644 index 0000000..14072cb --- /dev/null +++ b/test/fixtures/moteur/empreintes_defaut.json @@ -0,0 +1,721 @@ +{ + "reglages": { + "graines": [ + 1, + 2, + 3 + ], + "arret": 20000, + "historique": 1000, + "cases": [ + "separerAppartenances", + "nouveauxVoisins", + "nouvelleTable", + "varierAppartenances" + ], + "classement": { + "cles": [ + "petite-conflit", + "grande" + ], + "graine": 7, + "arret": 1000, + "nombre": 12 + }, + "bascule": { + "cles": [ + "petite", + "petite-conflit" + ], + "arret": 50000 + } + }, + "plans": { + "grande": { + "1": { + "empreinte": "2f637984985154ff3a8785a7d29092a634fdd4b3651c33448b877a1cb0077b16", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 3, + "excedentCollisions": 1, + "collisionsCumulees": 58, + "rencontresRepeteesChoisies": 74, + "retoursChoisis": 21, + "redondance": 1334 + } + }, + "2": { + "empreinte": "111a3077e5f0a381a81df2648c1aac5bb875a4ad84a8d95840210a0dcf905dba", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 3, + "excedentCollisions": 1, + "collisionsCumulees": 53, + "rencontresRepeteesChoisies": 49, + "retoursChoisis": 18, + "redondance": 1322 + } + }, + "3": { + "empreinte": "b6500109a02cc02b3a0ab121b57af6a15fd0a0e73e4460d65820aada73834ccd", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 3, + "excedentCollisions": 1, + "collisionsCumulees": 45, + "rencontresRepeteesChoisies": 30, + "retoursChoisis": 10, + "redondance": 1311 + } + } + }, + "grande-sans-exception": { + "1": { + "empreinte": "eb59055b0f65f2f5a4bb7cecb5b1c747d2ba3fc6fe8c77bcb17659b24cbedf8f", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 3, + "excedentCollisions": 2, + "collisionsCumulees": 54, + "rencontresRepeteesChoisies": 49, + "retoursChoisis": 19, + "redondance": 1314 + } + }, + "2": { + "empreinte": "c1908708b1b74643be66202beef64b689d8634aaf3db81ead0c275bc62b2bcd6", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 3, + "excedentCollisions": 0, + "collisionsCumulees": 43, + "rencontresRepeteesChoisies": 74, + "retoursChoisis": 27, + "redondance": 1246 + } + }, + "3": { + "empreinte": "c68933c83e2d0c53eb1798e108dd94f0bfd82e4dbf1a55309b576aad7dc585e0", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 3, + "excedentCollisions": 0, + "collisionsCumulees": 52, + "rencontresRepeteesChoisies": 74, + "retoursChoisis": 21, + "redondance": 1336 + } + } + }, + "petite": { + "1": { + "empreinte": "d13db767aaa2fc7ad59ad69973d663320931598b7d02a9212ca0f4e2949f8a21", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 10, + "redondance": 58 + } + }, + "2": { + "empreinte": "7de7cee8dcb6561c13b8fdd72ce8730f8022b9d2ff00ee7d11acc8a9a2c21558", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 14, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 17, + "redondance": 60 + } + }, + "3": { + "empreinte": "b531231af7422c46eaee40ece57222b9d15c57942dd822bda8651010c9fbec86", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 14, + "redondance": 61 + } + } + }, + "petite-conflit": { + "1": { + "empreinte": "eb8b6cfd59ab3c422c126ede84cf34319674bbf1ff1260963ff87d0dbf1134ad", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 11, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 14, + "redondance": 58 + } + }, + "2": { + "empreinte": "3d69f6c6701355b4945758c73721d222dadab5dff2af07f2f7b1fa8e3cee64bb", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 15, + "redondance": 61 + } + }, + "3": { + "empreinte": "d4daa8def32d39046daad3bfa7263ac52bcafc5d0b28eddf6efaf7359ae90f33", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 9, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 15, + "redondance": 59 + } + } + } + }, + "combinaisons": { + "separerAppartenances+nouveauxVoisins+nouvelleTable+varierAppartenances": { + "empreinte": "d13db767aaa2fc7ad59ad69973d663320931598b7d02a9212ca0f4e2949f8a21", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 10, + "redondance": 58 + } + }, + "separerAppartenances+nouveauxVoisins+nouvelleTable": { + "empreinte": "f76257b5ecf1260a0d7c81b948c5971ffb0c0c18ed6310979cfb7b2be6698dfb", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 11, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 15, + "redondance": 58 + } + }, + "separerAppartenances+nouveauxVoisins+varierAppartenances": { + "empreinte": "9b22b885a65791752a5928b7fe1b285355a4c336dec9cbd9e630aaa4f084ef04", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 7, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 15, + "redondance": 58 + } + }, + "separerAppartenances+nouveauxVoisins": { + "empreinte": "00885d606c84f32f15eef4731a9835a5f97a4754842f00e26f7f6fd73d6d3b4d", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 11, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 15, + "redondance": 60 + } + }, + "separerAppartenances+nouvelleTable+varierAppartenances": { + "empreinte": "cb80a1470adbb6203469abeafdf4f5fa03797242b0a80ca1665d73d5ab5ccc3b", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 11, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 16, + "redondance": 60 + } + }, + "separerAppartenances+nouvelleTable": { + "empreinte": "b97d6d1666c04024d0958b44e04667d0c68b205ef0a8a8473aafa61eaf688cbe", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 8, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 16, + "redondance": 58 + } + }, + "separerAppartenances+varierAppartenances": { + "empreinte": "1765e4f6977feac2298a40c8558b76cec322b2ddd3783db78b1fbe923aa1e88e", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 12, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 16, + "redondance": 60 + } + }, + "separerAppartenances": { + "empreinte": "caceccc8edeed9a9dcbd999308cb3a8b1bbca95475d9eddbbeb3563e4749ed40", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 15, + "redondance": 58 + } + }, + "nouveauxVoisins+nouvelleTable+varierAppartenances": { + "empreinte": "9e498d46ae56717fe5fee4cd33f06bc0c159116cfce761bac73969e85d668983", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 13, + "redondance": 58 + } + }, + "nouveauxVoisins+nouvelleTable": { + "empreinte": "ac14e9bd382bc0ac5878df97b67a3fec74b3891750b2aa3a69147882020a03cb", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 11, + "redondance": 58 + } + }, + "nouveauxVoisins+varierAppartenances": { + "empreinte": "2104c85460bc6751ec96249c8a65b5a5b09cb3028f6455d57145596b2ed893ef", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 15, + "redondance": 58 + } + }, + "nouveauxVoisins": { + "empreinte": "e19ecf733df91fdc2f6d1a25484e1b3625eed22e306b100b2f174b7147f5cc55", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 12, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 17, + "redondance": 58 + } + }, + "nouvelleTable+varierAppartenances": { + "empreinte": "f680569b067ea17677aae9659dea9977e528171dd28c788a6b9536da28316732", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 12, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 16, + "redondance": 59 + } + }, + "nouvelleTable": { + "empreinte": "9477b22afcc42ecf0017bc0dd5711916f6e546cd2a04d6598ac406c07ef57b27", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 14, + "redondance": 58 + } + }, + "varierAppartenances": { + "empreinte": "c3df5155baa7cbbb7d0b980c9fbd071247e2934ce19732137291b6bad66fcdcc", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 15, + "redondance": 60 + } + }, + "aucune": { + "empreinte": "9fd1147b184369d7ba5ffe04c81f3f721d6967fcecd7fc64a623006e76fbf0f9", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 1, + "retoursChoisis": 17, + "redondance": 58 + } + } + }, + "bascule": { + "petite": { + "1": { + "empreinte": "a028aa693dd46b17070762d3cac2a2713eb0ee9dfc7bf021b8fc234a27bcf671", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 12, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 15, + "redondance": 60 + } + }, + "2": { + "empreinte": "e72ba70986cbf2a15231d9defe4bcdc1490db6633552e97d4799d4221232da78", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 0, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 11, + "redondance": 72 + } + }, + "3": { + "empreinte": "b531231af7422c46eaee40ece57222b9d15c57942dd822bda8651010c9fbec86", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 14, + "redondance": 61 + } + } + }, + "petite-conflit": { + "1": { + "empreinte": "991a8d86d7ae00d3be99bb19990642efc2025eac51867b7def97102d998fc6b2", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 12, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 15, + "redondance": 60 + } + }, + "2": { + "empreinte": "3d69f6c6701355b4945758c73721d222dadab5dff2af07f2f7b1fa8e3cee64bb", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 15, + "redondance": 61 + } + }, + "3": { + "empreinte": "48aafbee06bc5fd1567b44b2d3734df55f91cd1e0d0a915f333f53be8bf92f90", + "grandeurs": { + "ecartAuPlafondAPrioriMax": 0, + "excedentCollisions": 0, + "collisionsCumulees": 14, + "rencontresRepeteesChoisies": 0, + "retoursChoisis": 14, + "redondance": 61 + } + } + } + }, + "classements": { + "petite-conflit": { + "connu": { + "ordre": [ + 7, + 10, + 2, + 1, + 12, + 8, + 4, + 9, + 5, + 11, + 6, + 3 + ], + "criteresAppliques": [ + "ecartAuPlafondAPrioriMax", + "excedentCollisions", + "collisionsCumulees", + "rencontresRepetees", + "redondance" + ], + "critereSaute": null + }, + "inconnu": { + "ordre": [ + 1, + 12, + 7, + 8, + 10, + 4, + 2, + 9, + 5, + 11, + 6, + 3 + ], + "criteresAppliques": [ + "excedentCollisions", + "collisionsCumulees", + "rencontresRepetees", + "redondance" + ], + "critereSaute": "ecartAuPlafondAPrioriMax" + }, + "propositions": { + "1": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 0, + "collisionsCumulees": 11, + "rencontresRepeteesChoisies": 4, + "retoursChoisis": 16, + "redondance": 53 + }, + "2": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 1, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 5, + "retoursChoisis": 12, + "redondance": 53 + }, + "3": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 2, + "collisionsCumulees": 16, + "rencontresRepeteesChoisies": 6, + "retoursChoisis": 14, + "redondance": 49 + }, + "4": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 1, + "collisionsCumulees": 10, + "rencontresRepeteesChoisies": 3, + "retoursChoisis": 16, + "redondance": 54 + }, + "5": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 2, + "collisionsCumulees": 15, + "rencontresRepeteesChoisies": 4, + "retoursChoisis": 17, + "redondance": 52 + }, + "6": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 2, + "collisionsCumulees": 15, + "rencontresRepeteesChoisies": 5, + "retoursChoisis": 15, + "redondance": 52 + }, + "7": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 12, + "rencontresRepeteesChoisies": 3, + "retoursChoisis": 17, + "redondance": 55 + }, + "8": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 0, + "collisionsCumulees": 13, + "rencontresRepeteesChoisies": 5, + "retoursChoisis": 16, + "redondance": 50 + }, + "9": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 2, + "collisionsCumulees": 14, + "rencontresRepeteesChoisies": 6, + "retoursChoisis": 15, + "redondance": 49 + }, + "10": { + "ecartAuPlafondAPrioriMax": 1, + "excedentCollisions": 0, + "collisionsCumulees": 15, + "rencontresRepeteesChoisies": 3, + "retoursChoisis": 18, + "redondance": 54 + }, + "11": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 2, + "collisionsCumulees": 15, + "rencontresRepeteesChoisies": 5, + "retoursChoisis": 15, + "redondance": 51 + }, + "12": { + "ecartAuPlafondAPrioriMax": 2, + "excedentCollisions": 0, + "collisionsCumulees": 11, + "rencontresRepeteesChoisies": 5, + "retoursChoisis": 15, + "redondance": 50 + } + } + }, + "grande": { + "connu": { + "ordre": [ + 10, + 9, + 8, + 1, + 5, + 12, + 3, + 2, + 7, + 6, + 4, + 11 + ], + "criteresAppliques": [ + "ecartAuPlafondAPrioriMax", + "excedentCollisions", + "collisionsCumulees", + "rencontresRepetees", + "redondance" + ], + "critereSaute": null + }, + "inconnu": { + "ordre": [ + 10, + 9, + 11, + 8, + 1, + 5, + 12, + 3, + 2, + 7, + 6, + 4 + ], + "criteresAppliques": [ + "excedentCollisions", + "collisionsCumulees", + "rencontresRepetees", + "redondance" + ], + "critereSaute": "ecartAuPlafondAPrioriMax" + }, + "propositions": { + "1": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 2, + "collisionsCumulees": 58, + "rencontresRepeteesChoisies": 128, + "retoursChoisis": 42, + "redondance": 1312 + }, + "2": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 3, + "collisionsCumulees": 70, + "rencontresRepeteesChoisies": 142, + "retoursChoisis": 32, + "redondance": 1326 + }, + "3": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 2, + "collisionsCumulees": 68, + "rencontresRepeteesChoisies": 112, + "retoursChoisis": 36, + "redondance": 1308 + }, + "4": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 4, + "collisionsCumulees": 75, + "rencontresRepeteesChoisies": 129, + "retoursChoisis": 32, + "redondance": 1314 + }, + "5": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 2, + "collisionsCumulees": 64, + "rencontresRepeteesChoisies": 124, + "retoursChoisis": 34, + "redondance": 1298 + }, + "6": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 4, + "collisionsCumulees": 69, + "rencontresRepeteesChoisies": 130, + "retoursChoisis": 38, + "redondance": 1282 + }, + "7": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 3, + "collisionsCumulees": 72, + "rencontresRepeteesChoisies": 116, + "retoursChoisis": 31, + "redondance": 1347 + }, + "8": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 2, + "collisionsCumulees": 57, + "rencontresRepeteesChoisies": 120, + "retoursChoisis": 39, + "redondance": 1305 + }, + "9": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 1, + "collisionsCumulees": 62, + "rencontresRepeteesChoisies": 120, + "retoursChoisis": 34, + "redondance": 1291 + }, + "10": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 1, + "collisionsCumulees": 57, + "rencontresRepeteesChoisies": 123, + "retoursChoisis": 38, + "redondance": 1287 + }, + "11": { + "ecartAuPlafondAPrioriMax": 5, + "excedentCollisions": 1, + "collisionsCumulees": 71, + "rencontresRepeteesChoisies": 119, + "retoursChoisis": 30, + "redondance": 1339 + }, + "12": { + "ecartAuPlafondAPrioriMax": 4, + "excedentCollisions": 2, + "collisionsCumulees": 67, + "rencontresRepeteesChoisies": 117, + "retoursChoisis": 44, + "redondance": 1301 + } + } + } + } +}