gestion_table_tournante_libre/src/application/generation.js
Mathieu Benoit ff31a6e1ca [ADD] engine for quality: A_max, facilitator diversity, run trace, pairs
The quality page needs figures the engine did not yet produce. It now
bounds each person's distinct memberships (A_max) and checks it with
the indicator guard; reads facilitator diversity over the evening and
per round; records the search's moves and record points; lists repeated
and imposed pairs; and lays out charts through one geometry module with
two bench thresholds. Chart colour tokens carry their own validation.
Default plans still equal the reference fingerprints; nothing new is
written to the file yet.
Checked: node 4768, navigateur 967, node-long 128; new modules at 100 %.

--- FR ---

[ADD] moteur de la qualité : A_max, diversité, course, paires

La page de qualité demande des chiffres que le moteur ne produisait pas.
Il borne désormais les appartenances distinctes de chacun (A_max) et la
garde des indicateurs la vérifie ; il lit la diversité des animateurs sur
la soirée et par tour ; il relève les mouvements et les points de la
recherche ; il liste les paires répétées et imposées ; un seul module de
géométrie dispose les graphiques, avec deux seuils au banc. Les jetons
des graphiques portent leur propre validation. Les plans par défaut
égalent les empreintes ; rien de neuf ne s'écrit encore au fichier.
Vérifié : node 4768, navigateur 967, longues 128 ; nouveaux modules à 100 %.

Assisted-by: Claude Opus 5.5
2026-10-12 04:29:27 -04:00

639 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// La génération (§ 5.7, § 5.8, § 5.10) : les exécuteurs qui font tourner la
// recherche du moteur, et le contrôleur qui la lance depuis la séance.
//
// Un exécuteur rend, pour une configuration et des réglages, une exécution :
// son résultat, une promesse des propositions de rechercher ; ses
// avancements ; son annulation, qui rejette le résultat par ErreurAnnulee,
// sans résultat partiel. L'exécuteur direct calcule dans le fil, une
// microtâche après le lancement : il sert les épreuves et le premier
// montage de l'application ; celui du travailleur (I13) calcule hors du fil
// et s'annule par terminate(). Une erreur de la recherche devient la même
// dans les deux, par versErreurGeneration.
//
// Le contrôleur tient aussi le réglage des cœurs de calcul, un réglage du
// poste qui vaut pour la séance de l'application et ne s'écrit nulle part :
// automatique, la génération emploie autant de cœurs que l'exécuteur annonce
// de processeurs logiques (coeursDisponibles, que src/main.js lit de la
// plateforme) ; sinon le nombre saisi, de 1 à ces disponibles. Un lancement
// en emploie min(nombre de propositions, cœurs réglés), et l'estimation
// compte en vagues de propositions calculées ensemble, chacune ralentie de
// ρ(W) : un cœur va moins vite quand ses voisins travaillent aussi. ρ croît
// de 1, sur un cœur seul, à RALENTISSEMENT_PLEINE_CHARGE du registre des
// constantes, quand les L processeurs logiques travaillent (ralentissement).
//
// Le contrôleur ne numérote rien et ne mesure aucun plan. Il refuse avant
// tout lancement un réglage hors de son domaine (REGLAGE_HORS_DOMAINE), puis
// ce que l'enregistrement refuserait — les refus de la séance, lus dans son
// état observable, dans l'ordre de son contrat —, puis
// ce qui rend la génération vaine ou impossible ; il garde la graine
// proposée et la durée du dernier calcul, en mémoire, pour la séance de
// l'application ; il vérifie les invariants de chaque proposition rendue,
// écarte celles dont le plan est déjà présent, et confie les autres à la
// commande enregistrerGeneration, une seule entrée du journal. Le nombre de
// propositions d'une génération est un réglage, jamais une constante d'ici.
//
// La course (§ 5.10). Chaque proposition du moteur porte ses mouvements
// évalués et ses points de relevé ; le contrôleur la confie telle quelle à
// enregistrerGeneration. Ce qui dépend de la machine ne vaut que pour la
// séance et ne s'écrit nulle part : le résumé de chaque génération — la
// durée prise autour du calcul entier, les cœurs employés, la somme des
// mouvements évalués de toutes les propositions calculées, identiques
// écartées comprises, et leur nombre —, rattaché à chaque proposition
// qu'elle a ajoutée, sous la graine, le compte d'arrêt et l'historique que
// celle-ci portait. Aucune proposition n'est datée seule. Ce qui est gardé
// s'oublie quand l'événement se ferme ou qu'un autre s'ouvre ; un
// rechargement de l'application l'oublie avec le contrôleur.
//
// Le compte d'inactivité de la séance est suspendu pendant toute la
// génération (debuterCalcul), et repart à sa fin, quelle qu'elle soit
// (finirCalcul, dans un finally). Passer en lecture, ou fermer l'événement,
// annule la génération en cours (surAnnulation).
import { constante } from '../geometrie/constantes.js';
import { normaliser, nombrePlacesManquantes } from '../moteur/configuration.js';
import { ErreurAnnulee, ErreurConfiguration } from '../moteur/erreurs.js';
import { rechercher, verifierReglages } from '../moteur/recherche.js';
import { verifierInvariants } from '../moteur/verification.js';
import { GENERATION_PAR_DEFAUT } from '../stockage/document.js';
import { derive, planDepuisFichier, versFichier } from '../stockage/placements.js';
import { lireEntier } from './capacite.js';
import { configurationDe } from './configuration.js';
// src/interface reçoit l'annulation d'ici : elle ne joint pas le moteur.
export { ErreurAnnulee } from '../moteur/erreurs.js';
/** Au-delà de cette durée estimée, en secondes, la génération demande confirmation (§ 5.10). */
export const SEUIL_GENERATION_LONGUE_S = 60;
/** Le plafond d'usage d'une génération, en secondes (§ 5.10) : un repère dit, jamais un refus. */
export const PLAFOND_USAGE_S = 30 * 60;
// Sans durée mesurée, une génération de plus de mouvements que ce compte —
// dix fois le réglage par défaut —, ralentissement compris, demande
// confirmation.
const MOUVEMENTS_SANS_MESURE = 10 * GENERATION_PAR_DEFAUT.nombre * GENERATION_PAR_DEFAUT.arret;
// Le ralentissement d'un cœur quand tous les processeurs logiques
// travaillent, nommé dans le registre des constantes mesurées (§ 14.1) : un
// nom de constante, que la table des libellés ne connaît pas.
const RALENTISSEMENT = Object.freeze({ nom: 'RALENTISSEMENT_PLEINE_CHARGE' });
// La plus grande graine, et le modulo de la suivante.
const GRAINE_MAX = 2 ** 32 - 1;
const GRAINES = 2 ** 32;
// Les champs des réglages, dans l'ordre où leurs refus se rendent, et le
// domaine de chacun ; max null : sans borne.
const CHAMPS = [
['graine', 0, GRAINE_MAX],
['nombre', 1, null],
['arret', 1, null],
['historique', 1, null],
];
const versSection = (section) => ({ geste: 'ouvrirSection', section });
/**
* Refus ou échec d'une génération : son code et ses détails — ceux de la
* séance, du moteur ou du contrôleur, tels quels. Le texte affiché est celui
* de son code, nu (§ 14.6) ; le message, code et détails en JSON, sert aux
* traces.
*/
export class ErreurGeneration extends Error {
/**
* @param {string} code
* @param {Object} [details]
*/
constructor(code, details = {}) {
super(`${code} ${JSON.stringify(details)}`);
this.code = code;
this.details = details;
}
}
ErreurGeneration.prototype.name = 'ErreurGeneration';
/**
* Les réglages d'une génération, contrôlés par la règle de rechercher
* (verifierReglages) avant tout calcul. Lève, pour le premier réglage hors
* de son domaine, ErreurGeneration('REGLAGE_HORS_DOMAINE', { reglage,
* remede: null }), reglage étant son nom — graine, arret, historique ou
* nombre —, sans le message du moteur ; ne rend rien sinon. Toute autre
* erreur du contrôle est relevée telle quelle.
*
* @param {Object} reglages
*/
export function exigerReglages(reglages) {
try {
verifierReglages(reglages);
} catch (erreur) {
if (erreur instanceof RangeError && typeof erreur.reglage === 'string') {
throw new ErreurGeneration('REGLAGE_HORS_DOMAINE', { reglage: erreur.reglage, remede: null });
}
throw erreur;
}
}
/**
* Le nombre de processeurs logiques d'un poste, tel que la plateforme le
* donne — hyperthreads compris —, rendu utilisable : un entier d'au moins 1
* tel quel, 1 pour toute autre valeur, absente comprise.
*
* @param {unknown} valeur
* @returns {number}
*/
export function coeursDe(valeur) {
return Number.isInteger(valeur) && valeur >= 1 ? valeur : 1;
}
/**
* Le ralentissement d'un cœur quand W cœurs travaillent ensemble sur un
* poste de L processeurs logiques : le rapport de la durée d'une même
* proposition sur l'un d'eux à sa durée sur un cœur seul. Il croît en ligne
* droite de 1, à W = 1, à rhoMax, à W = L : 1 + (rhoMax − 1)(W − 1)/(L − 1).
* Un poste d'un seul processeur logique n'a aucun ralentissement : 1.
*
* @param {number} W les cœurs employés, de 1 à L
* @param {number} L les processeurs logiques du poste, au moins 1
* @param {number} rhoMax le ralentissement à pleine charge, W = L
* @returns {number}
*/
export function ralentissement(W, L, rhoMax) {
if (L === 1) return 1;
return 1 + ((rhoMax - 1) * (W - 1)) / (L - 1);
}
/**
* L'erreur d'une recherche, la même dans les deux exécuteurs : ErreurAnnulee
* telle quelle ; une ErreurConfiguration devient ErreurGeneration de son code
* et de ses détails ; toute autre, RangeError comprise, est rendue telle
* quelle, pour être relevée. Les réglages sont contrôlés avant le calcul
* (exigerReglages) : une RangeError qui survient pendant lui vient d'ailleurs,
* d'un abonné aux avancements par exemple, et ne se dit pas comme un réglage.
*
* @param {unknown} erreur
* @returns {unknown}
*/
export function versErreurGeneration(erreur) {
if (erreur instanceof ErreurAnnulee) return erreur;
if (erreur instanceof ErreurConfiguration) return new ErreurGeneration(erreur.code, erreur.details);
return erreur;
}
/**
* @typedef {Object} Execution
* @property {(fn: (fait: number, total: number) => void) => () => void} surAvancement
* abonne fn aux avancements ; rend la fonction qui désabonne
* @property {Promise<import('../moteur/recherche.js').Proposition[]>} resultat
* rejetée par ErreurAnnulee à l'annulation, par ErreurGeneration sur
* une erreur du moteur ; les propositions par identifiant croissant
* @property {() => void} annuler sans résultat partiel
* @property {number} [coeurs] les cœurs que le calcul emploie ; 1 s'il
* ne le dit pas
*
* @typedef {Object} Executeur
* @property {(configuration: Object, reglages: Object, options?: {coeurs?: number}) => Execution} lancer
* options.coeurs : les cœurs que le calcul peut employer, 1 par défaut
* @property {number} [coeursDisponibles] les processeurs logiques du poste ;
* 1 s'il ne le dit pas
*/
/**
* L'exécuteur direct : rechercher dans le fil, une microtâche après lancer.
* Rien d'autre ne s'exécute pendant la recherche. annuler(), avant ce départ
* ou depuis un rappel d'avancement, fait lever ErreurAnnulee à la lecture
* suivante du signal par le moteur : avant la proposition suivante, au plus
* 1 000 mouvements plus tard. Chaque avancement est celui du moteur, toutes
* les 1 000 itérations. Il calcule une proposition après l'autre quel que
* soit options.coeurs, et chaque exécution dit n'employer qu'un cœur ; il
* annonce les cœurs qu'on lui donne, par coeursDe, pour qu'une épreuve règle
* ce que le contrôleur en lit.
*
* @param {{coeursDisponibles?: unknown}} [options]
* @returns {Executeur}
*/
export function creerExecuteurDirect({ coeursDisponibles } = {}) {
return {
coeursDisponibles: coeursDe(coeursDisponibles),
lancer(configuration, reglages) {
const signal = { aborted: false };
const abonnes = new Set();
const progression = (fait, total) => {
for (const fn of [...abonnes]) fn(fait, total);
};
const resultat = Promise.resolve().then(() => {
exigerReglages(reglages);
try {
return rechercher(configuration, reglages, { signal, progression });
} catch (erreur) {
throw versErreurGeneration(erreur);
}
});
return {
surAvancement(fn) {
abonnes.add(fn);
return () => {
abonnes.delete(fn);
};
},
resultat,
annuler() {
signal.aborted = true;
},
coeurs: 1,
};
},
};
}
// Le plan d'une proposition du moteur, rangé comme planDepuisFichier rend
// celui d'une proposition enregistrée : la même mise en forme, versFichier
// puis planDepuisFichier, pour que deux plans égaux s'écrivent pareil.
function planRange(proposition, configuration, produitVersion) {
const fichier = versFichier(proposition, configuration, { produitVersion, attribuerSieges: false, decalage: 0 });
return JSON.stringify(planDepuisFichier(fichier));
}
/**
* Le contrôleur de génération d'une séance (§ 5.7, § 5.10).
*
* @param {Object} parametres
* @param {Object} parametres.seance la séance d'A1 et A2 : etat,
* subscribe, surAnnulation, executer, debuterCalcul, finirCalcul
* @param {Executeur} parametres.executeur coeursDisponibles, qu'il annonce,
* borne le réglage des cœurs
* @param {import('./horloge.js').Horloge} parametres.horloge instant() mesure
* la durée d'un calcul
* @param {string} parametres.produitVersion
* @param {(nom: string) => number|null} [parametres.constante] la lecture
* du registre des constantes, constante de constantes.js par défaut ;
* le contrôleur y lit RALENTISSEMENT_PLEINE_CHARGE à chaque
* estimation, sans en garder la valeur
* @returns {Object} le contrôleur : grainePropose, reglagesProposes, valider,
* estimer, derniereGeneration, generationDe, generer, annuler,
* coeurs, reglerCoeurs, subscribe
*/
export function creerControleurGeneration({ seance, executeur, horloge, produitVersion, constante: lireConstante = constante }) {
// graine : la graine proposée ; base : celle de l'événement ouvert, null
// sans événement ; mesure : le dernier calcul abouti, null avant lui ;
// execution : celle en cours, null hors génération ; enCours vaut vrai du
// lancement à la fin de l'enregistrement.
// coeurs : le réglage des cœurs de calcul, que l'observable porte —
// automatique, le nombre saisi (manuel), les disponibles, et nombre, les
// cœurs réglés.
// generations : par identifiant d'une proposition ajoutée ici, { graine,
// arret, historique, resume }, ce qu'elle portait à l'ajout et le résumé
// de sa génération ; vidé quand base change.
let graine = 1;
let base = null;
let mesure = null;
let execution = null;
const generations = new Map();
const disponibles = coeursDe(executeur.coeursDisponibles);
let reglageCoeurs = { automatique: true, manuel: disponibles };
const coeurs = () => ({
...reglageCoeurs,
disponibles,
nombre: reglageCoeurs.automatique ? disponibles : reglageCoeurs.manuel,
});
let observable = { enCours: false, fait: 0, total: 0, annulee: null, coeurs: coeurs() };
const abonnes = new Set();
function publier(changements) {
observable = { ...observable, ...changements };
for (const fn of [...abonnes]) fn(observable);
}
// Une ouverture : l'événement passe de null, ou d'une autre base, à une
// base ; la graine proposée repart de 1.
// Une fermeture, ou l'ouverture d'un autre événement, oublie les
// générations gardées.
seance.subscribe((etat) => {
const suivante = etat.evenement?.base ?? null;
if (suivante !== null && suivante !== base) graine = 1;
if (suivante !== base) generations.clear();
base = suivante;
});
// Les réglages de génération de la charge ouverte, ceux d'une charge
// neuve sans événement.
const reglagesDeLaCharge = () => seance.etat().charge?.reglages.generation ?? GENERATION_PAR_DEFAUT;
function annulerAvec(cause) {
if (execution === null) return;
const courante = execution;
publier({ annulee: cause });
courante.annuler();
}
// Les cœurs qu'un lancement de nombre propositions emploie.
const coeursPour = (nombre) => Math.min(nombre, coeurs().nombre);
// Les mouvements d'une génération qui se suivent sur un même cœur : arret
// pour chacune des ⌈nombre / cœurs⌉ vagues de propositions calculées
// ensemble.
const mouvementsParCoeur = (nombre, arret, parallele) => Math.ceil(nombre / parallele) * arret;
// Le coût d'une génération sur parallele cœurs, en mouvements d'un cœur
// seul : ceux qui se suivent sur un même cœur, ralentis de ρ(parallele)
// sur les processeurs logiques du poste. ρ à pleine charge se lit au
// registre à chaque appel.
const cout = (nombre, arret, parallele) =>
mouvementsParCoeur(nombre, arret, parallele) * ralentissement(parallele, disponibles, lireConstante(RALENTISSEMENT.nom));
// La durée d'une génération, proportionnelle à son coût, celui de la
// mesure ramené aux cœurs qu'elle a employés : à cœurs égaux, le
// ralentissement se simplifie.
function estimer({ nombre, arret }) {
if (mesure === null) return { inconnue: true };
const parallele = coeursPour(nombre);
const secondes =
(cout(nombre, arret, parallele) * mesure.dureeMs) / (cout(mesure.nombre, mesure.arret, mesure.coeurs) * 1000);
return {
secondes,
longue: secondes > SEUIL_GENERATION_LONGUE_S,
auDelaDuPlafond: secondes > PLAFOND_USAGE_S,
mesure: { ...mesure },
coeurs: parallele,
};
}
// Longue : au-delà du seuil estimé, ou, sans mesure, quand le coût — les
// mouvements qui se suivent sur un même cœur, ralentis de ρ — dépasse dix
// fois le réglage par défaut.
function estLongue(reglages, estimation) {
if (estimation.inconnue === true) {
return cout(reglages.nombre, reglages.arret, coeursPour(reglages.nombre)) > MOUVEMENTS_SANS_MESURE;
}
return estimation.longue;
}
// Les refus d'avant lancement : les réglages d'abord, puis l'ordre du
// contrat ; rend la configuration et son instance quand la génération peut
// partir.
function exigerLancement(reglages, confirmee) {
exigerReglages(reglages);
const etat = seance.etat();
if (etat.evenement === null) throw new ErreurGeneration('AUCUN_EVENEMENT', { remede: null });
if (etat.refus !== null) throw new ErreurGeneration(etat.refus.code, etat.refus.details);
if (execution !== null || observable.enCours) throw new ErreurGeneration('GENERATION_EN_COURS', { remede: null });
const { charge } = etat;
if (!charge.participants.some(({ exclu }) => !exclu)) {
throw new ErreurGeneration('AUCUN_PARTICIPANT', { remede: versSection('participants') });
}
if (charge.tables.length === 0) throw new ErreurGeneration('AUCUNE_TABLE', { remede: versSection('tables') });
const configuration = configurationDe(charge);
let instance;
try {
instance = normaliser(configuration);
} catch (erreur) {
if (erreur instanceof ErreurConfiguration) throw new ErreurGeneration(erreur.code, { ...erreur.details, remede: null });
throw erreur;
}
const placesManquantes = nombrePlacesManquantes(instance);
if (placesManquantes > 0) {
throw new ErreurGeneration('PLACES_MANQUANTES', { placesManquantes, remede: versSection('tables') });
}
const estimation = estimer(reglages);
if (!confirmee && estLongue(reglages, estimation)) {
throw new ErreurGeneration('CONFIRMATION_REQUISE', { estimation, remede: null });
}
return { configuration, instance };
}
// Le calcul confié à l'exécuteur : ses avancements publiés, son annulation
// par la séance suivie ; rend les propositions et la durée du calcul. Une
// annulation lève ErreurAnnulee, même quand l'exécuteur a rendu un
// résultat entre-temps.
async function calculer(configuration, reglages) {
const debut = horloge.instant();
execution = executeur.lancer(configuration, reglages, { coeurs: coeursPour(reglages.nombre) });
const employes = coeursDe(execution.coeurs);
const desabonner = execution.surAvancement((fait, total) => publier({ fait, total }));
const desannuler = seance.surAnnulation(() => annulerAvec('seance'));
try {
const propositions = await execution.resultat;
if (observable.annulee !== null) throw new ErreurAnnulee();
return { propositions, dureeMs: horloge.instant() - debut, coeurs: employes };
} finally {
desabonner();
desannuler();
execution = null;
}
}
// Les propositions à enregistrer, par identifiant du moteur croissant :
// chacune sans violation des invariants, sinon GENERATION_INVARIANT_VIOLE,
// rang compris — la place de la proposition dans la suite des graines du
// lancement, qui n'est l'identifiant d'aucune proposition enregistrée ; puis sans
// celles dont le plan est celui d'une proposition présente sans dérive, ou
// d'une proposition déjà gardée de ce lancement.
function trier(propositions, configuration, instance) {
const ordonnees = [...propositions].sort((a, b) => a.id - b.id);
for (const proposition of ordonnees) {
const violations = verifierInvariants(instance, proposition.plan);
if (violations.length > 0) {
throw new ErreurGeneration('GENERATION_INVARIANT_VIOLE', { rang: proposition.id, violations, remede: null });
}
}
const charge = seance.etat().charge;
const presents = new Set(
charge.propositions.filter((p) => derive(p, charge).length === 0).map((p) => JSON.stringify(planDepuisFichier(p))),
);
const gardees = [];
for (const proposition of ordonnees) {
const plan = planRange(proposition, configuration, produitVersion);
if (presents.has(plan)) continue;
presents.add(plan);
gardees.push(proposition);
}
return { gardees, identiques: ordonnees.length - gardees.length };
}
// Les identifiants que l'enregistrement a donnés : ceux des propositions
// de la charge qui n'étaient pas avant le geste, et qui portent la graine,
// l'arrêt et l'historique d'une proposition gardée ; croissants.
function ajouteesDans(charge, avant, gardees) {
const cles = new Set(gardees.map(({ graine: g, arret, historique }) => `${g}|${arret}|${historique}`));
return charge.propositions
.filter(({ id, graine: g, arret, historique }) => !avant.has(id) && cles.has(`${g}|${arret}|${historique}`))
.map(({ id }) => id)
.sort((a, b) => a - b);
}
// Rattache le résumé d'une génération à chaque proposition ajoutée de la
// charge, sous la graine, le compte d'arrêt et l'historique qu'elle porte.
function garder(charge, ajoutees, resume) {
for (const id of ajoutees) {
const { graine: g, arret, historique } = charge.propositions.find((proposition) => proposition.id === id);
generations.set(id, { graine: g, arret, historique, resume });
}
}
async function generer(reglages, { confirmee = false } = {}) {
const { configuration, instance } = exigerLancement(reglages, confirmee);
graine = (reglages.graine + 1) % GRAINES;
publier({ enCours: true, fait: 0, total: reglages.nombre * reglages.arret, annulee: null });
seance.debuterCalcul();
try {
const { propositions, dureeMs, coeurs: employes } = await calculer(configuration, reglages);
mesure = {
dureeMs,
mouvements: reglages.nombre * reglages.arret,
participants: instance.N,
nombre: reglages.nombre,
arret: reglages.arret,
coeurs: employes,
};
const { gardees, identiques } = trier(propositions, configuration, instance);
if (gardees.length === 0) return { ajoutees: [], identiques, dureeMs };
const avant = new Set(seance.etat().charge.propositions.map(({ id }) => id));
await seance.executer('enregistrerGeneration', { propositions: gardees, configuration, produitVersion });
const charge = seance.etat().charge;
const ajoutees = ajouteesDans(charge, avant, gardees);
let evalues = 0;
for (const proposition of propositions) evalues += proposition.evalues;
garder(charge, ajoutees, Object.freeze({ dureeMs, coeurs: employes, evalues, nombre: propositions.length }));
return { ajoutees, identiques, dureeMs };
} finally {
seance.finirCalcul();
publier({ enCours: false });
}
}
return {
/** La graine proposée : 1 à chaque ouverture, puis la dernière lancée plus un, modulo 2^32. */
grainePropose: () => graine,
/** { graine, nombre, arret, historique } : la graine proposée, les trois autres de la charge. */
reglagesProposes() {
const { nombre, arret, historique } = reglagesDeLaCharge();
return { graine, nombre, arret, historique };
},
/**
* Les saisies des quatre champs, chacune lue par lireEntier (NON_ENTIER
* {saisie}), puis bornée à son domaine (HORS_DOMAINE {min, max}, max null
* sans borne) ; un historique non saisi — absent, null ou blanc — vaut
* celui de la charge. Rend { reglages, refus } : reglages null dès
* qu'un champ est refusé, refus dans l'ordre des champs.
*/
valider(saisies) {
const reglages = {};
const refus = [];
for (const [champ, min, max] of CHAMPS) {
const saisie = saisies[champ];
if (champ === 'historique' && (saisie === undefined || saisie === null || (typeof saisie === 'string' && saisie.trim() === ''))) {
reglages.historique = reglagesDeLaCharge().historique;
continue;
}
const lu = lireEntier(saisie);
if (lu.refus !== undefined) {
refus.push({ champ, code: lu.refus.code, details: lu.refus.details });
} else if (lu.valeur < min || (max !== null && lu.valeur > max)) {
refus.push({ champ, code: 'HORS_DOMAINE', details: { min, max, remede: null } });
} else {
reglages[champ] = lu.valeur;
}
}
return { reglages: refus.length === 0 ? reglages : null, refus };
},
/**
* L'estimation des réglages, proportionnelle aux mouvements qui se
* suivent sur un même cœur — arret × ⌈nombre / cœurs⌉ — ralentis de
* ρ(cœurs) (ralentissement), ceux de la dernière mesure ramenés de même
* aux cœurs qu'elle a employés :
* { inconnue: true } sans elle, sinon { secondes, longue,
* auDelaDuPlafond, mesure, coeurs }, coeurs étant ceux que ces réglages
* emploieraient.
*/
estimer,
/**
* La mesure du dernier calcul abouti de ce contrôleur, en mémoire ; null
* avant lui : { dureeMs, mouvements, participants, nombre, arret,
* coeurs }, coeurs étant ceux que l'exécution a employés.
*/
derniereGeneration: () => (mesure === null ? null : { ...mesure }),
/**
* La génération de cette séance d'où vient la proposition id de la
* charge ouverte : { dureeMs, coeurs, evalues, nombre }, une copie —
* la durée prise autour du calcul entier, les cœurs employés, la somme
* des mouvements évalués de toutes les propositions calculées, identiques
* écartées comprises, leur nombre. null quand aucune génération de la
* séance ne l'a ajoutée, quand la charge n'a pas de proposition id, ou
* quand celle-ci ne porte plus la graine, le compte d'arrêt et
* l'historique gardés pour elle.
*/
generationDe(id) {
const gardee = generations.get(id);
if (gardee === undefined) return null;
const proposition = seance.etat().charge?.propositions.find((candidate) => candidate.id === id);
if (
proposition === undefined
|| proposition.graine !== gardee.graine
|| proposition.arret !== gardee.arret
|| proposition.historique !== gardee.historique
) {
return null;
}
return { ...gardee.resume };
},
/**
* Lance une génération et l'enregistre : { ajoutees, identiques, dureeMs }.
* Refuse avant tout lancement, par ErreurGeneration, dans cet ordre :
* REGLAGE_HORS_DOMAINE {reglage} ; AUCUN_EVENEMENT ; le refus courant de
* la séance ; GENERATION_EN_COURS ;
* AUCUN_PARTICIPANT ; AUCUNE_TABLE ; le refus de normaliser ;
* PLACES_MANQUANTES ; CONFIRMATION_REQUISE {estimation}, que
* confirmee lève. Après la recherche : GENERATION_INVARIANT_VIOLE
* {rang, violations}, puis ce
* qu'executer lève, tel quel. Annulée : ErreurAnnulee.
*/
generer,
/** Annule la génération en cours, sans résultat partiel ; annulee vaut 'commande'. */
annuler: () => annulerAvec('commande'),
/**
* Le réglage des cœurs de calcul : { automatique, manuel, disponibles,
* nombre } — automatique vrai à la création ; manuel, le dernier nombre
* saisi, les disponibles avant toute saisie ; disponibles, ceux
* qu'annonce l'exécuteur, par coeursDe ; nombre, les cœurs réglés,
* disponibles en automatique, manuel sinon.
*/
coeurs,
/**
* Règle les cœurs de calcul, pour la séance de l'application : rien ne
* s'écrit, ni état, ni journal. saisie, quand elle est donnée, se lit
* par lireEntier (NON_ENTIER {saisie}), puis se borne de 1 aux
* disponibles (HORS_DOMAINE {min: 1, max}), remède null ; refusée, rien
* ne change, automatique compris. Rend { refus: null } ou { refus:
* {code, details} } ; un réglage accepté se publie aux abonnés.
*/
reglerCoeurs({ automatique, saisie } = {}) {
const suivant = { ...reglageCoeurs };
if (saisie !== undefined) {
const lu = lireEntier(saisie);
if (lu.refus !== undefined) return { refus: { code: lu.refus.code, details: lu.refus.details } };
if (lu.valeur < 1 || lu.valeur > disponibles) {
return { refus: { code: 'HORS_DOMAINE', details: { min: 1, max: disponibles, remede: null } } };
}
suivant.manuel = lu.valeur;
}
if (automatique !== undefined) suivant.automatique = automatique === true;
reglageCoeurs = suivant;
publier({ coeurs: coeurs() });
return { refus: null };
},
/**
* Le contrat des magasins de Svelte sur { enCours, fait, total, annulee,
* coeurs } : annulee, la cause de la dernière annulation, 'commande' ou
* 'seance', null dès le lancement suivant ; coeurs, le réglage des
* cœurs de calcul, celui que rend coeurs().
*/
subscribe(fn) {
abonnes.add(fn);
fn(observable);
return () => {
abonnes.delete(fn);
};
},
};
}