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
639 lines
29 KiB
JavaScript
639 lines
29 KiB
JavaScript
// © 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);
|
||
};
|
||
},
|
||
};
|
||
}
|