[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
2026-10-11 03:43:47 -04:00
// © 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
[ADD] screens: tags on the card, table goals, priority editor, import
The operator now reaches tags, goals and priorities from the screens.
A person's card adds and removes tags, the participant list carries a
tag legend; each table takes an evening goal or one per round with its
rule; the generation settings hold a keyboard-operable priority editor
(move, join, split, restore, weights), the two optional criteria and the
goal mode; the proposals table and its ranking sentence follow the
order; the import screen reads a participants or a goals file. Removing
a tag or a goal returns focus to a neighbouring control.
Checked: node 4590, navigateur 942, node-long 124; captures republished.
--- FR ---
[ADD] écrans : étiquettes, objectifs des tables, priorités, import
L'opérateur atteint étiquettes, objectifs et priorités depuis les écrans.
La fiche ajoute et retire les étiquettes, la liste porte leur légende ;
chaque table reçoit un objectif de soirée ou par tour avec sa règle ; les
réglages de génération portent un éditeur des priorités utilisable au
clavier (monter, joindre, séparer, rétablir, poids), les deux critères
facultatifs et le mode des objectifs ; le tableau des propositions et sa
phrase suivent l'ordre ; l'import lit un fichier de personnes ou
d'objectifs. Retirer une étiquette ou un objectif rend le focus à côté.
Vérifié : node 4590, navigateur 942, longues 124 ; captures republiées.
Assisted-by: Claude Opus 5.5
2026-10-11 17:53:27 -04:00
// la suit. Une charge sans proposition n'a pas de table partagée : l'éditeur
// des priorités y lit l'état des critères par une évaluation à lui, une fois
// par charge écrite.
[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
2026-10-11 03:43:47 -04:00
//
// 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' , { } ) ;
[ADD] screens: tags on the card, table goals, priority editor, import
The operator now reaches tags, goals and priorities from the screens.
A person's card adds and removes tags, the participant list carries a
tag legend; each table takes an evening goal or one per round with its
rule; the generation settings hold a keyboard-operable priority editor
(move, join, split, restore, weights), the two optional criteria and the
goal mode; the proposals table and its ranking sentence follow the
order; the import screen reads a participants or a goals file. Removing
a tag or a goal returns focus to a neighbouring control.
Checked: node 4590, navigateur 942, node-long 124; captures republished.
--- FR ---
[ADD] écrans : étiquettes, objectifs des tables, priorités, import
L'opérateur atteint étiquettes, objectifs et priorités depuis les écrans.
La fiche ajoute et retire les étiquettes, la liste porte leur légende ;
chaque table reçoit un objectif de soirée ou par tour avec sa règle ; les
réglages de génération portent un éditeur des priorités utilisable au
clavier (monter, joindre, séparer, rétablir, poids), les deux critères
facultatifs et le mode des objectifs ; le tableau des propositions et sa
phrase suivent l'ordre ; l'import lit un fichier de personnes ou
d'objectifs. Retirer une étiquette ou un objectif rend le focus à côté.
Vérifié : node 4590, navigateur 942, longues 124 ; captures republiées.
Assisted-by: Claude Opus 5.5
2026-10-11 17:53:27 -04:00
flushSync ( ) ;
expect ( appels ( ) , 'la charge sans proposition, évaluée une fois pour l’ état des critères' ) . toBe ( avantEcriture + 1 ) ;
aller ( m , 'participants' ) ;
aller ( m , 'generation' ) ;
expect ( appels ( ) , 'la charge sans proposition, revisitée' ) . toBe ( avantEcriture + 1 ) ;
const avantGeneration = appels ( ) ;
[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
2026-10-11 03:43:47 -04:00
await enregistrer ( m , { graine : 9 , nombre : 2 , arret : 300 } ) ;
[ADD] screens: tags on the card, table goals, priority editor, import
The operator now reaches tags, goals and priorities from the screens.
A person's card adds and removes tags, the participant list carries a
tag legend; each table takes an evening goal or one per round with its
rule; the generation settings hold a keyboard-operable priority editor
(move, join, split, restore, weights), the two optional criteria and the
goal mode; the proposals table and its ranking sentence follow the
order; the import screen reads a participants or a goals file. Removing
a tag or a goal returns focus to a neighbouring control.
Checked: node 4590, navigateur 942, node-long 124; captures republished.
--- FR ---
[ADD] écrans : étiquettes, objectifs des tables, priorités, import
L'opérateur atteint étiquettes, objectifs et priorités depuis les écrans.
La fiche ajoute et retire les étiquettes, la liste porte leur légende ;
chaque table reçoit un objectif de soirée ou par tour avec sa règle ; les
réglages de génération portent un éditeur des priorités utilisable au
clavier (monter, joindre, séparer, rétablir, poids), les deux critères
facultatifs et le mode des objectifs ; le tableau des propositions et sa
phrase suivent l'ordre ; l'import lit un fichier de personnes ou
d'objectifs. Retirer une étiquette ou un objectif rend le focus à côté.
Vérifié : node 4590, navigateur 942, longues 124 ; captures republiées.
Assisted-by: Claude Opus 5.5
2026-10-11 17:53:27 -04:00
expect ( appels ( ) , 'la charge écrite, mesurée une fois' ) . toBe ( avantGeneration + 1 ) ;
[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
2026-10-11 03:43:47 -04:00
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 ) ;
} ) ;
} ) ;