diff --git a/spec.md b/spec.md index 71637ff..d590c6b 100644 --- a/spec.md +++ b/spec.md @@ -3494,6 +3494,12 @@ couverture : | relance sous surveillance, après une modification | **2 secondes** | le développeur commence à grouper ses modifications, et le test cesse de désigner laquelle a cassé | | série `node` surveillée complète, à froid | **10 secondes** | le développeur change de fenêtre ; le retour de contexte coûte plus que l'épreuve, et le niveau glisse vers « avant un commit » | +**Le budget se mesure sur une machine de développement d'au moins quatre +cœurs.** Sur deux, le lanceur ne porte la série que dans un seul processus : +la relance qui suit la modification d'un module de base — celui que presque +toutes les épreuves importent — dépasse deux secondes, et ce dépassement y est +accepté. Le budget à froid de dix secondes, lui, y tient. + **Pourquoi ces deux valeurs.** En deçà de deux secondes, l'attention reste sur le code et le résultat se lit comme la suite du geste. Au-delà de dix, elle part ailleurs ; le niveau `node` cesse d'être lancé à chaque modification, et les diff --git a/src/moteur/configuration.js b/src/moteur/configuration.js index 3f7a854..1496bb2 100644 --- a/src/moteur/configuration.js +++ b/src/moteur/configuration.js @@ -70,7 +70,10 @@ const NOMS_CONTRAINTES = [ // Valeur citée dans un message : une chaîne s'écrit entre guillemets, pour // ne pas se lire comme le nombre qu'elle contient. -const decrire = (valeur) => (typeof valeur === 'string' ? JSON.stringify(valeur) : String(valeur)); +// Valeur citée dans un message : une chaîne entre guillemets, pour qu'elle +// ne se lise pas comme le nombre qu'elle contient ; toute autre valeur telle +// quelle. +export const decrire = (valeur) => (typeof valeur === 'string' ? JSON.stringify(valeur) : String(valeur)); function exigerListe(valeur, nom) { if (!Array.isArray(valeur)) throw new TypeError(`configuration.${nom} : liste attendue`); diff --git a/src/moteur/integration.long.test.js b/src/moteur/integration.long.test.js index 418ca14..9e59455 100644 --- a/src/moteur/integration.long.test.js +++ b/src/moteur/integration.long.test.js @@ -222,9 +222,8 @@ describe('intégration : la grande démonstration de bout en bout (§ 5.5, § 12 // configurations hors de ce cadre, d'une graine écrite ici. Les propriétés // éprouvées valent pour tout plan que rend la recherche, quelle qu'en soit // la qualité : un compte d'arrêt bref suffit. Un historique court fait jouer -// la bascule et la relance de la descente : avec cette graine, 86 des 460 -// propositions basculent dans l'ordre des contraintes, et 74 relancent leur -// descente au moins une fois. +// la bascule de la descente dans l'ordre des contraintes, qu'un plancher +// compte ci-dessous. const GRILLE = Object.freeze({ graine: 1_729, configurations: 300, arret: 3_000, nombre: 2 }); // Une salle sur cinq est trop petite, deux sont tendues, deux ont des places @@ -404,7 +403,7 @@ function juger(nom, { configuration, instance, aPriori, diagnostic }, propositio const violations = verifierInvariants(instance, plan); if (violations.length > 0) { fautes.push(`${nom} : ${JSON.stringify(violations)}`); - return; + return false; } const tableDe = indexerPlan(instance, plan); const mesures = mesurer(instance, tableDe); @@ -440,6 +439,9 @@ function juger(nom, { configuration, instance, aPriori, diagnostic }, propositio if (JSON.stringify(regenerer(configuration, { graine, arret, historique })) !== JSON.stringify(plan)) { fautes.push(`${nom} : la régénération rend un autre plan`); } + // Une proposition a basculé dans l'ordre des contraintes exactement quand + // son plan atteint un écart au plafond a priori maximal nul. + return Math.max(...ecartsAuPlafondAPriori(mesures, aPriori)) === 0; } // Planchers de la grille (§ 14.2) : ce que la boucle doit avoir éprouvé pour @@ -465,6 +467,7 @@ const PLANCHERS = Object.freeze([ ["avec plancher d'excédent", 'plancherExcedent', 10], // 15 ["avec plancher d'écart d'itinéraire", 'plancherEcartItineraire', 35], // 50 ['marches au dernier palier', 'marches', 400], // 460, une par proposition + ["propositions qui basculent dans l'ordre des contraintes", 'bascules', 60], // 86 ]); describe('intégration : grille de propriétés sur des configurations tirées (§ 14.12)', () => { @@ -484,6 +487,7 @@ describe('intégration : grille de propriétés sur des configurations tirées ( plancherExcedent: 0, plancherEcartItineraire: 0, marches: 0, + bascules: 0, }; const fautes = []; for (let c = 1; c <= GRILLE.configurations; c += 1) { @@ -525,7 +529,7 @@ describe('intégration : grille de propriétés sur des configurations tirées ( if (propositions.length !== GRILLE.nombre) fautes.push(`${nom} : ${propositions.length} propositions`); for (const proposition of propositions) { const nomProposition = `${nom}, proposition ${proposition.id}`; - juger(nomProposition, contexte, proposition, fautes); + if (juger(nomProposition, contexte, proposition, fautes)) comptes.bascules += 1; if (marcher(nomProposition, instance, proposition.graine, fautes)) comptes.marches += 1; } } diff --git a/src/moteur/integration.test.js b/src/moteur/integration.test.js index 9c7abee..b62b5e9 100644 --- a/src/moteur/integration.test.js +++ b/src/moteur/integration.test.js @@ -36,10 +36,7 @@ import { verifierIndicateurs, verifierInvariants } from './verification.js'; // // 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. +// fichier. const REGLAGES = Object.freeze({ graine: 314_159, arret: 100_000, nombre: 3 }); // La même génération, arrêtée au premier mouvement. Aucune de ses diff --git a/src/moteur/plafond.js b/src/moteur/plafond.js index d2236bf..7fae2f9 100644 --- a/src/moteur/plafond.js +++ b/src/moteur/plafond.js @@ -42,7 +42,7 @@ // Une case d'itinéraire en réserve vaut RESERVE, et RESERVE vaut LIBRE // (configuration.js) : une rangée de instance.fixe se lit comme l'itinéraire // qui laisse en réserve chaque tour libre. -import { LIBRE, RESERVE, STATUT, exigerPlanIndexe } from './configuration.js'; +import { LIBRE, RESERVE, STATUT, decrire, exigerPlanIndexe } from './configuration.js'; import { ErreurConfiguration } from './erreurs.js'; // Nombre de multiensembles au-delà duquel l'énumération refuse une @@ -107,7 +107,7 @@ function exigerCases(itineraire, T) { for (let i = 0; i < itineraire.length; i += 1) { const t = itineraire[i]; if (t !== RESERVE && !(Number.isInteger(t) && t >= 0 && t < T)) { - throw new RangeError(`itinéraire, case ${i} : index de table ${String(t)} hors de −1..${T - 1}`); + throw new RangeError(`itinéraire, case ${i} : index de table ${decrire(t)} hors de −1..${T - 1}`); } } } diff --git a/src/moteur/plafond.test.js b/src/moteur/plafond.test.js index d942a8d..152a682 100644 --- a/src/moteur/plafond.test.js +++ b/src/moteur/plafond.test.js @@ -480,6 +480,14 @@ describe('plafonds : valeurs exactes (§ 5.5, § 15)', () => { assert.throws(() => planDepuisIndex(instance, fautif), attendu); assert.throws(() => plafondsRealises(instance, fautif), attendu); }); + + test('un itinéraire hors forme cite la valeur comme la garde du plan indexé : la chaîne "-1" entre guillemets', () => { + const { instance } = forme({ capacites: [3, 3], ancres: [0, 0], autres: 4, tours: 2 }); + assert.throws(() => plafondItineraire(instance, 0, [0, '-1']), { + name: 'RangeError', + message: 'itinéraire, case 1 : index de table "-1" hors de −1..1', + }); + }); }); describe("plafond a priori : la programmation dynamique contre l'énumération (§ 14.11, § 14.12)", () => { diff --git a/src/moteur/recherche.js b/src/moteur/recherche.js index cf436ec..7f38282 100644 --- a/src/moteur/recherche.js +++ b/src/moteur/recherche.js @@ -315,7 +315,7 @@ function modifierPaire(etat, a, b, sens) { etat.paires[k] = apres; const ga = etat.groupe[a]; const gb = etat.groupe[b]; - const memeGroupe = ga === gb && etat.affilie[a] === 1; + const memeGroupe = ga === gb && etat.porteAppartenance[a] === 1; if (memeGroupe) etat.collisionsCumulees += sens; if ((sens > 0 ? avant : apres) !== 0) { etat.excedentPaires += sens; @@ -324,8 +324,8 @@ function modifierPaire(etat, a, b, sens) { } changerRencontres(etat, a, sens); changerRencontres(etat, b, sens); - if (etat.affilie[b] === 1) changerAffilie(etat, a, gb, sens); - if (etat.affilie[a] === 1) changerAffilie(etat, b, ga, sens); + if (etat.porteAppartenance[b] === 1) changerAffilie(etat, a, gb, sens); + if (etat.porteAppartenance[a] === 1) changerAffilie(etat, b, ga, sens); } // Ajoute sens aux visites de p à la table t, et ajuste les retours choisis. @@ -390,11 +390,12 @@ function etatInitial(structure, rng) { instance, contraintes: instance.contraintes, groupe: instance.groupe, - // affilie[p] vaut 1 quand p porte une appartenance, 0 pour SANS_GROUPE. + // porteAppartenance[p] vaut 1 quand p porte une appartenance, 0 pour + // SANS_GROUPE. // modifierPaire le lit au lieu de comparer un groupe à SANS_GROUPE : sous // le lanceur d'épreuves, chaque lecture d'une valeur importée passe par // un accesseur, que la boucle de recherche paierait à chaque paire. - affilie: Uint8Array.from(instance.groupe, (g) => (g === SANS_GROUPE ? 0 : 1)), + porteAppartenance: Uint8Array.from(instance.groupe, (g) => (g === SANS_GROUPE ? 0 : 1)), N, T, R, diff --git a/src/moteur/recherche.test.js b/src/moteur/recherche.test.js index 9c4bf4e..17ecbbc 100644 --- a/src/moteur/recherche.test.js +++ b/src/moteur/recherche.test.js @@ -929,19 +929,3 @@ describe('rechercher : bascule et relance, à la lettre (en-tête de recherche.j ); }); }); - -describe('rechercher : la petite démonstration (§ 15.3)', () => { - test('arret 200 000, trois propositions : chacune rencontre 8 pour les douze, zéro collision, zéro répétition', () => { - const configuration = demo('petite'); - const instance = normaliser(configuration); - const propositions = rechercher(configuration, { graine: 2_026, arret: 200_000, nombre: 3 }); - assert.equal(propositions.length, 3); - for (const { id, plan } of propositions) { - const mesures = mesurer(instance, indexerPlan(instance, plan)); - assert.deepEqual(mesures.rencontres, Array(12).fill(8), `proposition ${id}`); - assert.equal(mesures.collisionsCumulees, 0, `proposition ${id}`); - assert.deepEqual(mesures.rencontresRepetees, { choisies: 0, imposees: 0 }, `proposition ${id}`); - assert.equal(mesures.maxRencontresPaire, 1, `proposition ${id}`); - } - }); -}); diff --git a/test/arborescence.test.js b/test/arborescence.test.js index 3a7c6c9..7ce3021 100644 --- a/test/arborescence.test.js +++ b/test/arborescence.test.js @@ -109,7 +109,7 @@ const PARCOURS_DE_CLES = [ ['Object.keys', /\bObject\s*\??\.\s*keys\b/g], ['Object.entries', /\bObject\s*\??\.\s*entries\b/g], ['Object.values', /\bObject\s*\??\.\s*values\b/g], - ['Object[', /\bObject\s*(?:\?\.)?\s*\[/g], + ['Object[', /\bObject\s*(?:\?\.)?\s*\[(?!\s*\])/g], ['for…in', /\bfor\s*\(\s*(?:(?:const|let|var)\s+)?(?:[\p{L}_$][\p{L}\p{N}_$.]*|\[[^\]]*\]|\{[^}]*\})\s+in\b/gu], ['Reflect.ownKeys', /\bReflect\s*\??\.\s*ownKeys\b/g], ['Object.getOwnPropertyNames', /\bObject\s*\??\.\s*getOwnPropertyNames\b/g], @@ -306,6 +306,7 @@ describe('arborescence : déterminisme (§ 14.7)', () => { 'export const z = monObject.keys + objectKeys;', 'export const w = Object.fromEntries(paires);', 'for (const index of inventaire) t.push(index);', + '/** @param {Object[]} entrees les entrées, dans leur ordre */', ].join('\n'), }; assert.deepEqual(avecArbre(fichiers, releverDeterminisme), [