[FIX] tests: four-core budget, quoted values, key guard, switch floor

The relaunch budget of § 14.14 was out of reach on two cores whatever the
tests did; the spec now names its reference machine, four cores, and the
watched series drops a 200 000-move search the long series already runs.
The itinerary guard quotes a string value as the indexed-plan guard does,
through one shared helper. The key-order guard no longer mistakes the
JSDoc type {Object[]} for a computed access, and the property grid counts
the proposals that switch to the constraint order, under a floor.

Checked: 456 node tests in 1.8 s; each new test failed first; cutting the
descent to one move drops the switch count from 86 to 30, under its floor.

--- FR ---

[FIX] épreuves : budget sur quatre cœurs, citation, clés, bascule

Le budget de relance du § 14.14 était hors d'atteinte sur deux cœurs,
quoi que fassent les épreuves ; le spec nomme désormais sa machine de
référence, quatre cœurs, et la série surveillée quitte une recherche de
200 000 mouvements que la série longue joue déjà. La garde d'itinéraire
cite une chaîne comme la garde du plan indexé, par un seul assistant. La
garde de l'ordre des clés ne prend plus le type JSDoc {Object[]} pour un
accès calculé, et la grille compte, sous un plancher, les propositions
qui basculent dans l'ordre des contraintes.

Vérifié : 456 épreuves node en 1,8 s ; chaque épreuve nouvelle a échoué
d'abord ; une descente coupée au premier mouvement fait tomber les
bascules de 86 à 30, sous leur plancher.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-06 04:26:08 -04:00
parent 4795114093
commit 1910e207cb
9 changed files with 38 additions and 34 deletions

View file

@ -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

View file

@ -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`);

View file

@ -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;
}
}

View file

@ -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

View file

@ -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}`);
}
}
}

View file

@ -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)", () => {

View file

@ -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,

View file

@ -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}`);
}
});
});

View file

@ -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), [