[ADD] engine: end-to-end runs on the demonstrations, measured thresholds
The whole chain — search, both verifiers, measure, ceilings, ranking —
runs end to end on the demonstrations. At 500 000 moves the large one
reaches its proven floor: largest gap 1, no collision, no repeat. Its
quality thresholds were set after measurement, with the measurement in
the comment, never guessed.
Checked: small at 8 for all and certified; conflict variant spreads its 4
collisions over 4 pairs; large at gap 1 with 99 imposed returns; the
ranking holds when its order differs from id order.
--- FR ---
[ADD] moteur : la chaîne de bout en bout, seuils posés après mesure
Toute la chaîne — recherche, les deux vérificateurs, mesure, plafonds,
classement — tourne de bout en bout sur les démonstrations. À 500 000
mouvements, la grande atteint son plancher prouvé : écart maximal 1,
aucune collision, aucune répétition. Ses seuils de qualité sont posés
après mesure, la mesure en commentaire, jamais de tête.
Vérifié : petite à 8 pour tous et certifiée ; variante en conflit à 4
collisions sur 4 paires ; grande à l'écart 1 avec 99 retours imposés ; le
classement tient quand son ordre diffère de celui des identifiants.
Assisted-by: Claude Opus 5.5
2026-10-05 22:52:30 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuve d'intégration du moteur, de la configuration au classement, sur la
// petite démonstration et sa variante conflit (§ 5.5, § 5.6, § 5.7,
// § 15.3). Chaque proposition de rechercher traverse la chaîne entière :
// verifierInvariants juge son plan ; mesurer et plafondsRealises le
// mesurent ; verifierIndicateurs éprouve la ligne de chacun contre le
// plafond a priori ; classer ordonne les propositions, et minimumAtteint lit
// le certificat de la première ; diagnostiquer fournit les planchers que la
// meilleure respecte. Chaque quantité vient de son module (§ 13.2) :
// l'épreuve n'en recalcule aucune.
import assert from 'node:assert/strict' ;
import { describe , test } from '../../test/lanceur.js' ;
import { CATALOGUE , PLAN _PARFAIT _PETITE } from '../demo/catalogue.js' ;
import { CRITERES , classer } from './classement.js' ;
import { indexerPlan , normaliser } from './configuration.js' ;
import { diagnostiquer } from './diagnostic.js' ;
import { mesurer } from './indicateurs.js' ;
import { minimumAtteint } from './manque.js' ;
import { plafondsAPriori , plafondsRealises } from './plafond.js' ;
import { rechercher } from './recherche.js' ;
import { verifierIndicateurs , verifierInvariants } from './verification.js' ;
// Réglages de la génération : une graine fixée, écrite ici (§ 15.5, point 2).
[ADD] tests: budget reporter, one import parser, key-order guard
The time budget of § 14.14 had no enforcement: a reporter now prints the
total and the ten slowest tests, and fails a run whose node project
exceeds 30 s or node-long 60 s. Two guards parsed imports with two
patterns, one blind past an apostrophe in a comment; both now use one
parser anchored on « from ». The determinism guard refuses key-order
walks, and one test binds the application name across its three
declarations. The end-to-end generations drop to 100 000 moves.
Checked: each ceiling, the apostrophe case and a key walk are shown
failing before the guard that catches them.
--- FR ---
[ADD] épreuves : rapporteur de budget, imports, ordre des clés
Le budget de temps du § 14.14 n'avait aucune garde : un rapporteur
imprime désormais la durée totale et les dix épreuves les plus lentes,
et fait échouer une exécution dont le projet node dépasse 30 s ou
node-long 60 s. Deux gardes lisaient les imports par deux motifs, l'un
aveugle après une apostrophe en commentaire ; les deux passent par un
seul analyseur ancré sur « from ». La garde de déterminisme refuse les
parcours de clés, et une épreuve lie le nom de l'application à ses
trois déclarations. Les générations de bout en bout passent à 100 000.
Vérifié : chaque plafond, le cas de l'apostrophe et un parcours de clés
sont montrés en échec avant la garde qui les attrape.
Assisted-by: Claude Opus 5.5
2026-10-06 04:23:13 -04:00
// À 100 000 mouvements, avec cette graine, les trois propositions de chaque
// démonstration rencontrent 8 pour les douze, sans rencontre répétée, et ne
// portent que les collisions que le diagnostic impose : aucune dans la
// petite démonstration, 4 dans sa variante. Une proposition de la variante
// garde 15 retours choisis, les cinq autres aucun. Les assertions des
// épreuves tiennent avec cette graine dès 80 000 mouvements, soit une marge
// de 1,25 ; en deçà, les trois propositions de la petite démonstration
// gardent des retours choisis. La marge est celle de cette graine : sur les
// graines 1 à 64, les assertions tiennent pour 43 à 100 000, pour toutes à
// 200 000.
//
// Le compte pèse sur la relance sous surveillance (§ 14.14) : toute
// modification du moteur, ou du générateur que lit le catalogue, rejoue ce
// fichier. Sur deux cœurs, la relance qui suit une modification de
// recherche.js tient en 1,6 s au mur, contre 2,0 s à 200 000 mouvements ;
// celle qui suit indicateurs.js, qui rejoue sept fichiers, prend 2,6 s,
// contre 2,9 s.
const REGLAGES = Object . freeze ( { graine : 314_159 , arret : 100_000 , nombre : 3 } ) ;
[ADD] engine: end-to-end runs on the demonstrations, measured thresholds
The whole chain — search, both verifiers, measure, ceilings, ranking —
runs end to end on the demonstrations. At 500 000 moves the large one
reaches its proven floor: largest gap 1, no collision, no repeat. Its
quality thresholds were set after measurement, with the measurement in
the comment, never guessed.
Checked: small at 8 for all and certified; conflict variant spreads its 4
collisions over 4 pairs; large at gap 1 with 99 imposed returns; the
ranking holds when its order differs from id order.
--- FR ---
[ADD] moteur : la chaîne de bout en bout, seuils posés après mesure
Toute la chaîne — recherche, les deux vérificateurs, mesure, plafonds,
classement — tourne de bout en bout sur les démonstrations. À 500 000
mouvements, la grande atteint son plancher prouvé : écart maximal 1,
aucune collision, aucune répétition. Ses seuils de qualité sont posés
après mesure, la mesure en commentaire, jamais de tête.
Vérifié : petite à 8 pour tous et certifiée ; variante en conflit à 4
collisions sur 4 paires ; grande à l'écart 1 avec 99 retours imposés ; le
classement tient quand son ordre diffère de celui des identifiants.
Assisted-by: Claude Opus 5.5
2026-10-05 22:52:30 -04:00
// La même génération, arrêtée au premier mouvement. Aucune de ses
// propositions n'atteint son plafond a priori : avec cette graine, leurs
// écarts au plafond a priori maximaux valent 2, 3 et 3, et sur les graines
// 1 à 64, aucune proposition brève ne l'atteint non plus.
const REGLAGES _BREVES = Object . freeze ( { ... REGLAGES , arret : 1 } ) ;
// Configuration neuve d'une démonstration du catalogue.
const demo = ( cle ) => CATALOGUE . find ( ( entree ) => entree . cle === cle ) . construire ( ) ;
// Génère les propositions d'une configuration et les fait traverser la
// chaîne. Chaque plan est jugé par verifierInvariants avant d'être indexé :
// indexerPlan n'examine ni capacités ni réservations et lève à la première
// faute de forme, là où le vérificateur les nomme toutes. Vient ensuite la
// ligne N − 1 ≥ a priori ≥ réalisé ≥ rencontres de chacun. decalage
// s'ajoute aux identifiants que rend rechercher, comme l'appelant qui
// accumule des générations donne à chaque proposition un identifiant unique
// (§ 5.7). Rend le plafond a priori de chacun, et les propositions dans
// l'ordre de leurs identifiants, avec leurs mesures.
function generer ( configuration , etiquette , reglages , decalage = 0 ) {
const instance = normaliser ( configuration ) ;
const aPriori = plafondsAPriori ( instance ) ;
const propositions = rechercher ( configuration , reglages ) . map ( ( { id , plan } ) => {
const nom = ` ${ etiquette } , proposition ${ id + decalage } ` ;
assert . deepEqual ( verifierInvariants ( instance , plan ) , [ ] , nom ) ;
const tableDe = indexerPlan ( instance , plan ) ;
const mesures = mesurer ( instance , tableDe ) ;
const realises = plafondsRealises ( instance , tableDe ) ;
assert . deepEqual ( verifierIndicateurs ( mesures , aPriori , realises ) , [ ] , nom ) ;
return { id : id + decalage , mesures } ;
} ) ;
// Un parcours vide ne prouverait rien (§ 14.2).
assert . equal ( propositions . length , reglages . nombre , etiquette ) ;
return { aPriori , propositions } ;
}
// Le classement de propositions mesurées sur une même configuration, et
// celle qu'il met en tête.
function classerPropositions ( { aPriori , propositions } ) {
const classement = classer (
propositions . map ( ( { id , mesures } ) => ( { id , mesures , plafondsAPriori : aPriori } ) ) ,
) ;
const premiere = propositions . find ( ( { id } ) => id === classement . ordre [ 0 ] ) ;
return { classement , premiere } ;
}
// La génération de la variante conflit aux réglages de l'épreuve, calculée
// au premier appel puis partagée par les deux épreuves qui la lisent : la
// recherche en est le coût.
let variante = null ;
function genererVariante ( ) {
variante ? ? = generer ( demo ( 'petite-conflit' ) , 'petite-conflit' , REGLAGES ) ;
return variante ;
}
// Le plan écrit du § 15.3 fait visiter les quatre tables à chacun : aucun
// retour choisi. Le classement ne lit pas les retours (§ 5.7) : entre
// propositions égales sur ses cinq critères, l'identifiant désigne la
// première, qu'elle garde des retours ou non. L'épreuve exige donc le zéro
// d'une proposition au moins, la preuve que la recherche tient compte de
// nouvelleTable.
function exigerUneSansRetourChoisi ( etiquette , propositions ) {
const retours = propositions . map ( ( { mesures } ) => mesures . totalRetoursChoisis ) ;
assert . ok ( retours . includes ( 0 ) , ` ${ etiquette } : retours choisis par proposition ${ retours . join ( ', ' ) } ` ) ;
}
describe ( 'intégration : de la configuration au classement (§ 5.5, § 5.7, § 15.3)' , ( ) => {
test ( 'petite : chaque proposition tient invariants et indicateurs et ne porte aucune collision, une au moins aucun retour choisi ; la première du classement porte « minimum atteint »' , ( ) => {
const generation = generer ( demo ( 'petite' ) , 'petite' , REGLAGES ) ;
const { classement , premiere } = classerPropositions ( generation ) ;
// Le plafond a priori est connu : aucun critère n'est sauté, et le
// certificat se lit contre lui (§ 5.5).
assert . deepEqual ( classement . criteresAppliques , [ ... CRITERES ] ) ;
assert . equal ( classement . critereSaute , null ) ;
assert . equal ( minimumAtteint ( premiere . mesures , generation . aPriori ) , true ) ;
// Le certificat ne lit que les rencontres : il se décerne aussi à un plan
// qui réunit des collègues. Chaque appartenance de quatre membres peut
// se répartir sur les quatre tables à chaque tour, comme dans le plan
// écrit du § 15.3 : aucune proposition ne réunit de collègues, et le
// compteur de collisions se mesure à zéro (§ 15.3).
for ( const { id , mesures } of generation . propositions ) {
assert . equal ( mesures . collisionsCumulees , 0 , ` proposition ${ id } ` ) ;
}
exigerUneSansRetourChoisi ( 'petite' , generation . propositions ) ;
} ) ;
test ( 'petite-conflit : chaque proposition porte les 4 collisions du plancher du diagnostic, une au moins aucun retour choisi ; la meilleure respecte les planchers, rencontre 8 pour les douze, sur 4 paires, excédent 0' , ( ) => {
const configuration = demo ( 'petite-conflit' ) ;
// Le groupe de cinq ne se répartit pas sur quatre tables : au moins une
// collision par tour, 4 cumulées sur les 4 tours, portées par une paire
// distincte au moins (§ 15.3). Le diagnostic les chiffre ; une liste vide
// laisserait la boucle ci-dessous sans rien éprouver.
const { collisions } = diagnostiquer ( configuration ) ;
assert . deepEqual (
collisions . map ( ( { plancherCumulees , plancherPairesDistinctes } ) => [
plancherCumulees ,
plancherPairesDistinctes ,
] ) ,
[ [ 4 , 1 ] ] ,
) ;
const generation = genererVariante ( ) ;
const { premiere } = classerPropositions ( generation ) ;
// Tout plan valide respecte ces planchers : ils éprouvent l'accord du
// diagnostic et de la mesure, non la recherche.
for ( const plancher of collisions ) {
const mesure = premiere . mesures . parGroupe . find ( ( { groupe } ) => groupe === plancher . groupe ) ;
assert . ok (
mesure . collisionsCumulees >= plancher . plancherCumulees ,
` ${ plancher . groupe } : ${ mesure . collisionsCumulees } collisions cumulées, plancher ${ plancher . plancherCumulees } ` ,
) ;
assert . ok (
mesure . pairesDistinctes >= plancher . plancherPairesDistinctes ,
` ${ plancher . groupe } : ${ mesure . pairesDistinctes } paires distinctes, plancher ${ plancher . plancherPairesDistinctes } ` ,
) ;
}
assert . deepEqual ( premiere . mesures . rencontres , Array ( 12 ) . fill ( 8 ) ) ;
// La recherche se juge à ses collisions : chaque proposition atteint le
// plancher des collisions cumulées sans le dépasser. La meilleure les
// porte sur 4 paires distinctes, comme le plan écrit du § 15.3 (épreuve
// suivante).
for ( const { id , mesures } of generation . propositions ) {
assert . equal ( mesures . collisionsCumulees , collisions [ 0 ] . plancherCumulees , ` proposition ${ id } ` ) ;
}
assert . equal ( premiere . mesures . pairesDistinctes , 4 ) ;
// Deux voisins par tour, quatre tours : qui rencontre 8 personnes n'en
// revoit aucune. Chaque paire se réunit donc une fois au plus, et
// l'excédent 0 découle de 8 pour les douze. Il éprouve mesurer, non la
// recherche.
assert . equal ( premiere . mesures . excedentCollisions , 0 ) ;
exigerUneSansRetourChoisi ( 'petite-conflit' , generation . propositions ) ;
} ) ;
test ( 'le plan écrit du § 15.3, mesuré sur la variante conflit : 8 pour les douze, 4 collisions sur 4 paires, excédent 0' , ( ) => {
// Le témoin de l'épreuve précédente : la personne passée de C à A
// retrouve un membre de A à chacun des quatre tours, chaque fois un
// autre. Ce que l'épreuve exige de la recherche est donc atteignable.
const instance = normaliser ( demo ( 'petite-conflit' ) ) ;
const mesures = mesurer ( instance , indexerPlan ( instance , PLAN _PARFAIT _PETITE ) ) ;
assert . deepEqual ( mesures . rencontres , Array ( 12 ) . fill ( 8 ) ) ;
assert . equal ( mesures . collisionsCumulees , 4 ) ;
assert . equal ( mesures . pairesDistinctes , 4 ) ;
assert . equal ( mesures . excedentCollisions , 0 ) ;
} ) ;
test ( 'petite-conflit, deux générations accumulées : classer met la plus longue en tête, numérotée après la brève, et départage ses propositions égales par identifiant croissant (§ 5.7, § 15.5)' , ( ) => {
// La génération brève prend les identifiants 1 à 3, la longue 4 à 6 :
// l'ordre attendu contredit l'ordre des identifiants, et seul un
// classement qui applique ses critères peut le rendre.
const breve = generer ( demo ( 'petite-conflit' ) , 'petite-conflit, génération brève' , REGLAGES _BREVES ) ;
const variante = genererVariante ( ) ;
const longue = {
aPriori : variante . aPriori ,
propositions : variante . propositions . map ( ( { id , mesures } ) => ( { id : id + REGLAGES . nombre , mesures } ) ) ,
} ;
// Ce que l'ordre départage, mesuré avant d'être lu. Chaque proposition
// longue atteint son plafond a priori, et les trois sont égales sur les
// quatre autres critères du classement ; aucune brève n'atteint le sien.
// Sur une même population, la redondance moyenne ordonne comme la somme
// que lit le classement.
const autresCriteres = ( { mesures } ) => [
mesures . excedentCollisions ,
mesures . collisionsCumulees ,
mesures . rencontresRepetees . choisies ,
mesures . aggRedondance . tous . moyenne ,
] ;
for ( const proposition of longue . propositions ) {
const nom = ` proposition ${ proposition . id } ` ;
assert . equal ( minimumAtteint ( proposition . mesures , longue . aPriori ) , true , nom ) ;
assert . deepEqual ( autresCriteres ( proposition ) , autresCriteres ( longue . propositions [ 0 ] ) , nom ) ;
}
for ( const { id , mesures } of breve . propositions ) {
assert . equal ( minimumAtteint ( mesures , breve . aPriori ) , false , ` proposition ${ id } ` ) ;
}
// Le premier critère renvoie les brèves derrière les longues ; les
// longues, égales sur les cinq, se départagent par identifiant croissant
// (§ 15.5, point 4).
const { classement } = classerPropositions ( {
aPriori : longue . aPriori ,
propositions : [ ... breve . propositions , ... longue . propositions ] ,
} ) ;
assert . deepEqual ( classement . ordre . slice ( 0 , REGLAGES . nombre ) , [ 4 , 5 , 6 ] ) ;
assert . deepEqual ( [ ... classement . ordre . slice ( REGLAGES . nombre ) ] . sort ( ( a , b ) => a - b ) , [ 1 , 2 , 3 ] ) ;
} ) ;
} ) ;