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
348 lines
16 KiB
JavaScript
348 lines
16 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// L'orchestrateur du banc de mesure (§ 19.10), que lancent npm run banc et
|
||
// make banc. Son déroulé :
|
||
//
|
||
// 1. effacer les sorties du navigateur (scripts/banc/sorties/<nom>.json,
|
||
// chacune par son nom), pour qu'aucune sortie d'un passage précédent
|
||
// ne se lise comme une mesure de celui-ci ;
|
||
// 2. relever le coût du plafond a priori sous node (plafond.js) ;
|
||
// 3. jouer le projet banc de Vitest (vitest.banc.config.js), qui écrit
|
||
// les sorties ; une épreuve tombée — un contrôle du harnais, une
|
||
// mesure qui se refuse — arrête le passage sans relevé ;
|
||
// 4. lire les sorties, et assembler le relevé : la configuration de la
|
||
// machine, puis chaque mesure attendue, une fois, dans l'ordre du
|
||
// registre, avec le ralentissement sous lequel elle s'est prise ; une
|
||
// mesure manquante arrête le passage sans relevé ;
|
||
// 5. juger : une mesure de texte prise hors de la police embarquée
|
||
// arrête le passage sans relevé ; un verdict « non tenu » s'écrit au
|
||
// relevé, qui est une donnée, et rend la sortie non nulle ;
|
||
// 6. écrire scripts/banc/releve.json, puis imprimer le tableau des
|
||
// constantes, les valeurs proposées face à celles du registre et les
|
||
// écarts de leur accord.
|
||
//
|
||
// Le banc écrit un relevé, jamais le registre : src/geometrie/constantes.js
|
||
// ne se lit ici que pour l'ordre et les valeurs, et une valeur s'adopte à la
|
||
// main (releve.js). La configuration du relevé est la liste fermée du
|
||
// contrat — cadre, facteur d'échelle, versions, nombre de processeurs
|
||
// logiques, ralentissement, police — et ne nomme pas la machine : ni nom
|
||
// d'hôte, ni modèle de processeur, ni utilisateur, ni chemin, ni date
|
||
// (contrainte 27). Code de sortie : 0 quand le relevé est écrit et que
|
||
// chaque verdict est tenu, 1 sinon.
|
||
import { spawnSync } from 'node:child_process';
|
||
import { readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||
import os from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { CONSTANTES } from '../../src/geometrie/constantes.js';
|
||
import { optionsBanc, projetBanc } from './projet.js';
|
||
import { accordRegistre, nombre, tableauConstantes, verifierReleve } from './releve.js';
|
||
|
||
/** La racine du projet, d'où se résolvent les chemins relatifs. */
|
||
export const RACINE = fileURLToPath(new URL('../../', import.meta.url));
|
||
|
||
/** Le relevé, relatif à la racine. */
|
||
export const CHEMIN_RELEVE = 'scripts/banc/releve.json';
|
||
|
||
// Le dossier des sorties du navigateur, que navigateur.js écrit.
|
||
const DOSSIER_SORTIES = 'scripts/banc/sorties';
|
||
|
||
/** Les sorties que les épreuves du banc écrivent, par nom de fichier sans
|
||
* extension : le harnais, puis chaque mesure. */
|
||
export const SORTIES_NAVIGATEUR = Object.freeze(['harnais', 'temoin', 'sieges_lisibles', 'temps_image', 'parallele', 'qualite']);
|
||
|
||
/** Les gardes du banc : des mesures hors du registre, qui ne proposent pas
|
||
* de constante mais disent si une hypothèse tient, par ordre de nom. */
|
||
export const GARDES = Object.freeze(['DUREE_GENERATION_PAR_DEFAUT', 'TEMPS_IMAGE_LISTES']);
|
||
|
||
/** Les mesures qu'un relevé porte : chaque constante du banc du registre,
|
||
* dans son ordre, puis les gardes. Une constante du banc ajoutée au
|
||
* registre est attendue d'office, et manque tant que le banc ne la relève
|
||
* pas. */
|
||
export const ATTENDUES = Object.freeze([...CONSTANTES.filter((c) => c.proprietaire === 'banc').map((c) => c.nom), ...GARDES]);
|
||
|
||
/** La police embarquée, sous laquelle se prend toute mesure de texte. */
|
||
export const FAMILLE_EMBARQUEE = 'Noto Sans';
|
||
|
||
// Les mesures qui lisent le rendu du texte, et nomment donc leur police.
|
||
const MESURES_DE_TEXTE = Object.freeze(['SEUIL_SIEGES_LISIBLES', 'TOLERANCE_CHAINE_TEMOIN']);
|
||
|
||
// La machine de référence du banc : le poste, son processeur ralenti ×4 par
|
||
// le navigateur. C'est le ralentissement de la configuration du relevé ;
|
||
// chaque mesure porte en plus celui sous lequel elle s'est prise, que cite
|
||
// la configuration de sa constante.
|
||
const RALENTISSEMENT_REFERENCE = 4;
|
||
|
||
// Les constantes du banc que la machine de référence relève sans les
|
||
// proposer : ρ y reste à son défaut de travail. Leur mesure s'imprime pour
|
||
// information, jamais « à adopter ».
|
||
const RELEVEES_SANS_ADOPTION = Object.freeze(['RALENTISSEMENT_PLEINE_CHARGE']);
|
||
|
||
// Les champs de sommet qu'une sortie peut porter en plus de ses mesures, et
|
||
// que le relevé reprend tels quels.
|
||
const CHAMPS_REPRIS = Object.freeze(['plafondAPriori', 'parallele']);
|
||
|
||
const cheminSortie = (nom) => `${DOSSIER_SORTIES}/${nom}.json`;
|
||
const estAbsent = (erreur) => erreur?.code === 'ENOENT';
|
||
|
||
/**
|
||
* La configuration du relevé : le cadre du projet banc au facteur 1, les
|
||
* versions du navigateur et de Node, le nombre des processeurs logiques, le
|
||
* ralentissement de la machine de référence, la police embarquée. Du
|
||
* système, seul availableParallelism se lit : ni nom d'hôte, ni modèle de
|
||
* processeur, ni utilisateur, ni dossier.
|
||
*
|
||
* @param {{systeme: Pick<typeof os, 'availableParallelism'>,
|
||
* node: string, navigateur: string|null}} options
|
||
* @returns {import('./releve.js').ConfigurationReleve}
|
||
*/
|
||
export function configurationDuBanc({ systeme, node, navigateur }) {
|
||
return {
|
||
cadre: { ...projetBanc().provide.cadreBanc },
|
||
facteurEchelle: 1,
|
||
navigateur,
|
||
node,
|
||
processeursLogiques: systeme.availableParallelism(),
|
||
ralentissementCpu: RALENTISSEMENT_REFERENCE,
|
||
police: FAMILLE_EMBARQUEE,
|
||
};
|
||
}
|
||
|
||
// Le rang d'une mesure dans le relevé : son rang au registre, puis, pour
|
||
// une garde, après toutes les constantes, par ordre de nom.
|
||
function rangeur(constantes) {
|
||
const rang = (nom) => {
|
||
const auRegistre = constantes.findIndex((c) => c.nom === nom);
|
||
return auRegistre >= 0 ? auRegistre : constantes.length;
|
||
};
|
||
return (a, b) => rang(a.nom) - rang(b.nom) || (a.nom < b.nom ? -1 : a.nom > b.nom ? 1 : 0);
|
||
}
|
||
|
||
/**
|
||
* Le relevé d'un passage : la configuration, les mesures de toutes les
|
||
* sorties — chaque attendue une fois, rangées dans l'ordre du registre reçu
|
||
* puis les gardes par ordre de nom —, et plafondAPriori et parallele quand
|
||
* une sortie les porte. Une mesure sans ralentissement propre reçoit celui
|
||
* de la configuration de sa sortie, quand celle-ci en porte un : en copie,
|
||
* les sorties reçues restent intactes. Les sorties se lisent dans l'ordre
|
||
* de la table ; une sortie sans mesures (le harnais) n'apporte rien.
|
||
*
|
||
* @param {{configuration: import('./releve.js').ConfigurationReleve,
|
||
* sorties: Map<string, {mesures?: object[], plafondAPriori?: object[], parallele?: object[]}>,
|
||
* attendues: ReadonlyArray<string>,
|
||
* constantes?: ReadonlyArray<import('../../src/geometrie/constantes.js').Constante>}} options
|
||
* @returns {import('./releve.js').Releve}
|
||
* @throws {TypeError} des sorties qui ne sont pas une Map
|
||
* @throws {Error} une mesure attendue absente, une mesure en double ou
|
||
* inattendue, un champ repris porté par deux sorties, ou un relevé que
|
||
* verifierReleve refuse — le message nomme chaque faute
|
||
*/
|
||
export function assembler({ configuration, sorties, attendues, constantes = CONSTANTES }) {
|
||
if (!(sorties instanceof Map)) throw new TypeError('sorties du banc : une Map attendue');
|
||
const mesures = [];
|
||
const repris = new Map();
|
||
for (const [nomSortie, sortie] of sorties) {
|
||
const ralentissementSortie = sortie.configuration?.ralentissementCpu;
|
||
for (const mesure of sortie.mesures ?? []) {
|
||
if (!attendues.includes(mesure.nom)) throw new Error(`mesure inattendue : ${mesure.nom} (sortie ${nomSortie})`);
|
||
if (mesures.some((m) => m.nom === mesure.nom)) throw new Error(`mesure en double : ${mesure.nom} (sortie ${nomSortie})`);
|
||
const herite = ralentissementSortie !== undefined && !Object.hasOwn(mesure, 'ralentissementCpu');
|
||
mesures.push(herite ? { ...mesure, ralentissementCpu: ralentissementSortie } : mesure);
|
||
}
|
||
for (const champ of CHAMPS_REPRIS) {
|
||
if (!Object.hasOwn(sortie, champ)) continue;
|
||
if (repris.has(champ)) throw new Error(`${champ} : porté par deux sorties, ${repris.get(champ).sortie} et ${nomSortie}`);
|
||
repris.set(champ, { sortie: nomSortie, valeur: sortie[champ] });
|
||
}
|
||
}
|
||
const absentes = attendues.filter((nom) => !mesures.some((m) => m.nom === nom));
|
||
if (absentes.length > 0) throw new Error(`mesure attendue absente : ${absentes.join(', ')}`);
|
||
const releve = { format: 1, configuration, mesures: [...mesures].sort(rangeur(constantes)) };
|
||
for (const champ of CHAMPS_REPRIS) if (repris.has(champ)) releve[champ] = repris.get(champ).valeur;
|
||
const fautes = verifierReleve(releve, constantes);
|
||
if (fautes.length > 0) throw new Error(`relevé refusé : ${fautes.map(({ chemin, faute }) => `${chemin} : ${faute}`).join(' ; ')}`);
|
||
return releve;
|
||
}
|
||
|
||
/**
|
||
* Le jugement d'un relevé assemblé. bloquants : chaque mesure de texte
|
||
* prise sous une autre police que la police embarquée, ou qui n'en nomme
|
||
* aucune — le relevé ne s'écrit pas. nonTenus : chaque mesure au verdict
|
||
* « nonTenu », avec son critère — le relevé s'écrit, la sortie est non
|
||
* nulle. Dans l'ordre des mesures.
|
||
*
|
||
* @param {import('./releve.js').Releve} releve
|
||
* @returns {{bloquants: string[], nonTenus: string[]}}
|
||
*/
|
||
export function jugement(releve) {
|
||
const bloquants = [];
|
||
const nonTenus = [];
|
||
for (const mesure of releve.mesures) {
|
||
if (MESURES_DE_TEXTE.includes(mesure.nom) && mesure.police !== FAMILLE_EMBARQUEE) {
|
||
bloquants.push(`${mesure.nom} mesurée sous la police « ${mesure.police ?? 'inconnue'} », et non sous ${FAMILLE_EMBARQUEE}`);
|
||
}
|
||
if (mesure.verdict === 'nonTenu') nonTenus.push(`${mesure.nom} : verdict non tenu — ${mesure.critere}`);
|
||
}
|
||
return { bloquants, nonTenus };
|
||
}
|
||
|
||
// Le relevé committé, lu sur le disque.
|
||
const releveCommite = (racine = RACINE) => JSON.parse(readFileSync(join(racine, CHEMIN_RELEVE), 'utf8'));
|
||
|
||
/**
|
||
* Le tableau des constantes (§ 19.8, § 19.10) : tableauConstantes sur le
|
||
* registre et un relevé, par défaut le relevé committé. Une ligne par
|
||
* constante, sous un en-tête et son séparateur.
|
||
*
|
||
* @param {{releve?: import('./releve.js').Releve,
|
||
* constantes?: ReadonlyArray<import('../../src/geometrie/constantes.js').Constante>}} [options]
|
||
* @returns {string}
|
||
*/
|
||
export function regionConstantes({ releve = releveCommite(), constantes = CONSTANTES } = {}) {
|
||
return tableauConstantes(constantes, releve);
|
||
}
|
||
|
||
// Une valeur et son unité, à la française.
|
||
const avecUnite = (valeur, unite) => (valeur === null ? 'aucune valeur' : `${nombre(valeur)} ${unite}`);
|
||
|
||
// Ce qu'un passage imprime après l'écriture du relevé : le tableau, chaque
|
||
// constante du banc face à sa valeur proposée — égale, à adopter, ou
|
||
// relevée sans adoption —, chaque garde, puis l'accord du registre avec le
|
||
// relevé.
|
||
function imprimer(releve, sortie) {
|
||
sortie('');
|
||
sortie(regionConstantes({ releve }));
|
||
sortie('');
|
||
sortie('Valeurs proposées par le banc (à adopter à la main dans src/geometrie/constantes.js) :');
|
||
for (const constante of CONSTANTES) {
|
||
if (constante.proprietaire !== 'banc') continue;
|
||
const mesure = releve.mesures.find((m) => m.nom === constante.nom);
|
||
let suite = mesure.valeurProposee === constante.valeur ? 'égale' : 'à adopter';
|
||
if (RELEVEES_SANS_ADOPTION.includes(constante.nom)) suite = 'relevé, non adopté : défaut de travail sous la machine de référence';
|
||
sortie(
|
||
`- ${constante.nom} : registre ${avecUnite(constante.valeur, constante.unite)}, ` +
|
||
`relevé ${avecUnite(mesure.valeurProposee, constante.unite)} — ${suite}`,
|
||
);
|
||
}
|
||
sortie('Gardes :');
|
||
for (const mesure of releve.mesures) {
|
||
if (!GARDES.includes(mesure.nom)) continue;
|
||
const verdict = mesure.verdict === undefined ? '' : `, verdict ${mesure.verdict === 'tenu' ? 'tenu' : 'non tenu'}`;
|
||
sortie(`- ${mesure.nom} : ${avecUnite(mesure.valeurProposee, mesure.unite)}${verdict}`);
|
||
}
|
||
const ecarts = accordRegistre(CONSTANTES, releve);
|
||
if (ecarts.length === 0) sortie('Accord du registre avec le relevé : aucun écart.');
|
||
else {
|
||
sortie('Accord du registre avec le relevé :');
|
||
for (const { nom, faute } of ecarts) sortie(`- ${nom} : ${faute}`);
|
||
}
|
||
sortie('La région « constantes » de README.md se met à jour par node scripts/documentation.js engendrer.');
|
||
}
|
||
|
||
// Le relevé du plafond a priori, par le script lui-même : son JSON imprimé.
|
||
function plafondParScript(racine) {
|
||
const resultat = spawnSync(process.execPath, [join(racine, 'scripts/banc/plafond.js')], {
|
||
cwd: racine,
|
||
encoding: 'utf8',
|
||
stdio: ['ignore', 'pipe', 'inherit'],
|
||
});
|
||
if (resultat.status !== 0) throw new Error(`plafond.js : code ${resultat.status}`);
|
||
return JSON.parse(resultat.stdout);
|
||
}
|
||
|
||
// Le projet banc de Vitest, sorties à l'écran ; rend son code de sortie.
|
||
function vitestDuBanc(racine) {
|
||
const resultat = spawnSync(process.execPath, [join(racine, 'node_modules/vitest/vitest.mjs'), 'run', '--config', 'vitest.banc.config.js'], {
|
||
cwd: racine,
|
||
stdio: 'inherit',
|
||
});
|
||
return resultat.status ?? 1;
|
||
}
|
||
|
||
// La version du Chromium du banc, lancé par Playwright sous les options du
|
||
// projet banc : « Chromium <version> ».
|
||
async function versionNavigateur() {
|
||
const { chromium } = await import('playwright');
|
||
const { launchOptions = {} } = optionsBanc(process.env);
|
||
const navigateur = await chromium.launch({ ...launchOptions, headless: true });
|
||
try {
|
||
return `Chromium ${navigateur.version()}`;
|
||
} finally {
|
||
await navigateur.close();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Un passage du banc, selon le déroulé de l'en-tête. Chaque effet se reçoit,
|
||
* pour que les épreuves le remplacent ; par défaut, ceux de la machine.
|
||
*
|
||
* @param {{racine?: string, mesurerPlafond?: () => object[], lancerVitest?: () => number,
|
||
* lireNavigateur?: () => Promise<string>, systeme?: typeof os, node?: string,
|
||
* effacer?: (chemin: string) => void, lire?: (chemin: string) => string,
|
||
* ecrire?: (chemin: string, texte: string) => void, sortie?: (ligne: string) => void}} [effets]
|
||
* @returns {Promise<0|1>}
|
||
*/
|
||
export async function executer({
|
||
racine = RACINE,
|
||
mesurerPlafond = () => plafondParScript(racine),
|
||
lancerVitest = () => vitestDuBanc(racine),
|
||
lireNavigateur = versionNavigateur,
|
||
systeme = os,
|
||
node = process.version,
|
||
effacer = (chemin) => rmSync(join(racine, chemin), { force: true }),
|
||
lire = (chemin) => readFileSync(join(racine, chemin), 'utf8'),
|
||
ecrire = (chemin, texte) => writeFileSync(join(racine, chemin), texte),
|
||
sortie = (ligne) => process.stdout.write(`${ligne}\n`),
|
||
} = {}) {
|
||
for (const nom of SORTIES_NAVIGATEUR) effacer(cheminSortie(nom));
|
||
let releve;
|
||
try {
|
||
const plafondAPriori = mesurerPlafond();
|
||
const code = lancerVitest();
|
||
if (code !== 0) {
|
||
sortie(`Une épreuve du banc est tombée (code ${code}) : un contrôle du harnais, ou une mesure refusée. Aucun relevé écrit.`);
|
||
return 1;
|
||
}
|
||
const sorties = new Map([['plafond', { plafondAPriori }]]);
|
||
for (const nom of SORTIES_NAVIGATEUR) {
|
||
let texte;
|
||
try {
|
||
texte = lire(cheminSortie(nom));
|
||
} catch (erreur) {
|
||
if (estAbsent(erreur)) continue;
|
||
throw erreur;
|
||
}
|
||
sorties.set(nom, JSON.parse(texte));
|
||
}
|
||
const configuration = configurationDuBanc({ systeme, node, navigateur: await lireNavigateur() });
|
||
releve = assembler({ configuration, sorties, attendues: ATTENDUES });
|
||
} catch (erreur) {
|
||
sortie(`Aucun relevé écrit : ${erreur.message}`);
|
||
return 1;
|
||
}
|
||
const { bloquants, nonTenus } = jugement(releve);
|
||
if (bloquants.length > 0) {
|
||
for (const motif of bloquants) sortie(motif);
|
||
sortie('Aucun relevé écrit : une mesure de texte hors de la police embarquée ne mesure pas le logiciel.');
|
||
return 1;
|
||
}
|
||
ecrire(CHEMIN_RELEVE, `${JSON.stringify(releve, null, 2)}\n`);
|
||
sortie(`Relevé écrit : ${CHEMIN_RELEVE}`);
|
||
imprimer(releve, sortie);
|
||
for (const motif of nonTenus) sortie(motif);
|
||
return nonTenus.length > 0 ? 1 : 0;
|
||
}
|
||
|
||
// Vrai quand ce fichier est le script que Node a lancé, et non un module
|
||
// importé.
|
||
function estLanceDirectement() {
|
||
if (process.argv[1] === undefined) return false;
|
||
try {
|
||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (estLanceDirectement()) process.exitCode = await executer();
|