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)"))