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

'' + '' + '