[IMP] proposal evaluation: cached per frozen charge, validated once
The Génération and Propositions tabs re-evaluated every proposal on each visit, about 1 ms per proposal, with no bound on how many accumulate. The evaluation table is now kept per frozen event charge, which every write replaces, and both tabs share it. Each placement is validated once instead of three times; the public derive and planDepuisFichier still validate, and a guard keeps the unchecked variants to evaluer alone. At 200 proposals: Génération 214 to 17 ms on revisit, Propositions 310 to 118 ms. Checked: results deep-equal; node 4019, browser 873. --- FR --- [IMP] évaluation des propositions : gardée par charge, validée une fois Les onglets Génération et Propositions réévaluaient toutes les propositions à chaque visite, environ 1 ms chacune, sans borne sur leur nombre. La table d'évaluation se garde par charge figée, qu'une écriture remplace, et les deux onglets la partagent. Chaque placement se valide une fois au lieu de trois ; derive et planDepuisFichier publics valident toujours, une garde réserve les variantes sans contrôle à evaluer. À 200 propositions : Génération 214 → 17 ms au retour, Propositions 310 → 118 ms. Vérifié : résultats identiques ; node 4019, nav. 873. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
10f2c8039f
commit
156851e043
9 changed files with 447 additions and 20 deletions
5
spec.md
5
spec.md
|
|
@ -672,7 +672,8 @@ sur six paires passe devant un plan qui en concentre quatre sur une seule.
|
|||
affichée, et se retrouvent de toute façon par soustraction. Trois clés pour deux
|
||||
quantités indépendantes feraient un départage qui ne départage rien.
|
||||
|
||||
**Le plafond a priori par personne se calcule à chaque évaluation** : sa
|
||||
**Le plafond a priori par personne se calcule à chaque évaluation** d'une
|
||||
charge nouvelle — une même charge figée relit sa table — : sa
|
||||
maximisation est mesurée et ne coûte presque rien (§ 17, point 4), et le premier
|
||||
critère s'applique toujours. Le chemin où il manque reste celui du moteur, que
|
||||
l'application n'emprunte pas : le premier critère y est **sauté** et la page
|
||||
|
|
@ -3356,7 +3357,7 @@ des fonctions pures du module de géométrie.
|
|||
| critère | ce que Svelte apporte |
|
||||
|---|---|
|
||||
| tenir hors ligne | se compile en JavaScript ordinaire ; Vite est une dépendance de développement qui ne survit pas à la construction |
|
||||
| rester rapide | compile des mises à jour fines, sans arbre virtuel ni mémoïsation à poser à la main |
|
||||
| rester rapide | compile des mises à jour fines, sans arbre virtuel ni mémoïsation à poser à la main — à une exception près : la table d'évaluation des propositions, gardée dans le modèle pur sous l'identité de la charge figée de la séance, qu'une écriture remplace |
|
||||
| se tester sans navigateur | n'impose d'écrire aucun calcul dans un composant ; le composant ne contient que le câblage |
|
||||
| être joli sans y passer des semaines | un fichier de jetons — couleurs, rayons, ombres, échelle typographique, cible principale, largeur de page — décliné en deux thèmes ; ses familles de couleur : la page, le bandeau, la carte, l'action principale, les quatre états du plan, le neutre, les appartenances, la bande de mode, les messages (avis, refus, information), le plan, le papier |
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@
|
|||
// faute (fautes, S2) et sans dérive (derive, S2). Fautif ou en dérive, il
|
||||
// n'est ni mesuré ni classé, et ses raisons disent pourquoi — la partition
|
||||
// se fait avant classer, qui laisse passer deux populations de même taille.
|
||||
// derive et planDepuisFichier, qui lèvent sur une faute, ne reçoivent que
|
||||
// des placements sans faute.
|
||||
// evaluer valide chaque placement une fois, par fautes, puis lit le placement
|
||||
// sans faute par deriveSansRevalider et planSansRevalider, qui ne refont pas
|
||||
// cette validation : un placement fautif ne les atteint jamais, et c'est ce
|
||||
// seul appel à fautes qui l'arrête. resumeDuRetenu passe par derive et
|
||||
// planDepuisFichier, qui la refont et lèvent sur une faute.
|
||||
//
|
||||
// Les imposés viennent du diagnostic, les mêmes pour toute ligne ; les
|
||||
// rencontres répétées imposées dépendent du plan, et chaque ligne les lit
|
||||
|
|
@ -34,7 +37,7 @@ import { plafondsRealises } from '../moteur/plafond.js';
|
|||
import { verifierIndicateurs, verifierInvariants } from '../moteur/verification.js';
|
||||
import { ErreurConfiguration } from '../moteur/erreurs.js';
|
||||
import { CIBLE_REDONDANCE_PAR_DEFAUT, cibleRedondance, configurationDepuisCharge } from '../stockage/document.js';
|
||||
import { derive, fautes, planDepuisFichier } from '../stockage/placements.js';
|
||||
import { derive, deriveSansRevalider, fautes, planDepuisFichier, planSansRevalider } from '../stockage/placements.js';
|
||||
|
||||
export { CIBLE_REDONDANCE_PAR_DEFAUT, cibleRedondance };
|
||||
|
||||
|
|
@ -94,7 +97,9 @@ const nonMesuree = (id, statut, raisons) => ({
|
|||
|
||||
/**
|
||||
* La table d'évaluation d'une charge (contrat de A6). Ne modifie pas la
|
||||
* charge.
|
||||
* charge. Chaque proposition et le retenu se valident une seule fois, par
|
||||
* fautes : fautif, le placement s'arrête là ; sans faute, sa dérive et son
|
||||
* plan se lisent sans seconde validation.
|
||||
*
|
||||
* @param {import('../stockage/types.js').Charge} charge
|
||||
* @param {{plafondsAPriori?: 'calcul'|null}} [options]
|
||||
|
|
@ -118,9 +123,9 @@ export function evaluer(charge, { plafondsAPriori = 'calcul' } = {}) {
|
|||
function ligneDe(placement, id) {
|
||||
const sesFautes = fautes(placement);
|
||||
if (sesFautes.length > 0) return nonMesuree(id, 'fautif', sesFautes);
|
||||
const raisons = derive(placement, charge);
|
||||
const raisons = deriveSansRevalider(placement, charge);
|
||||
if (raisons.length > 0) return nonMesuree(id, 'derive', raisons);
|
||||
const plan = planDepuisFichier(placement);
|
||||
const plan = planSansRevalider(placement);
|
||||
const tableDe = indexerPlan(instance, plan);
|
||||
const mesures = mesurer(instance, tableDe);
|
||||
const realises = plafondsRealises(instance, tableDe);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { ecartsAuPlafondAPriori } from '../moteur/manque.js';
|
|||
import { plafondsAPriori } from '../moteur/plafond.js';
|
||||
import { rechercher } from '../moteur/recherche.js';
|
||||
import { analyser, capacite, configurationDepuisCharge } from '../stockage/document.js';
|
||||
import { planDepuisFichier } from '../stockage/placements.js';
|
||||
import { fautes, planDepuisFichier } from '../stockage/placements.js';
|
||||
import { VERSION } from '../version.genere.js';
|
||||
import { appliquerCommande } from './commandes.js';
|
||||
import {
|
||||
|
|
@ -293,6 +293,80 @@ describe('évaluation : un placement fautif (§ 8.9, point 3)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Le placement donné, sous un mandataire qui compte les lectures de
|
||||
// siegesAttribues : seule la validation (fautes) lit cette clé.
|
||||
function compte(placement) {
|
||||
const releve = { lectures: 0 };
|
||||
const mandataire = new Proxy(placement, {
|
||||
get(cible, cle, recepteur) {
|
||||
if (cle === 'siegesAttribues') releve.lectures += 1;
|
||||
return Reflect.get(cible, cle, recepteur);
|
||||
},
|
||||
});
|
||||
return { mandataire, releve };
|
||||
}
|
||||
|
||||
describe('évaluation : chaque placement se valide une fois (§ 8.9)', () => {
|
||||
test("une proposition fautive est rendue fautive, ses fautes pour raisons, non mesurée ; les autres se classent comme sans elle", () => {
|
||||
const base = generee(chargeDemo('petite'), { graine: 3, nombre: 3, arret: 300 });
|
||||
const charge = structuredClone(base);
|
||||
const fautive = charge.propositions[1];
|
||||
const [premiere, seconde] = fautive.placement[0].sieges;
|
||||
premiere.push(seconde.shift());
|
||||
const sansElle = structuredClone(base);
|
||||
sansElle.propositions.splice(1, 1);
|
||||
const table = evaluer(figer(charge));
|
||||
const attendue = evaluer(figer(sansElle));
|
||||
const ligne = table.propositions.find(({ id }) => id === fautive.id);
|
||||
assert.equal(ligne.statut, 'fautif');
|
||||
assert.ok(ligne.raisons.length > 0);
|
||||
assert.deepEqual(ligne.raisons, fautes(fautive));
|
||||
assert.equal(ligne.mesures, null);
|
||||
assert.equal(table.propositions.at(-1), ligne);
|
||||
assert.deepEqual(table.propositions.slice(0, -1), attendue.propositions);
|
||||
assert.deepEqual(table.classement, attendue.classement);
|
||||
});
|
||||
|
||||
test('un placement à la fois fautif et en dérive est rendu fautif, ses fautes pour raisons : la validation précède la dérive', () => {
|
||||
const base = generee(chargeDemo('petite'), { graine: 3, nombre: 3, arret: 300 });
|
||||
const retenue = executer(base, 'retenirProposition', { id: base.propositions[0].id });
|
||||
const charge = executer(retenue, 'exclureParticipant', { id: retenue.participants[0].id });
|
||||
const proposition = charge.propositions[1];
|
||||
for (const placement of [proposition, charge.retenu]) {
|
||||
const [a, b] = placement.placement[0].sieges;
|
||||
a.push(b.shift());
|
||||
}
|
||||
assert.ok(fautes(proposition).length > 0);
|
||||
assert.ok(fautes(charge.retenu).length > 0);
|
||||
const table = evaluer(figer(charge));
|
||||
const ligne = table.propositions.find(({ id }) => id === proposition.id);
|
||||
assert.equal(ligne.statut, 'fautif');
|
||||
assert.deepEqual(ligne.raisons, fautes(proposition));
|
||||
assert.equal(table.retenu.statut, 'fautif');
|
||||
assert.deepEqual(table.retenu.raisons, fautes(charge.retenu));
|
||||
assert.ok(table.propositions.filter(({ id }) => id !== proposition.id).every(({ statut }) => statut === 'derive'));
|
||||
});
|
||||
|
||||
test('evaluer valide chaque proposition et le retenu une seule fois : la validation y lit le placement autant que fautes seul', () => {
|
||||
const base = generee(chargeDemo('petite'), { graine: 3, nombre: 2, arret: 300 });
|
||||
const charge = executer(base, 'retenirProposition', { id: base.propositions[0].id });
|
||||
const seule = compte(structuredClone(charge.propositions[1]));
|
||||
fautes(seule.mandataire);
|
||||
assert.ok(seule.releve.lectures > 0);
|
||||
const proposition = compte(charge.propositions[1]);
|
||||
const retenu = compte(charge.retenu);
|
||||
charge.propositions[1] = proposition.mandataire;
|
||||
charge.retenu = retenu.mandataire;
|
||||
const table = evaluer(charge);
|
||||
assert.ok(table.propositions.every(({ statut }) => statut === 'comparable'));
|
||||
assert.equal(table.retenu.statut, 'comparable');
|
||||
assert.equal(proposition.releve.lectures, seule.releve.lectures);
|
||||
const seulRetenu = compte(structuredClone(executer(base, 'retenirProposition', { id: base.propositions[0].id }).retenu));
|
||||
fautes(seulRetenu.mandataire);
|
||||
assert.equal(retenu.releve.lectures, seulRetenu.releve.lectures);
|
||||
});
|
||||
});
|
||||
|
||||
describe('évaluation : le plafond a priori inconnu (§ 17, point 4)', () => {
|
||||
test("plafondsAPriori null : le premier critère est sauté et nommé ; écart d'itinéraire et certificat inconnus", () => {
|
||||
const charge = figer(generee(chargeDemo('petite-conflit'), { graine: 5, nombre: 2, arret: 300 }));
|
||||
|
|
|
|||
|
|
@ -2181,6 +2181,24 @@ describe('séance observable : etat() et subscribe (§ 8.4)', () => {
|
|||
compterFiges(seance.etat());
|
||||
});
|
||||
|
||||
test("la charge de l'état garde son identité tant que l'événement ne change pas, et chaque geste en rend une neuve, figée", async () => {
|
||||
const { seance } = await ouvertSur();
|
||||
await seance.passerEnEcriture();
|
||||
const etat = seance.etat();
|
||||
const { charge } = etat;
|
||||
seance.debuterCalcul();
|
||||
seance.finirCalcul();
|
||||
assert.notEqual(seance.etat(), etat);
|
||||
assert.equal(seance.etat().charge, charge);
|
||||
await seance.executer('ajouterParticipant', { champs: { nom: 'Ancolie' } });
|
||||
const apres = seance.etat().charge;
|
||||
assert.notEqual(apres, charge);
|
||||
compterFiges(apres);
|
||||
await seance.executer('exclureParticipant', { id: 3 });
|
||||
assert.notEqual(seance.etat().charge, apres);
|
||||
compterFiges(seance.etat().charge);
|
||||
});
|
||||
|
||||
test("l'état porte l'événement, la charge, l'examen, les retours, le calcul en cours ; sans événement, rien", async () => {
|
||||
const vide = monter().seance.etat();
|
||||
assert.deepEqual(
|
||||
|
|
|
|||
125
src/interface/generation/Evaluation.navigateur.test.js
Normal file
125
src/interface/generation/Evaluation.navigateur.test.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// L'évaluation que partagent Génération et Propositions, dans un vrai
|
||||
// moteur de rendu (§ 5.7, § 13.3) : tant que la charge de la séance ne
|
||||
// change pas, revenir à l'une ou l'autre section ne mesure rien de nouveau ;
|
||||
// une charge écrite se mesure une fois, et la meilleure proposition annoncée
|
||||
// la suit.
|
||||
//
|
||||
// Le module de l'évaluation est remplacé par ses propres fonctions,
|
||||
// espionnées (vi.mock, spy) : evaluer garde son effet, et ses appels se
|
||||
// comptent. Chaque épreuve monte App par monterApp, sur le système de
|
||||
// fichiers simulé, avec l'exécuteur direct ; le rendu se lit par les
|
||||
// attributs data-… ; les noms d'épreuve sont inventés.
|
||||
//
|
||||
// Le projet navigateur emploie expect, vi, et test et describe de Vitest
|
||||
// (§ 14.8).
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { flushSync } from 'svelte';
|
||||
import { evaluer, premiereDuClassement } from '../../application/evaluation.js';
|
||||
import { creerFormat } from '../../application/format.js';
|
||||
import { creerExecuteurDirect } from '../../application/generation.js';
|
||||
import { TABLES_APPLICATION } from '../../application/libelles.js';
|
||||
import { creerTraduction } from '../../application/traduction.js';
|
||||
import { rechercher } from '../../moteur/recherche.js';
|
||||
import { configurationDepuisCharge } from '../../stockage/document.js';
|
||||
import { VERSION } from '../../version.genere.js';
|
||||
import { fixerCadre } from '../../../test/navigateur/entrees.js';
|
||||
import { monterApp } from '../../../test/navigateur/montage.js';
|
||||
import { TABLES_INTERFACE } from '../libelles.js';
|
||||
import { meilleureProposition } from './meilleure.js';
|
||||
|
||||
vi.mock('../../application/evaluation.js', { spy: true });
|
||||
|
||||
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
||||
const format = creerFormat(t);
|
||||
|
||||
// Les montages de l'épreuve, démontés après elle ; le collecteur d'erreurs
|
||||
// n'a rien reçu pendant l'épreuve.
|
||||
let montes = [];
|
||||
async function monter(options) {
|
||||
const m = await monterApp(options);
|
||||
montes.push(m);
|
||||
return m;
|
||||
}
|
||||
beforeEach(async () => {
|
||||
await fixerCadre(1366, 700);
|
||||
});
|
||||
afterEach(() => {
|
||||
const erreurs = montes.flatMap((m) => m.erreurs());
|
||||
for (const m of montes.reverse()) m.demonter();
|
||||
montes = [];
|
||||
window.scrollTo(0, 0);
|
||||
expect(erreurs, 'erreurs reçues par le collecteur').toEqual([]);
|
||||
});
|
||||
|
||||
const dans = (m, selecteur) => m.racine.querySelector(selecteur);
|
||||
const charge = (m) => m.seance.etat().charge;
|
||||
const identifiant = (m) => dans(m, '[data-section="generation"] [data-meilleure] [data-meilleure-id]');
|
||||
const lignes = (m) => m.racine.querySelectorAll('[data-section="propositions"] tbody tr[data-proposition]');
|
||||
const appels = () => vi.mocked(evaluer).mock.calls.length;
|
||||
|
||||
// Ouvre la section nommée, son rendu à jour.
|
||||
function aller(m, section) {
|
||||
m.ui.allerA(section);
|
||||
flushSync();
|
||||
}
|
||||
|
||||
// Une génération du moteur de plus, enregistrée par la séance.
|
||||
async function enregistrer(m, reglages) {
|
||||
const configuration = configurationDepuisCharge(charge(m));
|
||||
const propositions = rechercher(configuration, { historique: 1000, ...reglages });
|
||||
await m.seance.executer('enregistrerGeneration', { propositions, configuration, produitVersion: VERSION.affichee });
|
||||
flushSync();
|
||||
}
|
||||
|
||||
// L'identifiant au rang 1 d'une table neuve de la charge courante, tel que
|
||||
// le bloc l'écrit.
|
||||
const rangUnAttendu = (m) => format.nombre(premiereDuClassement(evaluer(charge(m))).id);
|
||||
|
||||
describe('Génération et Propositions relisent une même évaluation (§ 5.7, § 13.3)', () => {
|
||||
test('revenir à Génération, puis à Propositions, ne mesure rien de nouveau ; une écriture mesure une fois, et l’annonce suit la charge écrite', async () => {
|
||||
const m = await monter({ executeur: creerExecuteurDirect() });
|
||||
await m.facade.chargerDemonstration('petite-conflit');
|
||||
await m.seance.passerEnEcriture();
|
||||
await enregistrer(m, { graine: 5, nombre: 3, arret: 300 });
|
||||
aller(m, 'participants');
|
||||
vi.mocked(evaluer).mockClear();
|
||||
|
||||
aller(m, 'generation');
|
||||
expect(appels(), '1re visite de Génération').toBe(1);
|
||||
const premiere = identifiant(m).textContent;
|
||||
aller(m, 'participants');
|
||||
aller(m, 'generation');
|
||||
expect(appels(), '2e visite de Génération').toBe(1);
|
||||
aller(m, 'propositions');
|
||||
expect(lignes(m).length).toBe(3);
|
||||
expect(appels(), 'Propositions après Génération').toBe(1);
|
||||
aller(m, 'generation');
|
||||
expect(appels(), 'Génération après Propositions').toBe(1);
|
||||
expect(identifiant(m).textContent).toBe(premiere);
|
||||
// Le modèle relit la même table que les deux sections.
|
||||
expect(format.nombre(meilleureProposition(charge(m)).id)).toBe(premiere);
|
||||
expect(appels(), 'le modèle après les deux sections').toBe(1);
|
||||
|
||||
// Une écriture qui remplace les propositions : la charge écrite se
|
||||
// mesure une fois, et l'annonce lit son rang 1, que l'ancienne charge
|
||||
// ne portait pas — un identifiant ne se réattribue jamais.
|
||||
const anciens = charge(m).propositions.map(({ id }) => id);
|
||||
const avantEcriture = appels();
|
||||
await m.seance.executer('effacerPropositions', {});
|
||||
await enregistrer(m, { graine: 9, nombre: 2, arret: 300 });
|
||||
expect(appels(), 'la charge écrite, mesurée une fois').toBe(avantEcriture + 1);
|
||||
const attendu = rangUnAttendu(m);
|
||||
const apres = appels();
|
||||
expect(identifiant(m).textContent).toBe(attendu);
|
||||
expect(anciens.map((id) => format.nombre(id))).not.toContain(attendu);
|
||||
aller(m, 'participants');
|
||||
aller(m, 'generation');
|
||||
expect(identifiant(m).textContent).toBe(attendu);
|
||||
aller(m, 'propositions');
|
||||
expect(lignes(m).length).toBe(2);
|
||||
expect(appels(), 'les deux sections sur la charge écrite').toBe(apres);
|
||||
});
|
||||
});
|
||||
|
|
@ -397,16 +397,36 @@ export function demandeConfirmation({ refus, charge }, id) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* La table d'évaluation d'une charge, ou ce qui l'empêche : null sans
|
||||
* proposition ni retenu, rien n'étant à mesurer ; le refus de normaliser —
|
||||
* une erreur qui porte un code — quand la configuration ne se tient pas.
|
||||
* Toute autre erreur remonte.
|
||||
*
|
||||
* @param {Object} charge
|
||||
* @returns {{table: Object|null, refus: null|{code: string, details: Object}}}
|
||||
*/
|
||||
export function evaluation(charge) {
|
||||
// La table d'évaluation de chaque charge figée en profondeur, sous l'identité
|
||||
// de la charge. La charge de l'état observable de la séance est une copie
|
||||
// figée en profondeur (seance.js, figer), remplacée par une copie neuve à
|
||||
// chaque changement de l'événement ouvert et gardée telle quelle sinon : son
|
||||
// identité suffit à dire que son contenu n'a pas changé, et Génération et
|
||||
// Propositions relisent la même table. Une charge qui n'est pas figée
|
||||
// partout se mesure à chaque appel, son contenu pouvant changer sous la
|
||||
// même identité. Le § 13.3 compte sur la réactivité de Svelte plutôt que sur
|
||||
// une mémoïsation posée à la main ; ce cache en est l'exception voulue, posé
|
||||
// dans le modèle pur et tenu par l'immuabilité de son entrée.
|
||||
const evaluees = new WeakMap();
|
||||
|
||||
// Vrai quand chaque objet atteint depuis valeur est figé. Un objet figé ne
|
||||
// redevient jamais modifiable : vrai une fois, vrai pour toujours.
|
||||
function figeePartout(valeur) {
|
||||
if (valeur === null || typeof valeur !== 'object') return true;
|
||||
if (!Object.isFrozen(valeur)) return false;
|
||||
for (const cle of Object.keys(valeur)) if (!figeePartout(valeur[cle])) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fige une valeur neuve et tout ce qu'elle atteint.
|
||||
function figer(valeur) {
|
||||
if (valeur === null || typeof valeur !== 'object' || Object.isFrozen(valeur)) return valeur;
|
||||
for (const cle of Object.keys(valeur)) figer(valeur[cle]);
|
||||
return Object.freeze(valeur);
|
||||
}
|
||||
|
||||
// La table d'une charge, mesurée à neuf.
|
||||
function evaluationNeuve(charge) {
|
||||
if (charge.propositions.length === 0 && charge.retenu === null) return { table: null, refus: null };
|
||||
try {
|
||||
return { table: evaluer(charge), refus: null };
|
||||
|
|
@ -416,6 +436,25 @@ export function evaluation(charge) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* La table d'évaluation d'une charge, ou ce qui l'empêche : null sans
|
||||
* proposition ni retenu, rien n'étant à mesurer ; le refus de normaliser —
|
||||
* une erreur qui porte un code — quand la configuration ne se tient pas.
|
||||
* Toute autre erreur remonte, et rien n'est retenu. Le résultat est figé en
|
||||
* profondeur ; pour une charge figée partout, c'est le même objet à chaque
|
||||
* appel, mesuré au premier.
|
||||
*
|
||||
* @param {Object} charge
|
||||
* @returns {{table: Object|null, refus: null|{code: string, details: Object}}}
|
||||
*/
|
||||
export function evaluation(charge) {
|
||||
const connue = evaluees.get(charge);
|
||||
if (connue !== undefined) return connue;
|
||||
const evaluee = figer(evaluationNeuve(charge));
|
||||
if (figeePartout(charge)) evaluees.set(charge, evaluee);
|
||||
return evaluee;
|
||||
}
|
||||
|
||||
/** Le nom affiché de la personne id de la charge, exclue comprise ; null
|
||||
* pour une personne que la charge n'a plus, et pour id null. */
|
||||
export function nomDe(charge, id) {
|
||||
|
|
|
|||
|
|
@ -669,6 +669,77 @@ describe('evaluation : la table d’une charge, ou ce qui l’empêche', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Fige en profondeur, comme la séance fige la charge de son état
|
||||
// observable : chaque objet atteint, puis l'objet lui-même.
|
||||
function figer(valeur) {
|
||||
if (valeur !== null && typeof valeur === 'object' && !Object.isFrozen(valeur)) {
|
||||
for (const element of Object.values(valeur)) figer(element);
|
||||
Object.freeze(valeur);
|
||||
}
|
||||
return valeur;
|
||||
}
|
||||
|
||||
// Vrai quand chaque objet atteint depuis valeur est figé.
|
||||
const figeePartout = (valeur) =>
|
||||
valeur === null || typeof valeur !== 'object' || (Object.isFrozen(valeur) && Object.values(valeur).every(figeePartout));
|
||||
|
||||
describe('evaluation : une table par charge figée, relue tant que la charge ne change pas (§ 13.3)', () => {
|
||||
test('une même charge figée rend deux fois la même table, figée en profondeur et égale à celle d’evaluer', () => {
|
||||
const charge = figer(generee(chargeDemo('petite'), { graine: 1, nombre: 3, arret: 100 }));
|
||||
const premiere = evaluation(charge);
|
||||
assert.equal(evaluation(charge), premiere);
|
||||
assert.deepEqual(premiere, { table: evaluer(charge), refus: null });
|
||||
assert.ok(figeePartout(premiere));
|
||||
const refusee = structuredClone(charge);
|
||||
refusee.reservations = [1, 2, 3, 4].map((participant) => ({ participant, table: 1, siege: null, portee: 'tous', tour: null }));
|
||||
figer(refusee);
|
||||
assert.equal(evaluation(refusee), evaluation(refusee));
|
||||
assert.equal(evaluation(refusee).refus.code, 'SURRESERVATION');
|
||||
});
|
||||
|
||||
test('chaque écriture rend une charge neuve, dont la table se mesure à neuf ; la charge d’avant garde la sienne', () => {
|
||||
const avant = figer(generee(chargeDemo('petite'), { graine: 1, nombre: 3, arret: 100 }));
|
||||
const deAvant = evaluation(avant);
|
||||
const exclue = figer(executer(avant, 'exclureParticipant', { id: avant.participants[0].id }));
|
||||
assert.deepEqual(evaluation(exclue), { table: evaluer(exclue), refus: null });
|
||||
assert.ok(evaluation(exclue).table.propositions.every(({ statut }) => statut === 'derive'));
|
||||
const plus = figer(generee(avant, { graine: 2, nombre: 2, arret: 100 }));
|
||||
assert.deepEqual(evaluation(plus), { table: evaluer(plus), refus: null });
|
||||
assert.equal(evaluation(plus).table.propositions.length, 5);
|
||||
const retenue = figer(executer(plus, 'retenirProposition', { id: plus.propositions[4].id }));
|
||||
assert.deepEqual(evaluation(retenue), { table: evaluer(retenue), refus: null });
|
||||
assert.equal(evaluation(retenue).table.retenu.id, plus.propositions[4].id);
|
||||
const effacee = figer(executer(retenue, 'effacerPropositions', {}));
|
||||
assert.deepEqual(evaluation(effacee), { table: evaluer(effacee), refus: null });
|
||||
assert.deepEqual(evaluation(effacee).table.propositions.map(({ id }) => id), [plus.propositions[4].id]);
|
||||
assert.equal(evaluation(avant), deAvant);
|
||||
});
|
||||
|
||||
test('une charge neuve qui reprend les objets figés de la précédente — propositions comprises — se mesure à neuf', () => {
|
||||
const avant = figer(generee(chargeDemo('petite'), { graine: 1, nombre: 3, arret: 100 }));
|
||||
const deAvant = evaluation(avant);
|
||||
const [premiere, ...autres] = avant.participants;
|
||||
const partagee = figer({ ...avant, participants: [{ ...premiere, exclu: true }, ...autres] });
|
||||
assert.equal(partagee.propositions, avant.propositions);
|
||||
assert.equal(partagee.tables, avant.tables);
|
||||
assert.deepEqual(evaluation(partagee), { table: evaluer(partagee), refus: null });
|
||||
assert.ok(evaluation(partagee).table.propositions.every(({ statut }) => statut === 'derive'));
|
||||
assert.equal(evaluation(avant), deAvant);
|
||||
});
|
||||
|
||||
test('une charge qui n’est pas figée partout se mesure à chaque appel : modifiée en place, sa table suit', () => {
|
||||
const charge = generee(chargeDemo('petite'), { graine: 1, nombre: 3, arret: 100 });
|
||||
assert.notEqual(evaluation(charge), evaluation(charge));
|
||||
Object.freeze(charge);
|
||||
const premiere = evaluation(charge);
|
||||
charge.propositions.pop();
|
||||
charge.participants[0].exclu = true;
|
||||
assert.notDeepEqual(evaluation(charge), premiere);
|
||||
assert.deepEqual(evaluation(charge), { table: evaluer(charge), refus: null });
|
||||
assert.ok(evaluation(charge).table.propositions.every(({ statut }) => statut === 'derive'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('les explications des colonnes et des critères (infobulles, § 5.4, § 5.5, § 5.7)', () => {
|
||||
// Le sens de chaque colonne : 'bas' quand la plus faible valeur sert le
|
||||
// mieux, 'grand' quand c'est la plus forte ; null quand l'explication ne
|
||||
|
|
|
|||
|
|
@ -348,6 +348,20 @@ export function versFichier(proposition, configuration, { produitVersion, attrib
|
|||
*/
|
||||
export function planDepuisFichier(p) {
|
||||
exigerCoherent(p, 'planDepuisFichier');
|
||||
return planSansRevalider(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* planDepuisFichier sans la validation : le même plan, pour un placement
|
||||
* dont l'appelant vient de lire fautes(p) vide. evaluer (evaluation.js) est
|
||||
* son seul appelant, qui valide chaque placement une fois avant de le lire ;
|
||||
* tout autre passe par planDepuisFichier. Un placement fautif y rend un
|
||||
* plan faux, sans lever.
|
||||
*
|
||||
* @param {import('./types.js').PropositionFichier|import('./types.js').Retenu} p
|
||||
* @returns {import('../moteur/types.js').Plan}
|
||||
*/
|
||||
export function planSansRevalider(p) {
|
||||
return {
|
||||
tables: [...p.tables],
|
||||
tours: p.placement.map(({ sieges }) => sieges.map((liste) => liste.filter((id) => id !== null).sort(croissant))),
|
||||
|
|
@ -362,7 +376,8 @@ const ordreDesRaisons = (a, b) =>
|
|||
comparerTextes(a.code, b.code) || (a.participant ?? a.table ?? 0) - (b.participant ?? b.table ?? 0);
|
||||
|
||||
// Raisons de dérive d'un placement cohérent, sans contrôle : derive les
|
||||
// rend après l'avoir contrôlé, examiner après l'avoir fait lui-même.
|
||||
// rend après l'avoir contrôlé, examiner après l'avoir fait lui-même,
|
||||
// deriveSansRevalider après que son appelant l'a fait.
|
||||
function raisonsDeDerive({ tables, capacites, tours, participants }, charge) {
|
||||
const raisons = [];
|
||||
const personneDe = new Map(charge.participants.map((personne) => [personne.id, personne]));
|
||||
|
|
@ -425,6 +440,21 @@ export function derive(p, charge) {
|
|||
return raisonsDeDerive(p, charge);
|
||||
}
|
||||
|
||||
/**
|
||||
* derive sans la validation : les mêmes raisons, pour un placement dont
|
||||
* l'appelant vient de lire fautes(p) vide. evaluer (evaluation.js) est son
|
||||
* seul appelant, qui valide chaque placement une fois avant de le lire ;
|
||||
* tout autre passe par derive. Un placement fautif y rend des raisons
|
||||
* fausses, sans lever.
|
||||
*
|
||||
* @param {import('./types.js').PropositionFichier|import('./types.js').Retenu} p
|
||||
* @param {import('./types.js').Charge} charge
|
||||
* @returns {Raison[]}
|
||||
*/
|
||||
export function deriveSansRevalider(p, charge) {
|
||||
return raisonsDeDerive(p, charge);
|
||||
}
|
||||
|
||||
// Ordre d'affichage des tables : numéro croissant, une table absente de la
|
||||
// charge — numéro null — après les autres ; à numéro égal, identifiant
|
||||
// croissant.
|
||||
|
|
|
|||
|
|
@ -1640,6 +1640,37 @@ function releverImportateursReserves(racine, reserves = IMPORTATEURS_RESERVES) {
|
|||
});
|
||||
}
|
||||
|
||||
// Les variantes de placements.js qui lisent un placement sans le valider :
|
||||
// evaluation.js seul les appelle, après avoir validé le placement une fois
|
||||
// par fautes ; tout autre appelant passe par planDepuisFichier et derive,
|
||||
// qui valident. Le relevé lit leurs noms, mots entiers, dans le code des
|
||||
// sources du projet — chaînes comprises, commentaires exclus —, hors de
|
||||
// leurs porteurs et des épreuves.
|
||||
const PORTEURS_SANS_REVALIDATION = ['src/stockage/placements.js', 'src/application/evaluation.js'];
|
||||
const NOM_SANS_REVALIDATION = /\b(?:planSansRevalider|deriveSansRevalider)\b/g;
|
||||
|
||||
// Relevé « fichier:ligne nom » des variantes sans validation nommées hors de
|
||||
// leurs porteurs, par fichier puis par ligne ; puis « porteur sans objet :
|
||||
// fichier » pour un porteur qui ne les nomme plus. Lève quand le périmètre
|
||||
// ne porte aucun fichier.
|
||||
function releverSansRevalidation(racine, porteurs = PORTEURS_SANS_REVALIDATION) {
|
||||
const fichiers = sourcesDuProjet(racine).filter((f) => !EPREUVE.test(f));
|
||||
assert.ok(fichiers.length > 0, 'aucun fichier source');
|
||||
const servis = new Set();
|
||||
const releve = fichiers.flatMap((fichier) => {
|
||||
const nom = enBarres(racine, fichier);
|
||||
const texte = readFileSync(fichier, 'utf8');
|
||||
const trouves = [...texteSansCommentaires(fichier, texte).matchAll(NOM_SANS_REVALIDATION)];
|
||||
if (porteurs.includes(nom)) {
|
||||
if (trouves.length > 0) servis.add(nom);
|
||||
return [];
|
||||
}
|
||||
return trouves.map(({ 0: variante, index }) => `${nom}:${ligneDe(texte, index)} ${variante}`);
|
||||
});
|
||||
const sansObjet = porteurs.filter((p) => !servis.has(p)).map((p) => `porteur sans objet : ${p}`);
|
||||
return [...releve, ...sansObjet];
|
||||
}
|
||||
|
||||
// Le nom d'un papier, que seul src/geometrie/papiers.js définit (§ 13.2) :
|
||||
// le stockage, l'application, l'imposition et l'impression tirent leur
|
||||
// domaine de PAPIERS. Le relevé lit « Lettre », mot entier, dans le code
|
||||
|
|
@ -1696,6 +1727,39 @@ function releverNomsDePapier(racine, porteurs = PORTEURS_DES_NOMS_DE_PAPIER) {
|
|||
return [...releve, ...sansObjet];
|
||||
}
|
||||
|
||||
describe('arborescence : la validation des placements (§ 8.9)', () => {
|
||||
test('planSansRevalider et deriveSansRevalider ne se nomment que dans placements.js, evaluation.js et les épreuves', () => {
|
||||
assert.deepEqual(releverSansRevalidation(RACINE), []);
|
||||
});
|
||||
|
||||
test('la garde des variantes sans validation relève un import, un appel ou une chaîne hors de leurs porteurs, script et composant compris ; commentaires et épreuves passent ; un porteur muet est sans objet', () => {
|
||||
const porteurs = {
|
||||
'src/stockage/placements.js': 'export function planSansRevalider(p) {}\nexport function deriveSansRevalider(p, c) {}\n',
|
||||
'src/application/evaluation.js': "import { deriveSansRevalider, planSansRevalider } from '../stockage/placements.js';\n",
|
||||
};
|
||||
const fichiers = {
|
||||
...porteurs,
|
||||
'src/application/places.js': [
|
||||
'// planSansRevalider, dans un commentaire',
|
||||
"import { planSansRevalider } from '../stockage/placements.js';",
|
||||
'const plan = planSansRevalider(p);',
|
||||
].join('\n'),
|
||||
'src/interface/Plan.svelte': ['<script>', " const nom = 'deriveSansRevalider';", '</script>'].join('\n'),
|
||||
'scripts/outil.js': "import { deriveSansRevalider } from '../src/stockage/placements.js';\n",
|
||||
'src/application/evaluation.test.js': "import { planSansRevalider } from '../stockage/placements.js';\n",
|
||||
'src/application/derive.js': 'const planSansRevaliderAutre = 1;\n',
|
||||
};
|
||||
assert.deepEqual(avecArbre(fichiers, releverSansRevalidation), [
|
||||
'scripts/outil.js:1 deriveSansRevalider',
|
||||
'src/application/places.js:2 planSansRevalider',
|
||||
'src/application/places.js:3 planSansRevalider',
|
||||
'src/interface/Plan.svelte:2 deriveSansRevalider',
|
||||
]);
|
||||
const muet = { 'src/stockage/placements.js': porteurs['src/stockage/placements.js'], 'src/application/evaluation.js': '\n' };
|
||||
assert.deepEqual(avecArbre(muet, releverSansRevalidation), ['porteur sans objet : src/application/evaluation.js']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arborescence : les sorties — PDF, impression, police (§ 11.1, § 13.4, § 17 point 2)', () => {
|
||||
test('les paquets de développement sont ceux de la liste fermée, les trois paquets des sorties épinglés exacts, sans script d’installation ; aucune dépendance d’exécution', () => {
|
||||
assert.deepEqual(releverPaquetsDeclares(RACINE), []);
|
||||
|
|
|
|||
Loading…
Reference in a new issue