From 7ad7dab1eef43ff4934c4fb1023db7c301740026 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 12:08:17 +0000 Subject: [PATCH] =?UTF-8?q?Passer=20=C3=A0=20l'anglais=20les=20derniers=20?= =?UTF-8?q?noms=20du=20gabarit,=20et=20deux=20d=C3=A9fauts=20que=20=C3=A7a?= =?UTF-8?q?=20met=20au=20jour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quatre-vingt-deux noms restaient en français dans le code de la page — des variables, des paramètres, des fonctions, et les membres du chargeur de blocs. Ils ne sont lus par personne d'autre que la page : rien n'obligeait à les garder. Les clés, elles, ne bougent pas — elles vont sur le disque et en reviennent. Le renommage est vérifié au rendu, comme l'inversion l'avait été : 7 734 chaînes affichées, dix-huit états de la vue, deux langues, avant et après. Zéro différence. Deux pièges ont coûté un défaut chacun, et ils sont notés dans CLAUDE.md. Le raccourci d'objet littéral n'est ni à renommer ni à sauter : le renommer renomme la propriété, le sauter laisse une référence vers une variable disparue — « noeuds is not defined », vu au rendu. Il faut le déplier : « noeuds: nodesByName » garde le nom gelé et pointe la nouvelle variable. Les deux-points d'un ternaire ne sont pas ceux d'une clé. « ? valeur : undefined » avait donc été épargné par la passe d'hier, et pointait vers un nom renommé depuis. Ça lève une erreur juste après une sauvegarde réussie par la page servie — le seul chemin que la recette pilote en curl et pas en navigateur. Et une clé gelée peut se cacher dans une chaîne : la traduction d'hier a changé annotationPatch(R, "nom", …) en "name", alors que tous les lecteurs, l'export et le corps de la requête demandent toujours « nom ». Un nom saisi dans la page partait dans un champ que personne ne lit, et la page l'affichait quand même puisqu'elle relit le champ qu'elle vient de remplir. ViewContractTest tient désormais cette ligne. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01J956wjynbd7fkZx4HpjHzP --- CLAUDE.md | 41 +- .../main/resources/lab/xray/dashboard.html | 618 +++++++++--------- .../lab/xray/report/ViewContractTest.java | 107 +++ 3 files changed, 446 insertions(+), 320 deletions(-) create mode 100644 orchestrator/src/test/java/lab/xray/report/ViewContractTest.java diff --git a/CLAUDE.md b/CLAUDE.md index 861d854..64f7b52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -320,20 +320,39 @@ to signal it. `ViewLanguageTest` checks the whole static body for that reason sentence it carries must have its translation. The rest of the text is born in JavaScript and only shows at render time: that check is done at the browser, before pushing. -**Renaming an identifier here needs a rendered check, not a compiler.** The template's code -holds names that are *also* frozen JSON keys — `nom`, `valeur`, `racine`, `chemin`, -`elagage` — read from and written to disk. A rename must therefore leave `.nom`, `nom:` and -`"nom"` alone and touch only the bare identifier; that much a script can do. What it cannot -see is the **object-literal shorthand**: `{...c, pkg, runs: [i]}` names a property by naming -a variable, so renaming the variable renames the property, and the reader two hundred lines -below still asks for the old one. Nothing fails at parse time, no test written in Java can -reach it, and the view simply comes up empty. It happened on 29 August 2026, and it is the -snapshot of the rendered strings that said so — not the 258 tests, which stayed green. +**The identifiers are English; the keys are not, and that line is the whole difficulty.** The +template's code holds names that are *also* frozen JSON keys — `nom`, `valeur`, `racine`, +`chemin`, `elagage`, `coupes`, `ordre` — read from and written to disk, so a rename must +leave `.nom`, `nom:` and `"nom"` alone and touch only the bare identifier. The page's own +internals have no such constraint: the block loader's members are English (`load`, `store`, +`touch`, `evict`, `pending`, `pinned`, `_queue`), because no file on disk names them — +`XR.bloc` is the single name the generated `vue/*.js` know, and it stays. + +**Three traps, each of which shipped a defect.** A script that renames bare identifiers gets +all three wrong unless it is told: + +- **The object-literal shorthand.** `{...c, pkg, runs: [i]}` names a property by naming a + variable. Renaming the variable renames the property (the view came up empty, 29 August); + *skipping* it leaves a reference to a variable that no longer exists (`noeuds is not + defined`, the same day). Neither is right: the shorthand must be **expanded** — + `noeuds: nodesByName` — which keeps the frozen name and points it at the new variable. +- **A ternary's colon is not a key.** `? valeur : undefined` reads as `valeur:` to a regex, + so the rename skipped it and left a name that had been renamed away. It throws only after + a successful save through the served page — the one path the acceptance drives with `curl` + rather than a browser, so nothing saw it for a day. +- **A frozen key inside a string.** Translating the identifiers turned `annotationPatch(R, + "nom", …)` into `"name"`, while every reader, the exporter and the save payload went on + asking for `nom`. A name typed in the page went into a field nobody read — and the page + showed it anyway, because it re-reads the input it just filled. `ViewContractTest` holds + that line now: a patched field outside `nom, description, etiquettes, elagage` fails the + build. **The way to verify a change here is to render it.** Before touching the block, take a snapshot of every string the page displays — both languages, a dozen states of the view — and take the -same one after: the inversion itself was carried out that way, and the only differences it left -were the ones intended. A reading of the diff proves nothing; the DOM does. +same one after: the inversion itself was carried out that way, and so was the rename of the +last eighty French names, which left **0 differences over 7 734 captured strings**. A reading +of the diff proves nothing; the DOM does. And the DOM does not prove everything either — it +never opens the save path, which is why the two defects above needed a test apiece. ## Conventions diff --git a/orchestrator/src/main/resources/lab/xray/dashboard.html b/orchestrator/src/main/resources/lab/xray/dashboard.html index 1adc29c..b7219f9 100644 --- a/orchestrator/src/main/resources/lab/xray/dashboard.html +++ b/orchestrator/src/main/resources/lab/xray/dashboard.html @@ -893,15 +893,15 @@

Runtime X-Ray

* ===================================================================================== */ const XR = { cache: new Map(), // block name -> what it brought - ordre: [], // from oldest to most recent + lruOrder: [], // from oldest to most recent scripts: new Map(), - attente: new Map(), // the same block asked for twice is loaded only once - PLAFOND: 6, - _recu: null, + pending: new Map(), // the same block asked for twice is loaded only once + CAP: 6, + _incoming: null, /** Called by every line of a block while it loads. */ bloc(type, srcKey, val){ - if (XR._recu) XR._recu.push([type, srcKey, val]); + if (XR._incoming) XR._incoming.push([type, srcKey, val]); }, /* Loads follow one another instead of overlapping, and that is not a detail: the @@ -909,58 +909,58 @@

Runtime X-Ray

it — the second overwrites it before the first has emptied. Seen. Parallelising them would gain nothing here: these are local files, and the queue blocks only for the time of a parse. */ - _file: Promise.resolve(), - - charger(blockName){ - if (XR.cache.has(blockName)) { XR.toucher(blockName); return Promise.resolve(); } - if (XR.attente.has(blockName)) return XR.attente.get(blockName); - const p = XR._file.then(() => new Promise((ok, ko) => { - if (XR.cache.has(blockName)) { XR.toucher(blockName); ok(); return; } - const recu = []; - XR._recu = recu; + _queue: Promise.resolve(), + + load(blockName){ + if (XR.cache.has(blockName)) { XR.touch(blockName); return Promise.resolve(); } + if (XR.pending.has(blockName)) return XR.pending.get(blockName); + const p = XR._queue.then(() => new Promise((ok, ko) => { + if (XR.cache.has(blockName)) { XR.touch(blockName); ok(); return; } + const received = []; + XR._incoming = received; const s = document.createElement("script"); s.src = blockName; s.onload = () => { - XR._recu = null; XR.attente.delete(blockName); XR.ranger(blockName, recu); ok(); + XR._incoming = null; XR.pending.delete(blockName); XR.store(blockName, received); ok(); }; s.onerror = () => { - XR._recu = null; XR.attente.delete(blockName); s.remove(); XR.scripts.delete(blockName); + XR._incoming = null; XR.pending.delete(blockName); s.remove(); XR.scripts.delete(blockName); ko(new Error("block not found: " + blockName)); }; document.head.appendChild(s); XR.scripts.set(blockName, s); })); - XR.attente.set(blockName, p); + XR.pending.set(blockName, p); // The queue must not stop on a missing block: the next one has every right to be there. - XR._file = p.catch(() => {}); + XR._queue = p.catch(() => {}); return p; }, /** Files what a block brought where the view already reads it. */ - ranger(blockName, recu){ - const pose = []; - for (const [type, srcKey, val] of recu){ - if (type === "src"){ D.sources[srcKey] = val; pose.push(["src", srcKey]); } - else if (type === "cumul"){ D.cumul[srcKey] = val; pose.push(["cumul", srcKey]); } - else if (type === "poids"){ D.poids[srcKey] = val; pose.push(["poids", srcKey]); } - else if (type === "presence"){ D.presence[srcKey] = val; pose.push(["presence", srcKey]); } + store(blockName, received){ + const placed = []; + for (const [type, srcKey, val] of received){ + if (type === "src"){ D.sources[srcKey] = val; placed.push(["src", srcKey]); } + else if (type === "cumul"){ D.cumul[srcKey] = val; placed.push(["cumul", srcKey]); } + else if (type === "poids"){ D.poids[srcKey] = val; placed.push(["poids", srcKey]); } + else if (type === "presence"){ D.presence[srcKey] = val; placed.push(["presence", srcKey]); } else if (type === "run"){ - const [uuid, champ] = srcKey.split("/"); + const [uuid, field] = srcKey.split("/"); const run = D.runs.find(r => r.uuid === uuid); - if (run){ run[champ] = val; pose.push(["run", run, champ]); } + if (run){ run[field] = val; placed.push(["run", run, field]); } } } - XR.cache.set(blockName, pose); + XR.cache.set(blockName, placed); // Global indexes do not age: they are small, they serve all the time, and // evicting them breaks accumulation as soon as a few more classes are opened — seen. if (XR.permanent(blockName)) return; - XR.toucher(blockName); - XR.evincer(); + XR.touch(blockName); + XR.evict(); }, - toucher(blockName){ + touch(blockName){ if (XR.permanent(blockName)) return; - XR.ordre = XR.ordre.filter(x => x !== blockName); XR.ordre.push(blockName); + XR.lruOrder = XR.lruOrder.filter(x => x !== blockName); XR.lruOrder.push(blockName); }, /** A global index: it crosses runs, it is small, it stays. */ @@ -975,50 +975,50 @@

Runtime X-Ray

* ceiling no longer held. The counter bounds the round, failing which a whole queue of * protected blocks would spin for ever. */ - evincer(){ - let tours = XR.ordre.length; - while (XR.ordre.length > XR.PLAFOND && tours-- > 0){ - const vieux = XR.ordre.shift(); - if (XR.protege.has(vieux)) { XR.ordre.push(vieux); continue; } - for (const pose of (XR.cache.get(vieux) || [])){ - if (pose[0] === "src") delete D.sources[pose[1]]; - else if (pose[0] === "cumul") delete D.cumul[pose[1]]; - else if (pose[0] === "poids") delete D.poids[pose[1]]; - else if (pose[0] === "presence") delete D.presence[pose[1]]; - else if (pose[0] === "run") delete pose[1][pose[2]]; + evict(){ + let rounds = XR.lruOrder.length; + while (XR.lruOrder.length > XR.CAP && rounds-- > 0){ + const oldest = XR.lruOrder.shift(); + if (XR.pinned.has(oldest)) { XR.lruOrder.push(oldest); continue; } + for (const placed of (XR.cache.get(oldest) || [])){ + if (placed[0] === "src") delete D.sources[placed[1]]; + else if (placed[0] === "cumul") delete D.cumul[placed[1]]; + else if (placed[0] === "poids") delete D.poids[placed[1]]; + else if (placed[0] === "presence") delete D.presence[placed[1]]; + else if (placed[0] === "run") delete placed[1][placed[2]]; } - XR.cache.delete(vieux); - const s = XR.scripts.get(vieux); - if (s){ s.remove(); XR.scripts.delete(vieux); } + XR.cache.delete(oldest); + const s = XR.scripts.get(oldest); + if (s){ s.remove(); XR.scripts.delete(oldest); } } }, /** What we do not free: what the view is showing right now. */ - protege: new Set(), + pinned: new Set(), /** The blocks needed to display these runs — each has its own, under it. */ async runs(indices){ - const voulus = []; - for (const i of indices) for (const b of ((D.runs[i] || {}).blocs || [])) voulus.push(b); - XR.protege = new Set(voulus); - await Promise.all(voulus.map(b => XR.charger(b).catch(e => console.error(e)))); + const wanted = []; + for (const i of indices) for (const b of ((D.runs[i] || {}).blocs || [])) wanted.push(b); + XR.pinned = new Set(wanted); + await Promise.all(wanted.map(b => XR.load(b).catch(e => console.error(e)))); }, /** A global index: it crosses runs, so it lives above them. */ async global(blockName){ const blockPath = (D.global || "vue/") + blockName; - if (XR.cache.has(blockPath)) { XR.toucher(blockPath); return; } - await XR.charger(blockPath).catch(e => console.error(e)); + if (XR.cache.has(blockPath)) { XR.touch(blockPath); return; } + await XR.load(blockPath).catch(e => console.error(e)); }, /** The block that carries this file's code, if there is one. */ async source(srcKey){ const bloc = D.sourcesDisponibles[srcKey]; - if (bloc && !D.sources[srcKey]) await XR.charger(bloc); + if (bloc && !D.sources[srcKey]) await XR.load(bloc); }, async cumulIndex(){ await XR.global("cumul.js"); }, - async poidsIndex(){ await XR.global("poids.js"); }, + async weightsIndex(){ await XR.global("poids.js"); }, async presenceIndex(){ await XR.global("presence.js"); } }; D.sources = {}; @@ -1068,9 +1068,9 @@

Runtime X-Ray

* * The setting is deliberately in the page and not in the measurement: it changes in one * click, with nothing to re-run, and the union recomputes on data already there. */ -let cumul = false; -try { cumul = localStorage.getItem("runtime-xray.cumul") === "1"; } catch(e){} -if (D.runs.length < 2) cumul = false; +let cumulative = false; +try { cumulative = localStorage.getItem("runtime-xray.cumul") === "1"; } catch(e){} +if (D.runs.length < 2) cumulative = false; /** The ticked runs, in the page's order. */ function tickedRuns(){ @@ -1086,7 +1086,7 @@

Runtime X-Ray

* same rule JaCoCo applies when merging two {@code .exec} files. */ function coverageForView(source){ - if (!cumul) return (R.coverage && R.coverage[source]) || {}; + if (!cumulative) return (R.coverage && R.coverage[source]) || {}; // The union is read from an index computed at assembly: per line, two bit masks — who // covered it entirely, who partially. Uniting a subset becomes a binary AND, one // operation per line, instead of a search per line AND per run. @@ -1095,8 +1095,8 @@

Runtime X-Ray

let maskBits = 0; for (const i of tickedRuns()) maskBits |= (1 << i); const out = {}; - for (const [nr, plein, partiel] of index){ - const p = plein & maskBits, q = partiel & maskBits; + for (const [nr, fully, partly] of index){ + const p = fully & maskBits, q = partly & maskBits; if (!p && !q) continue; const bits = p | q, which = []; for (let i = 0; i < D.runs.length; i++) if (bits & (1 << i)) which.push(i); @@ -1107,7 +1107,7 @@

Runtime X-Ray

/** The pips that say which runs covered a line. */ function runPips(c){ - if (!cumul || !c || !c.runs || !c.runs.length) return ""; + if (!cumulative || !c || !c.runs || !c.runs.length) return ""; return '' + c.runs.map(i => '' + (i + 1) + '').join("") + ''; @@ -1259,7 +1259,7 @@

Runtime X-Ray

/** What was typed into the page wins over what the report already carried. */ function annotationOf(run){ const local = annotations[run.uuid] || {}; - const base = annotationEnregistree(run); + const base = savedAnnotation(run); return { nom: local.nom !== undefined ? local.nom : base.nom, description: local.description !== undefined ? local.description : base.description, @@ -1267,9 +1267,9 @@

Runtime X-Ray

elagage: local.elagage !== undefined ? local.elagage : base.elagage }; } -function annotationPatch(run, champ, val){ +function annotationPatch(run, field, val){ const a = annotations[run.uuid] || (annotations[run.uuid] = {}); - if (val === null) delete a[champ]; else a[champ] = val; + if (val === null) delete a[field]; else a[field] = val; if (!Object.keys(a).length) delete annotations[run.uuid]; saveAnnotations(); } @@ -1304,12 +1304,12 @@

Runtime X-Ray

return r.ok ? r.json() : null; }) .then(j => { if (!j) return false; - const avant = JSON.stringify(serverFingerprints); + const before = JSON.stringify(serverFingerprints); serverAnnotations = j.annotations || {}; serverFingerprints = j.empreintes || {}; if (serverRevision === null) serverRevision = j.revision || null; else if (j.revision && j.revision !== serverRevision) announceNews(j.executions); - return JSON.stringify(serverFingerprints) !== avant; + return JSON.stringify(serverFingerprints) !== before; }) .catch(() => false); } @@ -1377,7 +1377,7 @@

Runtime X-Ray

/** A run's annotation as it is saved: the server first, otherwise the one the page * carried when it was built. */ -function annotationEnregistree(run){ +function savedAnnotation(run){ const remote = serverAnnotations[run.uuid]; if (remote !== undefined) { return typeof remote === "string" @@ -1393,7 +1393,7 @@

Runtime X-Ray

function annotationChanged(run){ const local = annotations[run.uuid]; if (!local) return false; - const a = annotationOf(run), e = annotationEnregistree(run); + const a = annotationOf(run), e = savedAnnotation(run); return JSON.stringify([a.nom, a.description, a.etiquettes, a.elagage]) !== JSON.stringify([e.nom, e.description, e.etiquettes, e.elagage]); } @@ -1405,9 +1405,9 @@

Runtime X-Ray

* annotating two runs never cross, and on the same run the second is warned instead of * overwriting the first. */ -function enregistrerAnnotations(bouton){ +function persistAnnotations(btn){ const run = R; - const say = t => { bouton.textContent = t; }; + const say = t => { btn.textContent = t; }; const a = annotationOf(run); const tags = a.etiquettes && Object.keys(a.etiquettes).length ? a.etiquettes : undefined; const el = pruningActive(run) ? pruningOf(run) : undefined; @@ -1457,7 +1457,7 @@

Runtime X-Ray

// “changed” designates only what has not yet gone out. delete annotations[run.uuid]; saveAnnotations(); - serverAnnotations[run.uuid] = val && Object.keys(val).length ? valeur : undefined; + serverAnnotations[run.uuid] = val && Object.keys(val).length ? val : undefined; serverFingerprints[run.uuid] = body.empreinte; say("saved ✓"); refresh(); @@ -1489,8 +1489,8 @@

Runtime X-Ray

/** Takes up a noms.json: the runs it names overwrite what we had, the others stay — * importing is not starting over. */ -function importAnnotations(texte){ - const parsed = JSON.parse(texte); +function importAnnotations(jsonText){ + const parsed = JSON.parse(jsonText); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("this file carries no run record"); } @@ -1503,16 +1503,16 @@

Runtime X-Ray

saveAnnotations(); return 1; } - let reprises = 0; + let retries = 0; for (const [uuid, val] of Object.entries(parsed)){ annotations[uuid] = (typeof val === "string") ? {nom: val} : {nom: val.nom || "", description: val.description || "", etiquettes: val.etiquettes || {}, elagage: val.elagage || null}; - reprises++; + retries++; } saveAnnotations(); - return reprises; + return retries; } function shortId(run){ return (run.uuid || "").slice(0, 8); } @@ -1541,7 +1541,7 @@

Runtime X-Ray

const e = pruningOf(run); return !!(e.racine || e.coupes.length); } -function poseElagage(run, e){ +function applyPruning(run, e){ annotationPatch(run, "elagage", {racine: e.racine, coupes: e.coupes}); renderAnnotActs(); } @@ -1554,12 +1554,12 @@

Runtime X-Ray

return node; } /** A copy of the tree without the cut branches, each node carrying its original path. */ -function copyPruned(node, path, coupes){ +function copyPruned(node, path, cuts){ const kids = []; for (const kid of node.children || []){ const p = path ? path + ";" + kid.name : kid.name; - if (coupes.includes(p)) continue; - kids.push(copyPruned(kid, p, coupes)); + if (cuts.includes(p)) continue; + kids.push(copyPruned(kid, p, cuts)); } return {name: node.name, total: node.total, children: kids, __path: path}; } @@ -1583,12 +1583,12 @@

Runtime X-Ray

const pruned = copyPruned(base, basePath, e.coupes); // Root brought down: we reintroduce a mute parent so the chosen node displays itself, // and not only its children — otherwise one no longer knows where one starts from. - const arbre = basePath + const treeNode = basePath ? {name: "all", total: run.calltree.total, children: [pruned], __path: ""} : pruned; run.__prunedKey = srcKey; - run.__pruned = arbre; - return arbre; + run.__pruned = treeNode; + return treeNode; } /** A node's path as the pruning designates it. */ @@ -1635,11 +1635,11 @@

Runtime X-Ray

reselect(); }); const cb = document.getElementById("cumulcb"); - cb.checked = cumul; + cb.checked = cumulative; cb.onchange = async () => { - cumul = cb.checked; - try { localStorage.setItem("runtime-xray.cumul", cumul ? "1" : "0"); } catch(e){} - if (cumul) await XR.cumulIndex(); + cumulative = cb.checked; + try { localStorage.setItem("runtime-xray.cumul", cumulative ? "1" : "0"); } catch(e){} + if (cumulative) await XR.cumulIndex(); refreshHeader(); renderLegend(); // draw() chooses for itself between the code and the overview. Calling a “drawCode” @@ -1719,7 +1719,7 @@

Runtime X-Ray

}; /** The button's arrow: defined here because the bar is built before the rest. */ -const ICONE_EXPORT = +const EXPORT_ICON = '

Runtime X-Ray

const shownGroups = runFiles().filter(g => g.toujours || g.items.some(i => i[0])); const entryOf = (g, it) => { const [ok, href, heading] = it; - const court = g.toujours ? it[3] : null; + const shortLabel = g.toujours ? it[3] : null; const what = g.toujours ? it[4] : it[3]; return ok ? '' + '' + esc(heading) + '' + esc(what) + '' - : '' + '' + esc(heading) + '' + '' + esc(what) + ' — not produced for this report yet.'; @@ -1834,7 +1834,7 @@

Runtime X-Ray

'' + '' + '