The coming configurable order must leave every default plan and ranking unchanged at equal seed. Fingerprints of the plans of the four shipped demos, of the acceptance switch, and of a twelve-proposal ranking where each criterion is the first to split some neighbouring pair are written once on the previous revision and verified by every run; rewriting them needs GTT_EMPREINTES=ecrire, any other value is refused by name. Checked: 20 node tests, 3 long; named mutations of the order caught. --- FR --- [ADD] épreuves du moteur : empreintes des plans et du classement actuels L'ordre réglable à venir doit laisser chaque plan et chaque classement par défaut inchangés à graine égale. Les empreintes des plans des quatre démonstrations, de la bascule d'acceptation et d'un classement de douze propositions où chaque critère départage une paire voisine sont écrites une fois sur la révision précédente et vérifiées à chaque exécution ; les récrire exige GTT_EMPREINTES=ecrire, toute autre valeur est refusée. Vérifié : 20 épreuves node, 3 longues ; mutations nommées de l'ordre prises. Assisted-by: Claude Opus 5.5
137 lines
6.4 KiB
JavaScript
137 lines
6.4 KiB
JavaScript
// © 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, []);
|
||
});
|
||
});
|