From 6ac796b8f7aba2ae0a7e60d567fc8207dfe63eab Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 25 Sep 2026 06:42:46 +0200 Subject: [PATCH] fix(ci,#17747): cliquet split-reading -- une revision en place n'est plus comptee comme un ajout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le garde BLOQUANT `Split-reading ratchet (base vs PR)` rougissait une PR qui n'ajoute aucune cellule : sur un carnet SANS id, la revision en place d'une cellule markdown non classee lecture etait comptee comme un ajout, et le bucket READING_BEFORE_CODE la signalait -- soit exactement le geste que le mandat user PRESCRIT (« si on rajoute une lecture, on modifie le paragraphe de lecture existant »), et que l'organe documente lui-meme comme a ne pas signaler. Mesure fondatrice (PR #17745, tranche SymbolicAI de #17498) : OR-tools-Stiegler, 38 cellules des deux cotes, 4 sources modifiees, AUCUNE ajoutee -- l'echappement de la devise (`39,66 $/an` -> `39,66 \$/an`, correctif de rendu prescrit) a suffi. Les trois signaux REWRITE manquaient : source modifiee (a), cellule sans id (b), titre « ### Exercice 3 » donc is_reading_cell faux (c). Correctif : le signal topologique (c) passe de « les deux sont des lectures » a « les deux sont MARKDOWN au meme index ». Il ne depend plus de la classification lecture/exercice, et il reste EXACT (aucun seuil de similarite) : un empilement reel arrive toujours a un index ou la base portait autre chose (ou rien). Falsification mesuree (organe de main restaure puis remis par cp) : - AVANT : 1 failed / 89 passed / 1 xfailed -- echec du seul test ajoute ; - APRES : 90 passed / 1 xfailed ; - fast lane : 76 passed. Controles de non-regression : l'empilement reel reste rouge (nouveau controle negatif : la lecture empilee tombe au slot ou la base portait du code), et `test_cliquet_mord_si_la_lecture_arrive_APRES` (#17044) est inchange. Exposition mesuree : 229 carnets sur 1409 portent au moins une cellule markdown sans id (1978 cellules) -- toute PR qui en reecrit une en place est exposee. Perimetre : 2 fichiers (organe + tests). Closes #17747. Co-Authored-By: Claude Sonnet 5 --- .../check_split_reading_cells.py | 26 ++++++---- .../tests/test_check_split_reading_cells.py | 48 +++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/scripts/notebook_tools/check_split_reading_cells.py b/scripts/notebook_tools/check_split_reading_cells.py index 6de2cfcd04..16e96e409d 100644 --- a/scripts/notebook_tools/check_split_reading_cells.py +++ b/scripts/notebook_tools/check_split_reading_cells.py @@ -426,10 +426,10 @@ def detect_added_readings(head_nb: dict, base_nb: dict | None) -> list[dict]: # que la campagne a produit des cellules sans id ET des ids # dupliques -- l'id est donc consomme une fois, ce qui borne les # doublons sans reouvrir la porte ; - # (c) meme position ET les deux cellules sont des lectures : la tete - # a revise celle qui occupait ce slot (#17044). Signal - # topologique, indispensable sur les carnets SANS id (le corpus - # en contient -- cf. les deux carnets du controle positif). + # (c) meme position ET les deux cellules sont markdown : la tete + # a revise celle qui occupait ce slot (#17044, etendu #17747). + # Signal topologique, indispensable sur les carnets SANS id (le + # corpus en contient -- cf. les deux carnets du controle positif). # La regle user dit « fusionner / reecrire, pas empiler » : la # reecriture EST l'action prescrite. Ne pas la signaler. is_rewrite = False @@ -441,8 +441,8 @@ def detect_added_readings(head_nb: dict, base_nb: dict | None) -> list[dict]: and base_cells[idx].get("cell_type") == "markdown" and cell_source(base_cells[idx]) == src): is_rewrite = True - # (c) meme position ET les DEUX cellules sont des lectures : la tete - # a REVISE celle qui occupait ce slot (#17044). Le mandat prescrit + # (c) meme position ET les DEUX cellules sont MARKDOWN : la tete a + # REVISE celle qui occupait ce slot (#17044). Le mandat prescrit # cette revision ; sans ce signal elle n'etait reconnue que sur # les carnets porteurs d'ids, donc le remede etait puni des que # les cellules n'en avaient pas (le corpus en contient : les deux @@ -450,9 +450,19 @@ def detect_added_readings(head_nb: dict, base_nb: dict | None) -> list[dict]: # EMPILEMENT reel a cote n'est pas vu ici : la lecture empilee # arrive a un index ou la base porte autre chose (ou rien), et # le compte de paires, lui, monte. + # + # Le signal ne depend PAS de la classification lecture/exercice + # (#17747) : une revision de prose en place n'est pas un + # empilement, quel que soit le titre. Exiger « les deux sont des + # lectures » rouvrait sur les carnets SANS id le faux positif que + # #17044 avait ferme pour les lectures. Mesure fondatrice : un + # simple echappement de `$` (``39,66 $`` -> ``39,66 \$``) dans + # une cellule sans id titree « ### Exercice 3 » suffisait a la + # faire passer pour un ajout, et le cliquet rougissait le geste + # que le mandat PRESCRIT. if (not is_rewrite and idx < len(base_cells) - and is_reading_cell(base_cells[idx]) - and is_reading_cell(cell)): + and base_cells[idx].get("cell_type") == "markdown" + and cell.get("cell_type") == "markdown"): is_rewrite = True if is_rewrite: base_counter[src] += 1 diff --git a/scripts/tests/test_check_split_reading_cells.py b/scripts/tests/test_check_split_reading_cells.py index b8bcecf2b2..68d22888a9 100644 --- a/scripts/tests/test_check_split_reading_cells.py +++ b/scripts/tests/test_check_split_reading_cells.py @@ -859,6 +859,54 @@ def test_diff_exempte_reecriture_en_place_d_une_lecture(): assert findings == [] +def test_diff_exempte_reecriture_en_place_d_une_cellule_sans_id_non_lecture(): + """#17747 -- revision en place d'une cellule markdown SANS id et NON + classee lecture (titre « Exercice 3 ») : ce n'est pas un ajout. + + Mesure fondatrice (OR-tools-Stiegler, tranche SymbolicAI de #17498) : un + simple echappement de `$` (``39,66 $`` -> ``39,66 \\$``) dans cette + cellule suffisait a la faire passer pour un ajout -- les trois signaux + REWRITE manquaient (source modifiee, pas d'id, pas deux lectures) -- et le + cliquet rougissait le geste que le mandat user PRESCRIT : « si on rajoute + une lecture, on modifie le paragraphe de lecture existant ». + """ + base = nb( + code("solver.Solve();"), + md("### Exercice 3 : Analyse de sensibilite du regime optimal\n" + "La solution optimale indique 5 aliments pour un cout de 39,66 $/an."), + ) + head = nb( + code("solver.Solve();"), + md("### Exercice 3 : Analyse de sensibilite du regime optimal\n" + "La solution optimale indique 5 aliments pour un cout de 39,66 \\$/an."), + ) + assert detect_added_readings(head, base) == [] + + +def test_diff_mord_si_l_empilement_remplace_le_slot_voisin(): + """Controle NEGATIF de #17747 : le signal de revision ne couvre QUE le + slot qu'il occupe. Un empilement qui pousse une lecture la ou la base + portait du code reste rouge, meme si la cellule du dessus a ete revisee + en place dans la meme PR -- sinon le signal serait un robinet ouvert. + """ + base = nb( + code("print(1)"), + md("### Analyse du resultat\nAncienne formulation."), + code("print(2)"), + ) + head = nb( + code("print(1)"), + md("### Analyse du resultat\nNouvelle formulation."), + md("### Lecture chiffree : le score atteint 0.94"), + code("print(2)"), + ) + findings = detect_added_readings(head, base) + # La revision du slot 1 est exemptee ; la lecture empilee au slot 2 (ou la + # base portait du code) reste signalee. + assert len(findings) == 1 + assert findings[0]["cells"] == [2] + + def test_diff_mute_si_ordre_inchange_et_contenu_identique(): """Une PR qui ne touche PAS au notebook ne signale rien (sanity check).""" base = head = nb(code("print(1)"))