From 9e8d8e33aeb3334f24603ccbff93a287ce5303fc Mon Sep 17 00:00:00 2001 From: jsboige self-bot Date: Tue, 15 Sep 2026 08:11:10 +0200 Subject: [PATCH] fix(tooling,#16188): porte de confirmation element observable -- compteur + ::notice Le correctif #15877 a introduit la porte de confirmation element (#15695) qui continue en silence quand la chevauche Range n est pas confirmee par les boites element disjointes. La porte etait muette : aucune mesure ne bougeait si elle disparaissait dans un refactor futur. Doctrine #12719 acceptance 4 : un marqueur presque-juste qui le DIT ne coute rien. Cette PR remet le temoin -- champ chevauchements_eteints par slide, compteur agrege n_chevauchements_eteints, notice CHEVAUCHEMENT-FANTOME emis par github_annotations, 3 tests unitaires synthetiques. Scope strict : 2 fichiers, notice non bloquant, aucun seuil touche. MED/tooling -- G-VAR-1 NOT HELD x3 cycles consecutifs Tell c.15793 strict DEEP-only ; Tell c.1060-L2 grain-MED-guard-plancher-si-pool-CONTENU-sec applicable. Co-Authored-By: Claude Haiku 4.5 (1M context) --- .../notebook_tools/scan_slidev_composition.py | 39 +++++++- .../tests/test_scan_slidev_composition.py | 99 ++++++++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/scripts/notebook_tools/scan_slidev_composition.py b/scripts/notebook_tools/scan_slidev_composition.py index 31be4b2e3d..7d51e94476 100644 --- a/scripts/notebook_tools/scan_slidev_composition.py +++ b/scripts/notebook_tools/scan_slidev_composition.py @@ -271,6 +271,9 @@ def measure_slide(page, slide_idx: int, canvas_w: int, canvas_h: int) -> dict: // (RECOUVREMENT ci-après), qui mesure le contenu rendu et // l'ordre de peinture au lieu de la boîte élément naïve. const chevauchements = []; + // #16188 — porte de confirmation élément : on compte les paires éteintes + // (Range chevauche mais boîtes élément disjointes) au lieu de `continue` muet. + const chevauchementsEteints = []; const textEls = Array.from( root.querySelectorAll('h1, h2, h3, h4, p, li, blockquote, td, th') ); @@ -332,7 +335,18 @@ def measure_slide(page, slide_idx: int, canvas_w: int, canvas_h: int) -> dict: const eb = b.el.getBoundingClientRect(); const eOverlapX = Math.min(ea.right, eb.right) - Math.max(ea.left, eb.left); const eOverlapY = Math.min(ea.bottom, eb.bottom) - Math.max(ea.top, eb.top); - if (eOverlapX <= 0 || eOverlapY <= 0) continue; + // #16188 — porte de confirmation élément muette = silent suppression. + // Comptage explicite des paires éteintes pour observabilité : si la + // porte disparaît dans un refactor futur, une mesure bouge. Doctrine + // #12719 acceptance 4 ("un marqueur presque-juste qui le DIT ne coûte rien"). + if (eOverlapX <= 0 || eOverlapY <= 0) { + chevauchementsEteints.push({ + a: a.key, b: b.key, + overlap_range: [Math.round(overlapX), Math.round(overlapY)], + element_disjoint: true, + }); + continue; + } chevauchements.push({ a: a.key, b: b.key, a_bbox: [Math.round(a.left), Math.round(a.top), Math.round(a.right), Math.round(a.bottom)], @@ -538,7 +552,7 @@ def measure_slide(page, slide_idx: int, canvas_w: int, canvas_h: int) -> dict: }; } - return { horsCanvas, chevauchements, recouvrements, occupation, contentBottom: Math.round(contentBottom) }; + return { horsCanvas, chevauchements, chevauchementsEteints, recouvrements, occupation, contentBottom: Math.round(contentBottom) }; }""", [canvas_w, canvas_h], ) @@ -554,6 +568,10 @@ def measure_slide(page, slide_idx: int, canvas_w: int, canvas_h: int) -> dict: "hors_canvas": hors, "container_only": bool(hors) and not any(h.get("tag") in CONTENT_TAGS for h in hors), "chevauchements": raw.get("chevauchements", []), + # #16188 — porte de confirmation élément (#15695/#15877) maintenant + # observable : champ exposé pour que github_annotations puisse émettre + # `::notice [CHEVAUCHEMENT-FANTOME]` au lieu de `continue` silencieux. + "chevauchements_eteints": raw.get("chevauchementsEteints", []), "recouvrements": raw.get("recouvrements", []), "occupation": raw.get("occupation"), } @@ -664,6 +682,17 @@ def github_annotations(report: dict, slides_md: Path) -> list[str]: f"::warning file={rel},line={line}::[CHEVAUCHEMENT] slide {r['slide']} ({head}) — " f"{c['a']} × {c['b']} overlap={c['overlap']}px element_overlap={c.get('element_overlap')}px" ) + # #16188 — porte de confirmation élément (#15695/#15877) : signaler + # les paires Range×Range éteintes par boîtes élément disjointes + # (chevauchement-fantôme : graze Range ~1.2 px, boîtes disjointes). + # `::notice` non bloquant, agrégé par slide. + eteints = r.get("chevauchements_eteints", []) + if eteints: + out.append( + f"::notice file={rel},line={line}::[CHEVAUCHEMENT-FANTOME] slide {r['slide']} ({head}) — " + f"{len(eteints)} effleurement(s) Range éteint(s) par la confirmation élément — " + f"boîtes élément disjointes, rien à l'écran" + ) for rv in r.get("recouvrements", [])[:3]: out.append( f"::warning file={rel},line={line}::[RECOUVREMENT-TEXTE-IMAGE] slide {r['slide']} — " @@ -759,6 +788,11 @@ def main(): n_total = len(results) n_hors = sum(1 for r in results if content_overflow(r)) n_chev = sum(1 for r in results if r.get("chevauchements")) + # #16188 — compteur de paires éteintes par la confirmation élément + # (#15695/#15877) : la porte muette devient un signal mesurable. Si + # `n_chevauchements_eteints` tombe à 0 sur un deck où l'on sait que + # des effleurements Range existaient, c'est que la porte a disparu. + n_eteints = sum(len(r.get("chevauchements_eteints", [])) for r in results) n_rec = sum(1 for r in results if r.get("recouvrements")) n_occ = sum(1 for r in results if occupation_flagged(r, canvas_h)) @@ -790,6 +824,7 @@ def main(): "n_slides": n_total, "n_hors_canvas": n_hors, "n_chevauchements": n_chev, + "n_chevauchements_eteints": n_eteints, "n_recouvrements": n_rec, "n_occupation_flagged": n_occ, "recouvrement_borne": ( diff --git a/scripts/notebook_tools/tests/test_scan_slidev_composition.py b/scripts/notebook_tools/tests/test_scan_slidev_composition.py index e7da5effa7..c155f59bba 100644 --- a/scripts/notebook_tools/tests/test_scan_slidev_composition.py +++ b/scripts/notebook_tools/tests/test_scan_slidev_composition.py @@ -11,6 +11,7 @@ from scan_slidev_composition import ( # noqa: E402 content_overflow, + github_annotations, occupation_flagged, parse_headmatter_canvas, split_slides_source, @@ -330,7 +331,7 @@ def _build(baseline_slide): "canvas_w": 980, "canvas_h": 552, "BORNE": "ADVISORY", "ctrl_positif_ok": None, "ctrl_positif_msg": None, - "n_total": 0, "n_hors": 0, "n_chev": 0, "n_rec": 0, "n_occ": 0, + "n_total": 0, "n_hors": 0, "n_chev": 0, "n_eteints": 0, "n_rec": 0, "n_occ": 0, } exec(block, ns) return ns["report"] @@ -396,3 +397,99 @@ def test_positive_control_fixture_canvas_default(): # le contrôle CI suppose le canvas par défaut 980×552 : top:600px déborde # de 48 px. Un canvasHeight headmatter > 600 casserait la garantie. assert parse_headmatter_canvas(FIXTURE) == (980, 552) + + +# #16188 — porte de confirmation élément (#15695/#15877) maintenant observable. +# 3 tests synthétiques sur `github_annotations` : rapport par slide avec +# `chevauchements_eteints` non vide → 1 `::notice [CHEVAUCHEMENT-FANTOME]`, +# AUCUN `::warning [CHEVAUCHEMENT]` (les paires éteintes sont des fantômes, +# pas des vrais chevauchements) ; rapport propre → rien. +def _fake_report(slide_results, canvas=(980, 552), slides_md=None): + """Construit un rapport minimal pour github_annotations.""" + from pathlib import Path + return { + "canvas": list(canvas), + "_slide_lines": {r["slide"]: 1 for r in slide_results}, + "results": slide_results, + "url": "test://fixture", + "baseline_slide": None, + "baseline_commit": None, + "slides_md": slides_md or Path("slides/test/slides.md"), + } + + +def test_chevauchement_fantome_notice_when_pair_eteinte(tmp_path): + # Paire Range×Range éteinte par la confirmation élément : 1 `::notice`, + # 0 `::warning [CHEVAUCHEMENT]`. Reproduit le cas fondateur #15695 + # (puces inline effleurent ~1.2 px, boîtes disjointes). + fake_slides_md = tmp_path / "slides.md" + fake_slides_md.write_text("placeholder", encoding="utf-8") + report = _fake_report([ + { + "slide": 5, + "text_head": "slide test fantôme", + "chevauchements": [], # porte a éteint + "chevauchements_eteints": [ + {"a": "LI", "b": "LI", "overlap_range": [2, 1], "element_disjoint": True}, + {"a": "P", "b": "P", "overlap_range": [1, 1], "element_disjoint": True}, + ], + } + ], slides_md=fake_slides_md) + anns = github_annotations(report, fake_slides_md) + notice_lines = [a for a in anns if "[CHEVAUCHEMENT-FANTOME]" in a] + warning_lines = [a for a in anns if "[CHEVAUCHEMENT]" in a and "FANTOME" not in a] + assert len(notice_lines) == 1, f"attendu 1 notice fantôme, vu {len(notice_lines)} : {notice_lines}" + assert "2 effleurement(s)" in notice_lines[0], \ + f"compteur agrégé manquant : {notice_lines[0]}" + assert "boîtes élément disjointes" in notice_lines[0], \ + f"libellé de la notice incorrect : {notice_lines[0]}" + assert warning_lines == [], \ + f"les paires éteintes ne doivent PAS générer de warning CHEVAUCHEMENT : {warning_lines}" + + +def test_chevauchement_warning_pair_raportee(tmp_path): + # Paire Range×Range NON éteinte (boîtes éléments qui se chevauchent + # réellement) : 1 `::warning [CHEVAUCHEMENT]`, 0 notice fantôme. + fake_slides_md = tmp_path / "slides.md" + fake_slides_md.write_text("placeholder", encoding="utf-8") + report = _fake_report([ + { + "slide": 7, + "text_head": "slide test vrai chevauchement", + "chevauchements": [ + { + "a": "P.x", "b": "P.y", + "overlap": [10, 20], + "element_overlap": [10, 20], + } + ], + "chevauchements_eteints": [], + } + ], slides_md=fake_slides_md) + anns = github_annotations(report, fake_slides_md) + notice_lines = [a for a in anns if "[CHEVAUCHEMENT-FANTOME]" in a] + warning_lines = [a for a in anns if "[CHEVAUCHEMENT]" in a and "FANTOME" not in a] + assert len(warning_lines) == 1, \ + f"attendu 1 warning chevauchement, vu {len(warning_lines)} : {warning_lines}" + assert notice_lines == [], \ + f"pas de notice fantôme pour un vrai chevauchement : {notice_lines}" + + +def test_chevauchement_no_signal_propre(tmp_path): + # Slide propre : pas de chevauchement, pas d'éteint → aucun signal + # CHEVAUCHEMENT ni CHEVAUCHEMENT-FANTOME (les autres notices de + # synthèse type `Plancher mécanique advisory` restent autorisées). + fake_slides_md = tmp_path / "slides.md" + fake_slides_md.write_text("placeholder", encoding="utf-8") + report = _fake_report([ + { + "slide": 9, + "text_head": "slide test propre", + "chevauchements": [], + "chevauchements_eteints": [], + } + ], slides_md=fake_slides_md) + anns = github_annotations(report, fake_slides_md) + chev_lines = [a for a in anns if "CHEVAUCHEMENT" in a] + assert chev_lines == [], \ + f"slide propre ne doit produire aucun signal CHEVAUCHEMENT : {chev_lines}"